node-storage
node-storage
In sintesi
Servizio di persistenza file e BLOB del tenant. Astrae il backend di storage dietro un'interfaccia StorageProvider con due implementazioni intercambiabili — MinIO (S3-compatible) per produzione e filesystem locale per sviluppo, test e deployment edge — e arricchisce ogni file con un livello di metadati transazionali in PostgreSQL (tabella allegati) collegabili a qualsiasi entità di dominio tramite tabelle di abbinamento many-to-many. Aggiunge sopra questi due livelli un sistema di link temporanei basato su Redis con TTL automatico, pensato per consentire download autenticati senza esporre credenziali del backend a chi consuma. È il deposito unico dei documenti generati dagli altri microservizi (PDF di stampa, render di template, export Excel, allegati di ticket e board).
Funzionalità principali
- Storage multi-backend con strategia selezionabile via
STORAGE_PROVIDER(minio|local), interfaccia unicaStorageProvider, factory che istanzia entrambi all'avvio e li registra nel container DI - Upload sicuro: multipart (fino a 50 MB / 50 file per richiesta di default) e variante base64-over-JSON per pipeline orchestrate; validazione MIME su whitelist (
STORAGE_ALLOWED_MIME_TYPES), controllo magic bytes e scansione ClamAV opzionale - Allegati tracciati: ogni file ha un record in
allegati(idallegato uuid, nomefile, dimensione, mime_type, datacaricamento) e zero/uno/più reference verso entità parent (es.boardelementiallegato.idboardelementi) tramite tabelle di abbinamento — schema convenzionale PZeta in italiano minuscolo - Soft-delete + cleanup orfani: rilevazione di file presenti in storage ma senza record DB (o viceversa) e job di pulizia idempotente via endpoint
admin - Link temporanei con token random 32-byte hex, TTL configurabile in giorni e numero massimo di download; consumo atomico via Lua script Redis che previene race condition TOCTOU sui download concorrenti
- Path confinement: validazione anti path-traversal (rifiuto di
.., path assoluti e segmenti vietati come.git,node_modules) applicata sia al provider locale sia al servizio applicativo - Rollback transazionale dell'upload: se la
INSERTsu PostgreSQL fallisce dopo la scrittura del BLOB, il file viene rimosso dallo storage per evitare orfani - Modulo orchestrator: si registra come modulo
@pzeta/orchestrator-nodeesponendo 4 task riusabili dai workflow (upload-file-base64,download-file,list-files,create-temporary-link) — pattern thin module su `node-orchestrator*
Architettura
Stack: Fastify v5 · @pzeta/fastify-utils (security, error handler, OpenAPI, validation, health, metrics, tracing, multipart wiring) · Inversify (DI con @injectable/@inject) · PostgreSQL via driver pg diretto · Redis (redis client v5) · MinIO SDK ufficiale minio v8 · @pzeta/orchestrator-node per integrazione hub · @pzeta/log per logging strutturato · Zod v4 per validazione I/O.
Layout DDD (src/):
| Layer | Contenuto |
|---|---|
domain/ | Aggregate Attachment (con reference verso entità parent) e TemporaryLink; value object AttachmentId, FileName, FileSize, MimeType, FilePath, Token, ExpiryDate, DownloadCounter; servizi FileValidationService, ImageProcessingService, IAntivirusService; errori tipizzati (StorageError, AttachmentError, TemporaryLinkError) |
application/ | Use case (UploadAttachmentUseCase, DownloadAttachmentUseCase, DeleteAttachmentUseCase, DeleteMultipleAttachmentsUseCase, ListAttachmentsByReferenceUseCase, FindOrphanedFilesUseCase, CleanOrphanedFilesUseCase, GetAttachmentUseCase); StorageService di basso livello; DTO e mapper; porte (IStorageService, ITemporaryLinksService) |
infrastructure/ | MinioStorageProvider, LocalStorageProvider, StorageFactory; PostgresAttachmentRepository; DatabaseService (transazioni + sanitizeIdentifier per identificatori dinamici); RedisTemporaryLinksService con Lua script atomico; ClamAVService opzionale; OrchestratorAdapter con manifest e 4 handler |
presentation/ | StorageController, AttachmentsController, TemporaryLinksController con plugin di route Fastify e schema Zod per request/response |
Pattern adottati: Strategy/Provider per i backend, Factory per la selezione runtime, Repository per la persistenza degli allegati, Use Case con rollback compensativo, Adapter verso orchestrator-node, Singleton per il client Redis.
Casi d'uso
- Archivio PDF da
node-print: il servizio di stampa produce un PDF (etichetta, documento di trasporto, ricevuta) e lo deposita innode-storagecon una reference alla riga business che lo ha generato; il frontend lo recupera viaGET /attachments/{id}/downloado tramite link temporaneo per condivisione esterna - Asset template per
node-renderer: template di stampa, font custom, loghi cliente e immagini di sfondo sono caricati come allegati conreferenceTable=template_assets; al render timenode-rendererchiamaGET /attachmentsfiltrando per reference e scarica il binario viadownload-filetask dell'orchestrator - Allegati di entità di dominio: ticket di assistenza, schede board (
boardelementi), anagrafiche e ordini espongono allegati tramite tabelle di abbinamento (boardelementiallegato,anagraficaallegato, ecc.); l'integrità referenziale è garantita daON DELETE CASCADEdalla tabellaallegati - Export ricorrente da
node-excel-export: un workflow schedulato genera un report.xlsx, lo carica via task orchestratorupload-file-base64e produce un link temporaneo (TTL 7 giorni, 1 download) inviato per email all'utente destinatario senza che l'utente abbia accesso al portale autenticato - Condivisione anonima controllata: l'operatore genera da UI un link temporaneo per condividere un documento con un fornitore esterno; il fornitore scarica il file via
GET /download/{token}senza credenziali, il contatore di download viene decrementato atomicamente e il link viene eliminato al raggiungimento del limite o alla scadenza Redis (TTL nativo)
Identità & esposizione
| Campo | Valore |
|---|---|
| Categoria | storage |
| Versione cluster | 1.0.11 |
| Image | gitea.pzetatouch.it/pzeta_touch/node-storage:1.0.8 |
| URL pubblico | https://ditta.pzeta.it/storage |
| Path regex ingress | `/storage(/ |
| Rewrite a backend | /$2 |
| DNS interno | node-storage-ditta.ditta.svc.cluster.local:3000 |
| Auth nginx | auth_request → node-user-auth |
| Repository | node-storage |
| Endpoint REST | 19 (vedi sezione "API reference") |
Endpoint operazionali
Endpoint convenzionali esposti da tutti i microservizi PZeta basati su @pzeta/fastify-utils:
| Path pubblico | Scopo |
|---|---|
https://ditta.pzeta.it/storage/health | liveness probe |
https://ditta.pzeta.it/storage/ready | readiness probe |
https://ditta.pzeta.it/storage/metrics | metriche Prometheus |
https://ditta.pzeta.it/storage/api-docs.json | spec OpenAPI runtime (richiede OPENAPI_EXPOSE_IN_PRODUCTION=true) |
https://ditta.pzeta.it/storage/api-docs | Swagger UI (solo in NODE_ENV !== production) |
Configurazione
Variabili rilevanti per un integratore (la lista esaustiva resta nel .env.example del repo):
| Variabile | Ruolo |
|---|---|
STORAGE_PROVIDER | minio (default in cluster) o local; pilota la StorageFactory |
STORAGE_MINIO_ENDPOINT / _PORT / _USE_SSL / _ACCESS_KEY / _SECRET_KEY / _BUCKET | Connessione al cluster MinIO; il bucket viene creato on-demand al primo upload se assente |
STORAGE_LOCAL_PATH | Base path del filesystem provider; soggetto a path confinement |
STORAGE_MAX_FILE_SIZE_BYTES | Limite per upload multipart (default 50 MB); applicato sia da @fastify/multipart sia dal FileValidationService |
STORAGE_ALLOWED_MIME_TYPES | Whitelist CSV di MIME accettati in upload; in assenza di valore esplicito viene applicata la whitelist sicura di default |
STORAGE_DISALLOWED_PATHS | Segmenti di path vietati (default .git,node_modules,.env,...) |
DB_HOST / _PORT / _NAME / _USER / _PASSWORD / _POOL_MAX | Connessione PostgreSQL per la tabella allegati e le tabelle di abbinamento |
REDIS_HOST / _PORT / _PASSWORD | Backend dei link temporanei; TTL gestito nativamente da Redis |
BASE_URL | URL pubblico usato per costruire l'url restituito alla creazione di un link temporaneo |
ORCHESTRATOR_ENABLED / ORCHESTRATOR_URL / ORCHESTRATOR_MODULE_ID / ORCHESTRATOR_API_KEY | Registrazione opzionale come modulo presso node-orchestrator |
Isolamento per tenant: il servizio non è multi-tenant a livello applicativo. L'isolamento è ottenuto in modo dichiarativo a livello di deployment: un'istanza per tenant (node-storage-<tenant> nel namespace omonimo), un bucket MinIO dedicato (STORAGE_MINIO_BUCKET=storage-<tenant> per convenzione), uno schema PostgreSQL separato per la tabella allegati. L'autenticazione nginx davanti all'ingress garantisce che le richieste non possano traversare istanze di tenant diversi.
Note eventing NATS
Il servizio non è connesso direttamente a NATS in questa release: nats.json è vuoto, non ci sono publish né subscribe statici verso il broker. La comunicazione asincrona con il resto della piattaforma avviene esclusivamente tramite il pattern thin module dell'orchestrator: OrchestratorAdapter istanzia un ModuleSDK di @pzeta/orchestrator-node che effettua il registration al boot (manifest con i 4 task pubblici), riceve i dispatch da node-orchestrator via HTTP request/reply, e ne ritorna l'esito. È l'orchestrator, non node-storage, a produrre gli eventi NATS di completamento task (workflow.events.task.*) consumati dalla timeline di esecuzione.
L'apertura del servizio a eventi di dominio nativi (es. storage.object.created / storage.object.deleted da consumare da node-notification o da workflow reattivi) è un'evoluzione naturale ma non ancora implementata: il pubblicatore tipico sarebbe UploadAttachmentUseCase dopo il commit transazionale e DeleteAttachmentUseCase dopo la conferma dello storage. Fino a quel momento, chi vuole reagire a un nuovo allegato deve costruire un workflow esplicito che invochi upload-file-base64 e poi un task downstream.
Dipendenze e dipendenti
Dipende da (servizi che questo servizio chiama):
Consumato da (chi chiama questo servizio):
node-printnode-rendererfrontend Vue
Infrastruttura (PostgreSQL, NATS, Redis, MinIO) non è elencata qui — vedi sezione Architettura del singolo servizio.