A local-first RAW photo management system for home networks. Drop RAW files into an import directory and RAW Manager automatically ingests them — deduplicating, extracting EXIF metadata, generating thumbnails, and making them available through a web gallery for browsing, rating, and culling.
Personal project. This app is built around my own RAW archiving and culling workflow. It is opinionated by design — the development pipeline maps directly to how I personally work with RAW files from shoot to final library.
RAW Manager sits between the camera SD card and a finished photo library. The steps I follow:
SD Card (automounted to /import)
│
▼
RAW Manager — ingest & deduplicate
│
▼
Cull "pending" RAWs — pick ✓ or reject ✗
│
▼
Move picks to "In Darkroom" ──► Darktable opens DARKROOM_DIR,
edits RAWs, exports JPEGs
│
▼
Upload JPEGs to Immich (main photo library)
│
▼
Mark RAWs as "Developed" in RAW Manager
In practice:
- My NAS automounts the SD card to the
IMPORT_DIRpath (/import), so importing is just inserting the card - I open RAW Manager in a browser, filter by pending, and quickly reject or keep shots using keyboard shortcuts
- Keepers I flag as in darkroom — RAW Manager hard-links those files into the
DARKROOM_DIRfolder - Darktable watches that folder; I develop the RAWs there and export JPEGs
- JPEGs go into Immich, my main photo archive
- Once processed I mark the RAWs as developed to keep the library clean
Planned: Immich integration
In the near future I plan to close the loop automatically:
- Watch the Darktable export folder and auto-upload JPEGs to Immich via the Immich API
- When an image is confirmed stored in Immich, automatically set the corresponding RAW to developed
- This would make the entire pipeline — from card insert to library — fully hands-off after the culling step
- Automatic watchdog — drop RAW files into
IMPORT_DIR; they're processed without manual steps - Deduplication — SHA256 hashing silently discards duplicate files
- EXIF metadata — camera, lens, ISO, aperture, shutter speed, focal length, date
- Tone-mapped thumbnails — JPEG previews rendered from RAW data using camera white balance
- Infinite-scroll grid, dark-themed, responsive
- Filter by rating, archive status, development status, camera model, and date range
- Filename search
- Month-based timeline scrubber for navigating large libraries
- 5-star ratings — rate individual or batches of photos
- Archive status —
pending/keep/discard - Development pipeline —
consideration→in darkroom→developed - Darkroom folder — files marked in darkroom are automatically hard-linked to
DARKROOM_DIRfor external editing
- Full-screen photo viewer with EXIF panel
- Zoom & pan — mouse wheel zoom, click-drag pan, double-click to toggle 100%/fit, pinch-to-zoom
- Navigate with keyboard or arrow buttons
- Rate, set status, set dev status, download original, or delete directly from the lightbox
- Select exactly 2 photos → Compare button appears in the selection bar
- Side-by-side view with synchronized zoom & pan (lock/unlock sync)
- Overlay / swipe mode — drag a divider to reveal each photo
- Drag & drop RAW files anywhere on the page — a full-screen drop target appears
- Or click Upload in the header to browse for files
- Files are validated against supported extensions before upload
- Queued files are picked up automatically by the ingestion service
- Select any number of photos; apply rating, status, or dev status to all in one API call
- Batch download — selected originals as a streaming ZIP archive
- Toast notifications for every action: ratings, status changes, errors, rebuild complete
| Extension | Camera Brand |
|---|---|
.cr2 |
Canon (DSLR) |
.cr3 |
Canon EOS R series |
.arw |
Sony |
.nef |
Nikon |
.rw2 |
Panasonic |
.raf |
Fujifilm |
.orf |
Olympus |
.dng |
Adobe / Universal |
.heif / .hif |
Various (experimental) |
Requirements: Docker and Docker Compose.
git clone <repo-url> RAW-Manager
cd RAW-Manager
docker compose up --build -dOpen http://localhost:8000 in your browser.
To import photos, copy RAW files into ./data/import/. The ingestion service processes them automatically — or use the Upload button in the web UI.
When a RAW file lands in the import directory:
- Settle check — waits 2 s, confirms file size is stable (guards against partial writes)
- Deduplication — computes SHA256; skips if hash already exists in the database
- Metadata extraction — runs
exiftoolto pull all EXIF fields - Safe copy — moves the file to
ORIGINALS_DIR/YYYY/MM/filenamewith hash verification - Thumbnail generation — renders a tone-mapped JPEG via
rawpy+ Pillow, resized toTHUMB_SIZE - Database insert — writes the record to SQLite
- Cleanup — removes the original from the import directory
data/
├── import/ # Drop RAW files here (or upload via the web UI)
├── originals/ # Organized storage: YYYY/MM/filename
├── thumbnails/ # Content-addressed JPEGs (named by SHA256 hash)
├── darkroom/ # Hard-links to files marked "in darkroom"
└── raw_manager.db # SQLite database (WAL mode)
Gallery (no lightbox open):
| Key | Action |
|---|---|
1 – 5 |
Batch rate selected photos |
K |
Batch keep selected |
X |
Batch reject selected |
Lightbox:
| Key | Action |
|---|---|
1 – 5 |
Set star rating |
P |
Mark pending |
K |
Mark keep |
X |
Mark discard |
← / → |
Previous / next photo |
Escape |
Close lightbox |
| Scroll | Zoom in/out |
| Double-click | Toggle 100% / fit |
Compare mode:
| Key | Action |
|---|---|
V |
Toggle side-by-side / overlay |
S |
Toggle sync lock |
Escape |
Close compare |
| Variable | Default | Description |
|---|---|---|
IMPORT_DIR |
./data/import |
Directory watched for incoming RAW files |
ORIGINALS_DIR |
./data/originals |
Organized RAW storage |
THUMB_DIR |
./data/thumbnails |
Generated JPEG thumbnails |
DB_PATH |
./data/raw_manager.db |
SQLite database path |
DARKROOM_DIR |
./data/darkroom |
Darkroom file copies |
API_HOST |
0.0.0.0 |
API bind address |
API_PORT |
8000 |
API bind port |
THUMB_SIZE |
800 |
Maximum thumbnail dimension (px) |
Interactive docs (Swagger UI): http://localhost:8000/docs
| Method | Path | Description |
|---|---|---|
GET |
/api/photos |
List photos with filters and cursor pagination |
GET |
/api/photos/count |
Count matching photos |
GET |
/api/photos/timeline |
Photo counts grouped by year-month |
GET |
/api/photos/{id} |
Single photo by ID |
PATCH |
/api/photos/{id} |
Update rating, status, dev_status |
PATCH |
/api/photos/batch |
Batch-update multiple photos in one transaction |
DELETE |
/api/photos/{id} |
Delete photo record and original file |
GET |
/api/files/thumb/{id} |
Serve thumbnail JPEG (immutable cache) |
GET |
/api/files/original/{id} |
Serve original RAW file |
POST |
/api/upload |
Upload RAW files to IMPORT_DIR (multipart, max 20) |
POST |
/api/download |
Stream selected originals as ZIP |
POST |
/api/rebuild |
Trigger background database rebuild |
GET |
/api/rebuild/status |
Rebuild progress |
GET |
/api/stats |
Aggregate library statistics |
GET |
/api/cameras |
Distinct camera models |
GET |
/api/health |
Health check |
RAW-Manager/
├── Dockerfile # Multi-stage: Node builds frontend, Python serves everything
├── docker-compose.yml # api + ingestion services
├── requirements.txt
├── backend/
│ ├── main.py # FastAPI app — all endpoints
│ ├── database.py # SQLite CRUD layer with schema versioning
│ ├── ingestion.py # Watchdog-based ingestion engine
│ └── utils/
│ ├── exif_utils.py # EXIF extraction via exiftool
│ ├── file_utils.py # SHA256, safe copy, path organisation
│ └── raw_processing.py # RAW → JPEG via rawpy + Pillow
├── docs/
│ ├── screenshot-gallery.jpg
│ ├── screenshot-lightbox.png
│ └── screenshot-compare.png
└── frontend/
├── vite.config.js # Dev server on :5173, proxies /api → :8000
├── tailwind.config.js # Dark theme, custom surface palette
└── src/
├── App.jsx
├── components/
│ ├── CompareView.jsx # Side-by-side / overlay compare
│ ├── DropZone.jsx # Drag-and-drop upload
│ ├── Gallery.jsx # Infinite-scroll photo grid
│ ├── Lightbox.jsx # Full-screen viewer with zoom/pan
│ ├── PhotoCard.jsx
│ ├── Settings.jsx # Rebuild dialog
│ ├── StarRating.jsx
│ ├── TimelineScrubber.jsx
│ ├── Toolbar.jsx
│ └── ToastContainer.jsx # Toast notification renderer
└── hooks/
├── useKeyboardShortcuts.js
├── usePhotos.js
├── useToast.jsx
└── useZoomPan.js # Zoom/pan state and event handlers
# Debian / Ubuntu
sudo apt install libimage-exiftool-perl libraw-devpython -m venv .venv && source .venv/bin/activate
pip install -r requirements.txt
uvicorn backend.main:app --host 0.0.0.0 --port 8000 --reload # API
python -m backend.ingestion # Ingestion (separate terminal)cd frontend
npm install
npm run dev # http://localhost:5173 — proxies /api to :8000| Layer | Technology |
|---|---|
| Backend | Python 3.12, FastAPI, SQLite 3 (WAL), rawpy, Pillow, watchdog |
| Frontend | React 18, Vite, Tailwind CSS 3 |
| Deployment | Docker, Docker Compose |


