Paul Nothaft 576924fa57 fix(faces): scan external/reference photos instead of skipping them (#1090) (#1091)
* fix(faces): scan external/reference photos instead of skipping them (#1090)

faceProcessor short-circuited every photo with source_origin 'external'
or 'reference' straight to 'skipped', before the sidecar was ever
contacted. On an external-media install that is the entire library — the
reporter's gallery sat at 0/3230 with every row skipped and no error, and
a rescan changed nothing.

The guard was correct when written: resolvePhotoStorageKey returns null
for anything outside managed storage, so ensurePreviewImage could not
build a preview and there was nothing to send. #1078 removed that
limitation one release earlier — ensurePreviewImage now reads externals
straight off the mount via resolvePhotoFilePath and writes the preview
into managed storage, so the key faceProcessor already fetches through
getStorage() is readable like any other. The guard outlived its reason.

Photos whose source is genuinely gone still return a null preview key
and land in the existing 'failed' branch, which is the honest outcome:
that is a broken photo, not an unsupported one. The blanket skip was
absorbing those too.

No migration or manual reset needed — enqueueEvent already re-queues
rows with face_status in (NULL, 'failed', 'skipped'), so previously
skipped photos get picked up on the next scan.

* fix(faces): queue external imports for scanning (#1090)

The other half of the same bug, found by external review — and my first
counter-argument against it was wrong.

Managed uploads are enqueued by photoProcessor, which writes face_status
'pending' once a photo is processed (photoProcessor.js:573, commented as
"the only correct place to enqueue"). External media never goes through
photoProcessor at all: adminExternalMedia inserts rows directly, leaving
face_status NULL.

faceQueue.claimNextPhoto only claims 'pending' (faceQueue.js:64), so an
import into an already-enabled event produced nothing until someone
pressed Re-scan. Lifting the skip guard alone made external photos
scannable but still not scanned — which looks like a complete fix right
up until you import a photo.

Resolved once per import rather than per file, since it is a per-event
setting and the loop can run to a thousand files, and guarded on both
the global flag and the per-event toggle exactly as photoProcessor
guards it, so installs without the feature still never write a
face_status. A failure to read the setting logs and imports anyway — the
photos are the point.

No video guard: walkDir only collects jpg/jpeg/png/webp, so nothing
faceProcessor would skip as video can arrive through this route.

* fix(faces): enqueue imports only after the event path is written

External review caught a race I introduced in the previous commit.

Marking rows 'pending' as they were inserted published claimable work
while events.external_path still held the old value — or none at all, on
a first import, since the route only writes it after the entire
thumbnail loop. The face worker polls continuously, so on any import
long enough to matter (the loop is ~100-300ms per photo, and the
reporter's library is 6500+) it would claim those rows, resolve them
against the wrong directory and mark them permanently 'failed' — a state
only an explicit Re-scan clears. That is strictly worse than the
unscanned photos this set out to fix.

Ids are now collected during the loop and marked pending in one pass
after the event path is written, chunked at 500 because SQLite caps a
statement at 999 bound parameters.

The test now drives the real route instead of re-implementing its logic,
and observes the mid-loop state from inside the per-photo thumbnail
call — the only hook that can see the window the race lived in. Verified
it discriminates: deleting the enqueue fails two tests, and moving it
back onto the insert fails the ordering test specifically.

* fix(faces): read the face setting after the import, not before

Third external-review round. The setting was captured before a loop that
runs for many minutes on a large library, so an admin who enabled
detection during an import left every photo imported after that moment
at NULL forever — the toggle endpoint only queues rows that already
existed when it fired.

Ids are now collected unconditionally and the setting is evaluated
immediately before the queue update, off a freshly read event row. The
guard is unchanged in substance: both the global flag and the per-event
toggle, so installs without the feature still never write a face_status.

Test flips the toggle from inside the mocked per-photo thumbnail call,
which is the same mid-loop hook the ordering test uses.

---------

Co-authored-by: Paul Nothaft <paul@MacStudio-von-Paul.local>
2026-08-20 07:18:45 +02:00

PicPeak Logo

📸 PicPeak

Open-source, self-hosted photo sharing for events.

License: MIT Docker Buy Me A Coffee

Homepage · Live Demo · Documentation · Support


PicPeak is a powerful, self-hosted open-source alternative to commercial photo-sharing platforms like PicDrop.com and Scrapbook.de. Built for photographers and event organizers, it makes it simple to share beautiful, time-limited photo galleries with clients while keeping full control over your data and branding.

PicPeak Gallery Preview

Important

PicPeak has moved to its own GitHub organization. Docker images are now at ghcr.io/picpeak/picpeak/{backend,frontend} and active development is on main. The old ghcr.io/the-luap/... path still responds but its tags are frozen at 2026-05-27 — if updates never arrive, check your image path first. See docs/migration-to-org.md for the one-line docker-compose.yml edit.

Contents

🎮 Live Demo

Try PicPeak without installing anything — demo.picpeak.app · admin panel

Email Password
demo@picpeak.app Demo2026!

The demo resets periodically. Uploaded content may be removed without notice.

🚀 Quick Start

Get PicPeak running in under 5 minutes:

# Clone the repository
git clone https://github.com/PicPeak/picpeak.git
cd picpeak

# Copy the environment template — the defaults work out of the box.
# Machine secrets (JWT, DB, Redis) are auto-generated on first run, and the
# admin account is created in the browser. Edit .env only to customise
# (domain, SMTP, storage paths, …) — nothing is required.
cp .env.example .env

# Start with Docker Compose
docker compose up -d

# Access at http://localhost:3000

On first start, open http://localhost:3000/admin and follow the in-browser setup to create your admin account. Full details — the one-time setup token, Docker file permissions, and ARM64 notes — are in First-run setup.

Updating / release channels: set PICPEAK_CHANNEL (stable default, or beta) in .env, then docker compose pull && docker compose up -d. See RELEASING.md for the promotion cadence.

Or: one container, no compose file

For a home server, a NAS, or a single small studio, the all-in-one image runs the whole app as one process with SQLite — no compose file, no separate database, no reverse proxy to wire up:

docker run -d --name picpeak -p 3000:3000 \
  -v picpeak:/data \
  -e JWT_SECRET="$(openssl rand -base64 48)" \
  ghcr.io/picpeak/picpeak/aio:stable

Then open http://localhost:3000/admin and read the setup token with docker exec picpeak cat /data/db/SETUP_TOKEN.

The compose stack above is still the right choice for anything busier — SQLite takes one writer at a time, and Postgres is what scales. You can move to it later without reinstalling: take a .picpeak backup and restore it into the full stack. See Single-container install for the volume layout, the external-Postgres variant, TLS, and the limits.

🌟 Why PicPeak?

Unlike expensive SaaS solutions, PicPeak gives you:

  • 💰 No Monthly Fees — one-time setup, unlimited galleries
  • 🔒 Complete Data Control — your photos stay on your server
  • 🎨 White-Label Ready — full branding customization
  • 📱 Mobile-First Design — beautiful on all devices
  • 🌍 Multi-Language — built-in i18n (EN, DE)

Features

For photographers — drag & drop upload, auto-expiring & password-protected galleries, automated emails, an analytics dashboard, custom themes, a public landing page, and a Live Slideshow projector view that auto-picks-up new uploads during live events.

For clients — clean mobile-optimized galleries, one-click bulk downloads, smart search, optional guest uploads, and download protection (watermarking + right-click prevention).

Technical — Docker-ready, automatic thumbnail generation, external media reference mode, smart archiving of expired galleries, S3-compatible storage backends, webhooks, and security-first defaults (JWT, rate limiting, CORS).

🧾 For studios — CRM & Accounting (Beta, off by default)
  • 📝 Quotes → Contracts → Invoices — one deal lineage; cancel-and-reissue (Storno) keeps issued invoices immutable
  • ⏱️ Hours Logging & Calendar — per-customer time tracking; admin calendar of events, logged hours, and pending quotes/contracts
  • 🧾 Inbound Supplier Invoices & Expenses — capture received invoices (upload/camera, rasterised server-side), categorise, and re-bill costs to clients
  • 📊 Tax Report & Accountant Export — period-scoped income/cost report with VAT breakdown; PDF/CSV plus a Treuhänder/Banana (Swiss/LI) journal export
  • 🌍 VAT & Multi-currency — single VAT-code registry snapshotted onto each document

Warning

CRM & Accounting — examples only, verify locally. Feature-flagged off by default. Seeded contract blocks are written by the maintainer, not a lawyer; QR-bills/SEPA payloads and every tax, VAT and Treuhänder/Banana figure are computed from your input and defaults and are jurisdiction-specific guidance only. Have your lawyer review contracts, scan a test QR with your bank's app, and verify all numbers with your accountant / Treuhänder / tax authority before customer-facing use. Read the CRM disclaimers first.

📖 Documentation

Full documentation lives at docs.picpeak.app — deployment, admin settings, API, branding, and more.

Topic Link
🚀 Deployment (Docker, env, reverse proxy, SSL) docs.picpeak.app/deployment
📦 Single-container install (one docker run, SQLite) docs.picpeak.app/deployment/single-container
⚙️ Admin settings reference docs.picpeak.app/guides/admin-settings
🎯 Creating events docs.picpeak.app/guides/creating-events
📽️ Live Slideshow docs.picpeak.app/features/live-slideshow
💾 Backup & Restore docs.picpeak.app/guides/backup-restore
🔌 API reference docs.picpeak.app/api
🪝 Webhooks docs.picpeak.app/features/webhooks
💾 Storage backends (local / S3) docs.picpeak.app/features/storage-backends
💻 System requirements & tuning docs.picpeak.app/deployment/system-requirements
🧾 CRM & Accounting docs.picpeak.app/features/crm · disclaimers
🗺️ Roadmap GitHub Issues

Project meta: Contributing · License · Security · Code of Conduct

📊 Comparison with Alternatives

Feature PicPeak PicDrop Scrapbook.de Pixieset
Self-Hosted
Custom Branding Full Limited Limited (paid)
Monthly Cost $0* $29-199 €19-99 ~$60
Storage Limit Unlimited** 50-500GB 100-1000GB 3GBUnlimited***
Client Uploads Limited
API Access Paid
Open Source
Customer Accounts
Quotes / Contracts / Invoices 🧪 Beta
Incoming Invoices & Accounting 🧪 Beta

*You bring your own server and, optionally, a domain. **Limited only by your server storage. ***Pixieset's "unlimited" is photos only; video is capped by plan. 🧪 Beta = built but feature-flagged off by default.

🏗️ Tech Stack

  • Backend: Node.js, Express, SQLite/PostgreSQL
  • Frontend: React, Tailwind CSS, Framer Motion
  • Storage: Local filesystem (default) or S3-compatible object store (AWS S3, MinIO, R2, B2, Wasabi, Spaces) — see Storage Backends
  • Email: SMTP with customizable templates
  • Analytics: Privacy-focused with Umami integration
  • External media: point PicPeak at EXTERNAL_MEDIA_ROOT to reference existing originals read-only, index quickly, and generate thumbnails on demand

📸 Screenshots

Click to see the admin dashboard, analytics, and event management

🎛️ Admin Dashboard

PicPeak Admin Dashboard

📊 Analytics & Insights

PicPeak Analytics Dashboard

📁 Event Management

PicPeak Events Management

🤝 Contributing

We love contributions! PicPeak is built by photographers, for photographers — whether you're fixing bugs, adding features, or improving docs. See the Contributing Guide to get started.

Found a security issue? Please open a security issue. See SECURITY.md for the policy.

Support the Project

PicPeak is free, open source, and self-hostable forever. If it saves you time or replaces a paid subscription, consider buying me a coffee — it directly funds new features, bug fixes, and keeping the demo + docs running. You can also star the repo, share it, file good bug reports, or open a PR.

🙏 Acknowledgments

PicPeak is inspired by the best features of commercial platforms while remaining completely open source. It's developed with AI assistance, but human-tested end-to-end, security-audited, and human-reviewed for quality.

👥 Contributors

A huge thank you to the people whose code, reports, and feedback have shaped PicPeak:

@the-luap — creator and lead maintainer

  • Gallery foundation (events, uploads, sharing, download protection, templates)
  • Backup & restore, analytics, branding/theming
  • The architecture every later feature builds on

@Luca-Timo

  • Native Apple Silicon multi-arch images
  • CRM & accounting suite (quotes/contracts/invoices)
  • Hours logging & Treuhänder/Banana tax export
  • Gallery header/banner decoupling

@Rekoo-PS — bug reports & product feedback

  • Login-loop fix, mobile-lightbox overhaul, bulk-delete workflow
  • Also a BuyMeACoffee supporter

If you've contributed and aren't listed here, please open a PR — this list is meant to grow.

📄 License

PicPeak is released under the MIT License. Use it freely for personal or commercial projects.


Made with ❤️ by photographers, for photographers
Homepage · Live Demo · Documentation · Support

S
Description
Secure photo sharing platform for weddings and events with automatic expiration and email notifications
Readme MIT 157 MiB
2025-07-24 15:24:20 +02:00
Languages
JavaScript 59.8%
TypeScript 38.5%
Shell 0.7%
Python 0.5%
CSS 0.4%
Other 0.1%