aboutsummaryrefslogtreecommitdiffstats
path: root/README.md
blob: df1f37edb089f55d3d1eee206ab1c13f88754694 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
# MedMan — Self-Hosted Media Manager with Tagging & squashr Integration

MedMan is a web-based media manager designed to work alongside
[squashr](https://git.kug.is/squashr.git) (SquashFS/OverlayFS/LUKS backup tool).

## Overview

```
1. Drop files into source directories (or upload via web UI)
       ↓
2. MedMan scans/ingests → files are distributed into data directories
   using a full-hash directory hierarchy (every 2 hex chars = one level)
       ↓
3. Browse & tag files through the web interface
       ↓
4. Export tags → archival.py packs them into SquashFS containers
       ↓
5. Burn containers to CD/DVD/Blu-Ray or archive offline
       ↓
6. Offline use: mount container, start MedMan, import index.json →
   browse & search media with full tag metadata
```

## Configuration

MedMan is configured primarily through **Docker volume mounts**. Environment
variables are minimal:

| Variable | Description | Default |
|---|---|---|
| `MEDMAN_PORT` | HTTP port | `8080` |
| `MEDMAN_HOST` | Bind address | `0.0.0.0` |
| `MEDMAN_W` | Comma-separated weights for data-dir distribution | equal weight |

### Directory Structure

All directories are inside `/app` in the container and wired via
`docker-compose.yml` mounts:

```
/app/
├── srcs/           ← source directories (immediate subdirs)
│   ├── src0/       ← mount: ./src0:/app/srcs/src0
│   └── src1/       ← mount: ./src1:/app/srcs/src1
├── dats/           ← data directories (immediate subdirs)
│   ├── data0/      ← mount: ./data0:/app/dats/data0
│   └── data1/      ← mount: ./data1:/app/dats/data1
├── imports/        ← auto-import directories (immediate subdirs, read-only)
│   └── my-archive/ ← mount: /mnt/archive:/app/imports/my-archive:ro
└── db/             ← SQLite database + thumbnails (auto-created)
```

**Key principle**: Every immediate subdirectory of `srcs/`, `dats/`, and
`imports/` is automatically discovered. No comma-separated path lists in
environment variables.

### Data Distribution Weights

`MEDMAN_W` controls how new files are distributed across data directories:

```bash
# 3 data dirs — dir 0 gets 50%, dirs 1 and 2 get 25% each
MEDMAN_W=2,1,1
```

If not set, distribution is equal across all data directories.

## Hash-Based Storage

Every file is SHA-256 hashed. All 32 byte-pairs of the 64-character hex
hash become directory levels:

```
Hash:  a1b2c3d4e5f6...a1b2c3d4e5f6 (64 chars)
Path:  data0/a1/b2/c3/d4/e5/f6/.../a1/b2/c3/d4/e5/f6
```

- 32 directory levels + filename (last hex pair)
- Automatic deduplication via content hashing
- Distribution across data directories according to `MEDMAN_W` weights
- No `rel` column stored in the database — the path is always deterministically
  computed from the hash

## Quickstart (Docker)

```bash
cd medman
docker compose up -d
```

MedMan is available at `http://localhost:8080`.

Drop files into `src0/` or `src1/` — they are automatically scanned and
distributed into the data directories on startup. Upload is also supported
via the web UI.

## Auto-Import on Startup

Directories mounted under `/app/imports/` are scanned on startup (and on
each manual scan). Each subdirectory may contain:

- `index.json` — a MedMan export file with tags and file references
- Files in the canonical hash-prefix hierarchy (same layout as data dirs)

All tags and file registrations from `index.json` are imported automatically
if the referenced files exist on disk. Import directories receive zero
distribution weight (new files are never placed there).

Example: Mount a read-only SquashFS container containing an `index.json` and
canonical file paths:

```yaml
# docker-compose.yml
volumes:
  - /mnt/my-archive:/app/imports/my-archive:ro
```

## Features

- **Source directories**: files auto-ingested on startup, upload via UI
- **Manual scan button**: re-scan source and import directories anytime
- **Hash-based storage**: content-addressable, no duplicates
- **Multiple data directories**: weighted random distribution
- **Move**: relocate individual files or selections between data dirs
- **Tag system**: plain labels, no color coding
- **Tag search**: combined AND search across multiple tags
- **Bulk operations**: select-all, batch tag/untag/move/delete
- **Previous/Next navigation**: arrow keys or buttons in detail view
- **Checkbox selection**: click the checkbox on any thumbnail to toggle
- **Export/Import**: tags as JSON (export/import)
- **archival.py**: create SquashFS + LUKS containers directly from MedMan
- **Container mount API**: mount SquashFS containers as additional data dirs

## Database Schema

```sql
tags(id, name, ts)
files(hash PK, didx, size, mt, w, h, dur, ts)
ftags(id, hash FK→files, tid FK→tags, UNIQUE(hash, tid))
```

`didx` is the index into the `dats/` subdirectory list. The on-disk path
is always `dats/<name>/<h2rel(hash)>`.

## API

| Endpoint | Method | Description |
|---|---|---|
| `/api/files` | GET | All files (`?tag=` filter) |
| `/api/files/<hash>` | DELETE | Delete file |
| `/api/files/<hash>/move` | POST | `{"didx":N}` move to data dir |
| `/api/files/<hash>/tags` | GET/POST | Get or add tags |
| `/api/files/<hash>/tags/<id>` | DELETE | Remove tag from file |
| `/api/tags` | GET/POST | List or create tags |
| `/api/tags/<id>` | DELETE | Delete tag |
| `/api/tags/export` | GET | Full tag export (`?tags=a,b` AND filter) |
| `/api/tags/import` | POST | Import tag JSON |
| `/api/search?tags=a,b` | GET | AND tag search |
| `/api/upload` | POST | Upload file(s) |
| `/api/scan` | POST | Scan sources + imports |
| `/api/dat` | GET | Data directory stats |
| `/api/containers/mount` | POST | Mount container dir as data dir |
| `/api/containers/mount` | DELETE | Unmount container dir |
| `/thumb/<hash>` | GET | JPEG thumbnail |
| `/raw/<hash>` | GET | Original file |

## Export Format (index.json)

```json
{
  "ts": "2025-01-15T12:00:00+00:00",
  "tags": [{"id": 1, "name": "example"}],
  "files": {
    "a1b2c3d4...": {"didx": 0, "tags": ["example"]}
  }
}
```

The on-disk path for each file is deterministic: `h2rel(hash)` → 32 hex-pair
directory levels. No `rel` field is needed.

## archival.py — Creating SquashFS Containers

```bash
# Build containers via MedMan API
python3 archival.py --medman-url http://localhost:8080 \
    --dat /app/data0 /app/data1 \
    --tags vacation 2024 \
    --container-size 4700372992 \
    --output-prefix ./archive

# Build with LUKS encryption
python3 archival.py --db /app/db/medman.db \
    --dat /app/data0 /app/data1 \
    --tags photos \
    --container-size 25000000000 \
    --output-prefix ./photos \
    --cryptsetup --key-file /path/to/keyfile

# Direct DB access (no running MedMan needed)
python3 archival.py --db /app/db/medman.db \
    --dat /app/data0 /app/data1 \
    --container-size 10000000000 \
    --output-prefix ./offline
```

The script uses `tar → sqfstar` piping (no staging directories, no temp
files beyond a single index.json) — exactly like squashr's
`build_squash_image_tar_sqfstar`.

### Container Size Margin

A margin of 1 MiB is reserved per container for the `index.json` and
filesystem metadata overhead. Adjust with `--margin` if needed.

### Filter Options

| Option | Description |
|---|---|
| `--tags a b` | Only files with ALL given tags (AND) |
| `--hashes a1b2...` | Specific hashes |
| `--hashes-from-file` | Read hashes from file |
| `--from-date 2024-01-01` | Modified on or after |
| `--to-date 2024-12-31` | Modified on or before |
| `--mime-type video` | Filter by media type |

## squashr Integration

MedMan containers produced by `archival.py` are standard SquashFS images
with an `index.json` at the root plus the canonical hash hierarchy. They
can be mounted and imported into any MedMan instance via the auto-import
feature or the container mount API.

## License

GNU AGPLv3