# Photobooth FTP Ingestion
This guide explains how to operate the Photobooth FTP workflow end‑to‑end: 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 event’s 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`):
```env
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:
```bash
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/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: ``
- Failure: ``
- 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 event’s hot storage, applies thumbnail/watermark/security scan, sets `photos.ingest_source = sparkbooth`.
Example cURL (JSON response):
```bash
curl -X POST https://app.example.com/api/v1/photobooth/upload \
-F "media=@/path/to/photo.jpg" \
-F "username=PB123" \
-F "password=SECRET" \
-F "message=Wedding booth"
```
Example cURL (request XML response):
```bash
curl -X POST https://app.example.com/api/v1/photobooth/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.