Files
fotospiel-app/docs/ops/photobooth

Photobooth FTP Ingestion

This guide explains how to operate the Photobooth FTP workflow endtoend: provisioning FTP users for tenants, running the ingest pipeline, and exposing photobooth photos inside the Guest PWA.

Architecture Overview

  1. vsftpd container (port 2121) accepts uploads into a shared volume (default /var/www/storage/app/photobooth). Each event receives isolated credentials and a dedicated directory.
  2. Control Service (REST) provisions FTP accounts. Laravel calls it during enable/rotate/disable actions.
  3. Photobooth settings (Filament SuperAdmin) define global port, rate limit, expiry grace, and Control Service connection.
  4. Ingest command copies uploaded files into the events storage disk, generates thumbnails, records photos.ingest_source = photobooth, and respects package quotas.
  5. Guest PWA filter consumes /api/v1/events/{token}/photos?filter=photobooth to render the “Fotobox” tab. Sparkbooth uploads reuse this filter via ingest_source = sparkbooth.
Photobooth -> FTP (vsftpd) -> photobooth disk
       photobooth:ingest (queue/scheduler)
            -> Event media storage (public disk/S3)
            -> packages_usage, thumbnails, security scan

Sparkbooth -> HTTP upload endpoint -> ingest (direct, no staging disk)

Environment Variables

Add the following to .env (already scaffolded in .env.example):

PHOTOBOOTH_CONTROL_BASE_URL=https://control.internal/api
PHOTOBOOTH_CONTROL_TOKEN=your-control-token
PHOTOBOOTH_CONTROL_TIMEOUT=5

PHOTOBOOTH_FTP_HOST=ftp.internal
PHOTOBOOTH_FTP_PORT=2121

PHOTOBOOTH_USERNAME_PREFIX=pb
PHOTOBOOTH_USERNAME_LENGTH=8
PHOTOBOOTH_PASSWORD_LENGTH=8

PHOTOBOOTH_RATE_LIMIT_PER_MINUTE=20
PHOTOBOOTH_EXPIRY_GRACE_DAYS=1

PHOTOBOOTH_IMPORT_DISK=photobooth
PHOTOBOOTH_IMPORT_ROOT=/var/www/storage/app/photobooth
PHOTOBOOTH_IMPORT_MAX_FILES=50
PHOTOBOOTH_ALLOWED_EXTENSIONS=jpg,jpeg,png,webp

# Sparkbooth defaults (optional overrides)
SPARKBOOTH_ALLOWED_EXTENSIONS=jpg,jpeg,png,webp
SPARKBOOTH_MAX_SIZE_KB=8192
SPARKBOOTH_RATE_LIMIT_PER_MINUTE=20
SPARKBOOTH_RESPONSE_FORMAT=json

Filesystem Disk

config/filesystems.php registers a photobooth disk that must point to the shared volume where vsftpd writes files. Mount the same directory inside both the FTP container and the Laravel app container.

Control Service Contract

Laravel expects the Control Service to expose:

POST   /users                  { username, password, path, rate_limit_per_minute, expires_at, ftp_port }
POST   /users/{username}/rotate { password, rate_limit_per_minute, expires_at }
DELETE /users/{username}
POST   /config                 { ftp_port, rate_limit_per_minute, expiry_grace_days }

Authentication is provided via PHOTOBOOTH_CONTROL_TOKEN (Bearer token).

Scheduler & Commands

Command Purpose Default schedule
photobooth:ingest [--event=ID] [--max-files=N] Pulls files from the Photobooth disk and imports them into the event storage. every 5 minutes
photobooth:cleanup-expired De-provisions FTP accounts after their expiry. hourly

You can run the ingest job manually for a specific event:

php artisan photobooth:ingest --event=123 --max-files=20

Sparkbooth HTTP Uploads (Custom Upload)

Use this when Sparkbooth runs in “Custom Upload” mode instead of FTP.

  • Endpoint: POST /api/v1/photobooth/sparkbooth/upload
  • Auth: per-event username/password (set in Event Admin → Fotobox-Uploads; switch mode to “Sparkbooth”).
  • Body (multipart/form-data): media (file or base64), username, password, optionally name, email, message.
  • Response:
    • JSON success: {"status":true,"error":null,"url":null}
    • JSON failure: {"status":false,"error":"Invalid credentials"}
    • XML (if format=xml or event preference is XML):
      • Success: <rsp status="ok" url="..."/>
      • Failure: <rsp status="fail"><err msg="Invalid credentials" /></rsp>
  • Limits: allowed extensions reuse photobooth defaults; max size SPARKBOOTH_MAX_SIZE_KB (default 8 MB); per-event rate limit SPARKBOOTH_RATE_LIMIT_PER_MINUTE (fallback to photobooth rate limit).
  • Ingest: writes straight to the events hot storage, applies thumbnail/watermark/security scan, sets photos.ingest_source = sparkbooth.

Example cURL (JSON response):

curl -X POST https://app.example.com/api/v1/photobooth/sparkbooth/upload \
  -F "media=@/path/to/photo.jpg" \
  -F "username=PB123" \
  -F "password=SECRET" \
  -F "message=Wedding booth"

Example cURL (request XML response):

curl -X POST https://app.example.com/api/v1/photobooth/sparkbooth/upload \
  -F "media=@/path/to/photo.jpg" \
  -F "username=PB123" \
  -F "password=SECRET" \
  -F "format=xml"

Tenant Admin UX

Inside the Event Admin PWA, go to Event → Fotobox-Uploads to:

  1. Enable/disable the Photobooth link.
  2. Rotate credentials (max 10-char usernames, 8-char passwords).
  3. Switch mode (FTP or Sparkbooth), view rate limit + expiry info, copy ftp:// or POST URL + creds.

Guest PWA Filter

The Guest gallery now exposes a “Fotobox” tab (both preview card and full gallery). API usage:

GET /api/v1/events/{token}/photos?filter=photobooth
Headers: X-Device-Id (optional)

Response items contain ingest_source, allowing the frontend to toggle photobooth-only views.

Operational Checklist

  1. Set env vars from above and restart the app.
  2. Ensure vsftpd + Control Service are deployed; verify port 2121 and REST endpoint connectivity.
  3. Mount shared volume to /var/www/storage/app/photobooth (or update PHOTOBOOTH_IMPORT_ROOT + filesystems.disks.photobooth.root).
  4. Run migrations (php artisan migrate) to create settings/event columns.
  5. Seed default storage target (e.g., MediaStorageTarget::create([... 'key' => 'public', ...])) in non-test environments if not present.
  6. Verify scheduler (Horizon or cron) is running commands photobooth:ingest and photobooth:cleanup-expired.
  7. Test end-to-end: enable Photobooth on a staging event, upload a file via FTP, wait for ingest, and confirm it appears under the Fotobox filter in the PWA.
  8. Test Sparkbooth: switch event mode to Sparkbooth, copy Upload URL/user/pass, send a sample POST (or real Sparkbooth upload), verify it appears under the Fotobox filter.