feat: simplify folder setup — single mount, auto bootstrap, browser dialog
Cleans up the maze of overlapping ways folders entered the app, plus
removes the dead trash plumbing left over from the soft-discard
refactor.
Setup model (now)
- ONE env var: PHOTO_DIRS in .env, set to the host path of your
library. Compose mounts that at /photos. That's the entire setup.
- On first boot, the backend auto-creates a SourceRoot row named
"Library" pointing at /photos so the user sees their photos
immediately without configuring anything.
- Source roots and discard live in the database; mulita.yml only
carries operational settings (thumbnails, scanner, performance).
- The "Add Source Folder" dialog is now a directory browser
restricted server-side to /photos and any existing source root —
the user clicks through actual mounted directories instead of
typing container paths they can't possibly know.
Backend
- New services/scanner.bootstrap_default_source_root(): if no
SourceRoot rows exist and /photos is mounted, create one. Wired
into the lifespan handler before cleanup + initial scan.
- New GET /library/browse?path= returning the immediate child
directories of `path`, validated to live under one of the allowed
roots (default mount + every active SourceRoot). Hidden entries
are filtered. Children are tagged with is_existing_root so the UI
can show an "Added" badge. Returns parent path for up-nav, or
null when at the top of the allowed scope.
- scan_all_source_roots now reads from the DB instead of the YAML
config so DB-managed source roots are honoured by initial scan.
- Dropped the placeholder source_roots block from mulita.yml — the
paths /photos/main and /photos/iphone never existed and just
produced startup warnings.
- Dropped TrashSettings, settings.trash, settings.source_roots,
and the SourceRoot pydantic model from config.py. Soft discard
has owned this for a while; it was dead code.
Compose
- Single ${PHOTO_DIRS:-./photos}:/photos:rw mount in both backend
and worker.
- Removed the hardcoded ~/Pictures:/host/Pictures:rw mount — the
PHOTO_DIRS variable is the single source of truth now.
- Removed the trash_data named volume + mounts (no consumers).
- backend/Dockerfile no longer creates /data/trash; it now creates
/data/proxies (which the proxy endpoint actually uses).
Frontend
- AddSourceFolderDialog rewritten as a directory tree picker:
loads /library/browse on open, lets the user navigate up via a
ChevronUp button or down by clicking subfolders, shows the
current path inline, and adds whatever directory is currently
shown. Existing source roots are tagged "Added" so the user
knows what's already registered. Errors from the backend (e.g.
trying to navigate outside the allowed scope) surface inline.
- New library.browse() helper + BrowseChild / BrowseResponse types
in services/api.ts.
Docs
- README Quick Start rewritten around the single PHOTO_DIRS env
var, with macOS/Linux/Windows examples.
- New "How mounted folders and source folders relate" section that
spells out the two-layer model (mount = visibility, source root
= scanning) so the most common confusion is addressed up front.
- Added a "Read-only libraries" subsection that lists exactly which
endpoints fail under :ro.
- "Configuration" section reframed: source roots are managed by the
UI/API now, mulita.yml is operational settings only.
- .env file now has examples for the common host paths.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
110
README.md
110
README.md
@@ -34,53 +34,96 @@ A self-hosted, Docker-deployed photo management application inspired by Lightroo
|
||||
|
||||
### Prerequisites
|
||||
- Docker and Docker Compose
|
||||
- Photo directories to mount
|
||||
|
||||
### Setup
|
||||
### Setup (one variable)
|
||||
|
||||
1. Clone the repository:
|
||||
1. Clone the repo:
|
||||
```bash
|
||||
git clone <repository-url>
|
||||
cd muleimage
|
||||
```
|
||||
|
||||
2. Configure your photo directories in `.env`:
|
||||
2. Set **one** environment variable in `.env` — the **host** directory
|
||||
that contains your photo library. Whatever you point at will become
|
||||
your library inside Mulita.
|
||||
|
||||
```bash
|
||||
# macOS / Linux
|
||||
PHOTO_DIRS=/Users/you/Pictures
|
||||
|
||||
# or any folder
|
||||
PHOTO_DIRS=/mnt/nas/photos
|
||||
|
||||
# Windows (WSL)
|
||||
PHOTO_DIRS=/mnt/c/Users/you/Pictures
|
||||
```
|
||||
|
||||
3. Start the stack:
|
||||
```bash
|
||||
# Edit .env file
|
||||
PHOTO_DIRS=/path/to/your/photos
|
||||
docker compose up -d
|
||||
```
|
||||
|
||||
3. Start the application:
|
||||
```bash
|
||||
docker-compose up -d
|
||||
4. Open `http://localhost:3000`. On first boot Mulita will:
|
||||
- Mount your `PHOTO_DIRS` at `/photos` inside the container
|
||||
- Auto-create a source root called **Library** pointing at `/photos`
|
||||
- Queue an initial scan, generate thumbnails, and start serving them
|
||||
|
||||
You don't need to touch `mulita.yml` or the API to get started.
|
||||
|
||||
### How "mounted folders" and "source folders" relate
|
||||
|
||||
There are two layers, and confusing them is the most common source of
|
||||
"why doesn't this work" questions:
|
||||
|
||||
| Layer | Lives in | What it controls |
|
||||
|---|---|---|
|
||||
| **Mount** | `docker-compose.yml` (`${PHOTO_DIRS}:/photos:rw`) | What's *visible* inside the container |
|
||||
| **Source root** | Database (managed by the UI) | What the scanner *walks and indexes* |
|
||||
|
||||
Both are required. A folder that isn't mounted is invisible to the
|
||||
container regardless of what the database says, and a folder that's
|
||||
mounted but not registered as a source root won't be scanned.
|
||||
|
||||
In practice, the default flow handles this for you: you mount one host
|
||||
directory via `PHOTO_DIRS`, and the bootstrap automatically registers it
|
||||
as a source root. If you want to add a *subfolder* of your library as a
|
||||
separate source root (so it shows up as its own item in the sidebar),
|
||||
use the **Add Source Folder** button — the dialog is a directory browser
|
||||
restricted to what's mounted, so you can only add things the container
|
||||
can actually see.
|
||||
|
||||
### Adding more libraries
|
||||
|
||||
Today the compose file mounts a single host directory as `/photos`.
|
||||
If you want multiple libraries from different host paths, edit
|
||||
`docker-compose.yml` and add additional mount lines, e.g.:
|
||||
|
||||
```yaml
|
||||
volumes:
|
||||
- ${PHOTO_DIRS}:/photos:rw
|
||||
- /Volumes/Archive:/archive:rw # additional library
|
||||
```
|
||||
|
||||
4. Access the application at `http://localhost:3000`
|
||||
Then in the UI, click **Add Source Folder**, browse to `/archive`, and
|
||||
add it. (Multi-mount support via env vars is on the roadmap.)
|
||||
|
||||
### Photo directory mounts and permissions
|
||||
### Read-only libraries
|
||||
|
||||
Mulita is a Lightroom-style manager — file operations (rename, move,
|
||||
discard, empty discard pile) need to mutate the filesystem under your
|
||||
photo mounts. The default `docker-compose.yml` mounts:
|
||||
|
||||
- `${PHOTO_DIRS}` → `/photos` (read-write)
|
||||
- `~/Pictures` → `/host/Pictures` (**read-write** by default so file
|
||||
operations work on your system Pictures folder out of the box)
|
||||
|
||||
If you want a strict read-only library — for example pointing at a
|
||||
network share or your authoritative archive — change `:rw` to `:ro`
|
||||
on the mount in `docker-compose.yml`. Mulita will keep working for
|
||||
browsing, rating, color labels, picks, heaps, and the discard flag,
|
||||
but the following endpoints will return an error from the OS
|
||||
(`EROFS` / `Read-only file system`):
|
||||
The default mount is `:rw` because file operations (rename, move,
|
||||
empty discard pile) need to mutate the filesystem. If you want a
|
||||
strict read-only library — pointing at a network share, an
|
||||
authoritative archive, etc. — flip `:rw` to `:ro` in
|
||||
`docker-compose.yml`. Mulita will keep working for browsing, rating,
|
||||
color labels, picks, heaps, and the (soft) discard flag, but the
|
||||
following will return an OS error:
|
||||
|
||||
- `PATCH /photos/{id}` with a new `filename` (rename)
|
||||
- `POST /photos/move` (bulk move)
|
||||
- `DELETE /discard/empty` (file unlinks)
|
||||
- Future move / copy endpoints
|
||||
|
||||
**Heads up**: with `:rw`, Mulita has full write access to whatever
|
||||
host directory you mount under `~/Pictures`. Treat the same way you
|
||||
would Lightroom's catalog folder.
|
||||
host directory you mount. Treat the same way you would Lightroom's
|
||||
catalog folder.
|
||||
|
||||
## Architecture
|
||||
|
||||
@@ -126,11 +169,12 @@ npm run dev
|
||||
|
||||
## Configuration
|
||||
|
||||
Edit `mulita.yml` to configure:
|
||||
- Source photo directories
|
||||
- Thumbnail sizes and quality
|
||||
- Scanner settings
|
||||
- Performance tuning
|
||||
Source roots are managed by the UI / API (the database owns them). Edit
|
||||
`mulita.yml` to configure operational settings only:
|
||||
|
||||
- Thumbnail sizes, quality, and format
|
||||
- Scanner behaviour (watch, batch size, initial scan)
|
||||
- Performance tuning (concurrency, cache TTLs, DB pool)
|
||||
|
||||
## Performance
|
||||
|
||||
|
||||
Reference in New Issue
Block a user