Passa al contenuto principale

Media storage

Flo memorizza gli upload tramite un'astrazione (IStorageProvider) con due implementazioni, selezionate da STORAGE_PROVIDER:

ProviderSTORAGE_PROVIDERStore di appoggioURL pubblici
LocalFileSystemStorageProviderlocal (default)wwwroot/uploads sul volume del containerServiti dalle API (ApiBaseUrl)
R2StorageProviderr2Cloudflare 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:

  1. Presign — POST /api/v1/storage/presign (o POST /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.

  2. 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.

  3. Confirm — POST /api/v1/storage/confirm verifica:

    • 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-Type memorizzato 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.

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):

LimiteValore
Dimensione max file100 MB (104.857.600 byte)
Max pixel immagini100 MP (GIF/WebP animati: 50 MP totali, best-effort lato frontend)
Tipi MIME immagineimage/jpeg, image/png, image/gif, image/webp
Contenitore videovideo/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/mp4 ovunque; tipi documento (application/pdf, DOCX/XLSX/PPTX, ZIP, TXT, CSV) solo nella cartella attachments.
  • 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 _pending sono 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 providerImmagineVideo
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 originaleDownload → 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_resizing sia enable_media_optimization sono attivi, CF vince per OptimizedUrl.
  • Richiede un hostname CDN capace di trasformazioni: un dominio personalizzato dentro una zona Cloudflare con Trasformazioni abilitate. Un dominio gestito *.r2.dev nudo 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 width più stretta; le dimensioni delle immagini sono rilevate tramite format=json e quelle dei video tramite ffprobe, poi persistite in MediaDimension così 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:

MetodoEndpointRuoloScopo
POST/runSuperAdminAvvio di una scansione ({ entityTypes?: string[] }, omesso = tutti gli abilitati)
POST/cancelSuperAdminAnnullamento della scansione in corso
GET/statusAdmin/SuperAdminUltima esecuzione, dettaglio per tipo, feed live, tipi disponibili
GET/runs?take=NAdmin/SuperAdminCronologia esecuzioni
GET/flagged?entityType=XProMedia 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​

FileScopo
Flo.BE/Services/Storage/IStorageProvider.csAstrazione dello storage
Flo.BE/Services/Storage/R2StorageProvider.cs / LocalFileSystemStorageProvider.csImplementazioni dei provider
Flo.BE/Services/Storage/StorageUploadService.csFlusso presign → confirm, validazione, ridenominazione
Flo.BE/Services/Storage/StorageMediaUrlPolicy.csValidazione 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.csSuperficie HTTP
shared/cloudflare-media-limits.jsonLimiti di upload condivisi con il frontend

Correlati: FFmpeg, Feature flag, Variabili d'ambiente.