Media storage
Flo memorizza gli upload tramite un'astrazione (IStorageProvider) con due implementazioni, selezionate da STORAGE_PROVIDER:
| Provider | STORAGE_PROVIDER | Store di appoggio | URL pubblici |
|---|---|---|---|
LocalFileSystemStorageProvider | local (default) | wwwroot/uploads sul volume del container | Serviti dalle API (ApiBaseUrl) |
R2StorageProvider | r2 | Cloudflare R2 (S3-compatibile) | R2_CDN_URL/<key> |
Quando il lease di failover è armato (FAILOVER_ENABLED=true), il provider è avvolto da PrimaryLeaseStorageProvider, che nega le scritture su un nodo standby.
Variabili d'ambiente rilevanti: STORAGE_PROVIDER, R2_ENDPOINT, R2_ACCESS_KEY, R2_SECRET_KEY, R2_BUCKET, R2_CDN_URL, R2_TENANT_PREFIX — vedi Variabili d'ambiente.
Flusso di upload (presign → PUT → confirm)
I client autenticati (e i token API pubblici con media.write) caricano tramite un flusso in due fasi implementato da StorageUploadService:
-
Presign —
POST /api/v1/storage/presign(oPOST /api/public/v1/{entityRoute}/{id}/media/presign) valida la richiesta e restituisce un URL PUT prefirmato più una chiave_pending:{tenant}/_pending/{folder}/{guid}.{ext}I presign sono legati all'utente richiedente e al content type dichiarato. Scadenza: 5 minuti per le immagini, 15 minuti per video/documenti.
-
PUT del client — il browser carica direttamente su R2 (nessun byte transita dalle API). Il provider locale accetta invece un classico upload multipart via
POST /api/v1/storage/upload. -
Confirm —
POST /api/v1/storage/confirmverifica:- che la chiave pending appartenga al tenant e si trovi sotto
/_pending/; - che lo stesso utente che ha fatto il presign stia confermando;
- che il
Content-Typememorizzato corrisponda a quello del presign; - che la dimensione sia sotto il limite di Cloudflare (
< 100 MB); - che i primi 64 byte corrispondano ai magic byte attesi per il tipo.
In caso di successo l'oggetto è spostato da
_pending/alla chiave finale, la proprietà è registrata e un job di ottimizzazione è accodato per immagini/video. - che la chiave pending appartenga al tenant e si trovi sotto
Le ridenominazioni (POST /api/v1/storage/rename) sono limitate all'uploader o allo staff (Admin/Pro); i nomi file sono sanificati e le chiavi restano dentro il prefisso del tenant.
Regole di validazione
La validazione è guidata da shared/cloudflare-media-limits.json, unica fonte di verità condivisa con il frontend (CloudflareMediaLimits / media-validation.service.ts):
| Limite | Valore |
|---|---|
| Dimensione max file | 100 MB (104.857.600 byte) |
| Max pixel immagini | 100 MP (GIF/WebP animati: 50 MP totali, best-effort lato frontend) |
| Tipi MIME immagine | image/jpeg, image/png, image/gif, image/webp |
| Contenitore video | video/mp4, max 600 s, video H.264, audio AAC/MP3 |
Gli upload sono inoltre vincolati da:
- Cartelle consentite:
activities,galleries,users,immobili/immobiles,catalogitems/catalog-items,attachments,newsletter,activity-information,articles,authors. - Tipi di contenuto: immagini +
video/mp4ovunque; tipi documento (application/pdf, DOCX/XLSX/PPTX, ZIP, TXT, CSV) solo nella cartellaattachments. - Corrispondenza estensione ↔ content-type: l'estensione del file deve essere una delle estensioni esatte consentite per il tipo dichiarato.
- Policy URL media: i payload CRUD generici possono referenziare solo URL prodotti dallo storage provider del tenant (
StorageMediaUrlPolicy); URL esterni e chiavi_pendingsono rifiutati. - Rate limit: presign 120/min per utente, upload media 120/min per token/IP, scritture pubbliche di integrazione 300/min per token, flussi browser pubblici anonimi 60/min per IP.
Pipeline di ottimizzazione lato server
Dietro il flag enable_media_optimization. MediaOptimizationBackgroundService esegue il polling ogni 2 minuti (ed è risvegliato dai trigger di upload), elabora un job alla volta (prevenzione OOM su VPS condivisi), ritenta fino a 3 volte e pulisce le directory residue media-temp/ all'avvio. Il ramo dipende dalle capacità del provider:
| Capacità del provider | Immagine | Video |
|---|---|---|
Locale (SupportsImageOptimization=true, SupportsServerSideVideoOptimization=false) | Ottimizzata in place su disco (sostituzione atomica solo se più piccola) | Ottimizzato in place |
R2 (SupportsImageOptimization=false, SupportsServerSideVideoOptimization=true) | Download → varianti → upload → cancellazione originale | Download → varianti → upload → cancellazione originale |
Varianti immagine (R2): larghezze 320, 640, 1024, 1920 WebP più un WebP a dimensione originale; le varianti più larghe della sorgente sono saltate; le sorgenti oltre 8000 px sono prima ridimensionate. Varianti video (R2): 480p, 720p, 1080p H.264 MP4, saltando le altezze sopra la sorgente. Le varianti sono memorizzate accanto all'originale con suffisso _320w.webp / _480p.mp4 e registrate come righe MediaVariant indicizzate dalla chiave storage sorgente. Il blob originale è eliminato solo dopo che esiste almeno una variante.
Le letture risolvono le varianti memorizzate all'URL della variante più grande (o a un srcset), con una cache read-through MediaDimension per le dimensioni intrinseche.
Ridimensionamento edge di Cloudflare
Dietro il flag enable_cf_image_resizing. Quando attivo e lo storage è supportato da CDN, il backend riscrive gli URL media verso le trasformazioni Cloudflare invece di servire le varianti memorizzate:
https://cdn.example.com/cdn-cgi/image/format=auto,quality=80,width=1920/<path> # immagini
https://cdn.example.com/cdn-cgi/media/mode=video,width=720/<path> # video
- Priorità: quando sia
enable_cf_image_resizingsiaenable_media_optimizationsono attivi, CF vince perOptimizedUrl. - Richiede un hostname CDN capace di trasformazioni: un dominio personalizzato dentro una zona Cloudflare con Trasformazioni abilitate. Un dominio gestito
*.r2.devnudo restituisce 404 su/cdn-cgi/, quindi il backend lo tratta come incapace di trasformazioni e restituisce l'URL originale. - Le miniature delle card usano una
widthpiù stretta; le dimensioni delle immagini sono rilevate tramiteformat=jsone quelle dei video tramite ffprobe, poi persistite inMediaDimensioncosì il probe gira una volta sola per blob. - Gli URL che contengono già
/cdn-cgi/sono lasciati intatti (idempotente).
Scansione di compatibilità media
Console SuperAdmin (Impostazioni > Compatibilità media) che trova i media che violano i limiti di Cloudflare, così gli asset incompatibili possono essere corretti manualmente — non esiste remediation automatica. Gli endpoint vivono sotto /api/v1/media-scan:
| Metodo | Endpoint | Ruolo | Scopo |
|---|---|---|---|
| POST | /run | SuperAdmin | Avvio di una scansione ({ entityTypes?: string[] }, omesso = tutti gli abilitati) |
| POST | /cancel | SuperAdmin | Annullamento della scansione in corso |
| GET | /status | Admin/SuperAdmin | Ultima esecuzione, dettaglio per tipo, feed live, tipi disponibili |
| GET | /runs?take=N | Admin/SuperAdmin | Cronologia esecuzioni |
| GET | /flagged?entityType=X | Pro | Media segnalati, tutti i tipi o uno |
Sei adapter IScannableMediaSource enumerano le entità con media: CatalogItem (controllato da enable_catalogo), Immobile (controllato da enable_immobili), Activity, User, Gallery e FileAttachment di tipo immagine/video. Ogni elemento è ispezionato per livelli (metadati HEAD, poi stream ffprobe completo) e il risultato è salvato come MediaScanResult con flag NeedsFixing ed elenco JSON dei problemi. Una singola sorgente in errore non interrompe mai l'esecuzione.
Riferimento al codice
| File | Scopo |
|---|---|
Flo.BE/Services/Storage/IStorageProvider.cs | Astrazione dello storage |
Flo.BE/Services/Storage/R2StorageProvider.cs / LocalFileSystemStorageProvider.cs | Implementazioni dei provider |
Flo.BE/Services/Storage/StorageUploadService.cs | Flusso presign → confirm, validazione, ridenominazione |
Flo.BE/Services/Storage/StorageMediaUrlPolicy.cs | Validazione URL media per i payload CRUD |
Flo.BE/Services/MediaOptimization/ | Pipeline in background, varianti, policy URL CF, probe dimensioni |
Flo.BE/Services/Media/ | Scansione compatibilità, ispezione, limiti Cloudflare |
Flo.BE/Controllers/StorageController.cs / MediaScanController.cs | Superficie HTTP |
shared/cloudflare-media-limits.json | Limiti di upload condivisi con il frontend |
Correlati: FFmpeg, Feature flag, Variabili d'ambiente.