Salta al contenuto principale

11 - Gestione Documenti: Versioning, Categorie e Condivisione

Il Sistema Documentale di Play The Event Nella gestione di un evento, i documenti rappresentano un asset critico: fatture, contratti, permessi, locandine,...

Cosa ne pensi di questo articolo?

11 - Gestione Documenti: Versioning, Categorie e Condivisione
Play the EventFoto:panumas nikhomkhaiPexels

In Breve

Il Sistema Documentale di Play The Event Nella gestione di un evento, i documenti rappresentano un asset critico: fatture, contratti, permessi, locandine,...

Il Sistema Documentale di Play The Event

Nella gestione di un evento, i documenti rappresentano un asset critico: fatture, contratti, permessi, locandine, certificati e ricevute devono essere organizzati, versionati e condivisi in modo sicuro. In Play The Event, il modulo documentale è stato progettato seguendo i principi del Domain-Driven Design, con DocumentoEvento come Aggregate Root che coordina versioning, categorizzazione automatica, estrazione metadati e condivisione tramite link temporanei.

Questo approccio garantisce integrità dei dati, tracciabilità completa delle modifiche e un'organizzazione automatica dei file in cartelle logiche basate sul tipo MIME e sulla categoria del documento.

Cosa Troverai in Questo Articolo

  • L'Aggregate Root DocumentoEvento e il pattern DDD per i documenti
  • Le 11 categorie predefinite e le categorie personalizzate per evento
  • L'auto-detection delle cartelle basata su MIME type e categoria
  • Il sistema di versioning con aggiunta e ripristino versioni
  • I link di condivisione con scadenza temporale e limite download
  • L'estrazione automatica dei metadati (EXIF, PDF, Office)
  • L'integrità dei file tramite hashing SHA-256
  • Il collegamento documenti-spese e lo storage organizzato

DocumentoEvento: l'Aggregate Root

DocumentoEvento è l'Aggregate Root del bounded context documentale. Ogni documento appartiene a un evento specifico, è caricato da un utente e gestisce internamente la propria cronologia di versioni e i link di condivisione. Il factory method crea() garantisce che ogni documento nasca in uno stato valido, con la prima versione già creata.

Entity DocumentoEvento - Struttura Principale
@Entity
@Table(name = "documenti_evento")
public class DocumentoEvento {

    private static final long MAX_DIMENSIONE_BYTES = 50L * 1024 * 1024; // 50MB

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private Long eventoId;
    private String titolo;           // max 200 caratteri
    private String descrizione;      // max 1000 caratteri

    @Enumerated(EnumType.STRING)
    private CategoriaDocumento categoria;

    private Long categoriaPersonalizzataId;

    @Enumerated(EnumType.STRING)
    private TipoCartella tipoCartella;

    private Integer versioneCorrente;
    private String nomeFileOriginale;
    private String mimeType;
    private Long dimensioneBytes;
    private Long caricatoDaId;
    private Long spesaId;            // collegamento opzionale a spesa

    @Convert(converter = JsonMapConverter.class)
    private Map<String, Object> metadati = new HashMap<>();

    @OneToMany(mappedBy = "documento", cascade = CascadeType.ALL)
    @OrderBy("numeroVersione DESC")
    private List<VersioneDocumento> versioni = new ArrayList<>();

    @OneToMany(mappedBy = "documento", cascade = CascadeType.ALL)
    private Set<LinkCondivisioneDocumento> linkCondivisione = new HashSet<>();
}

La tabella documenti_evento utilizza cinque indici per ottimizzare le query più frequenti: per evento, per categoria, per tipo cartella, per utente che ha caricato il documento e per spesa collegata. L'optimistic locking tramite @Version previene conflitti di aggiornamento concorrente.

Factory Method con Validazione Completa

Il costruttore è protected e accessibile solo tramite il factory method crea(), che valida tutti i parametri prima di creare l'istanza. Il tipo cartella viene determinato automaticamente in base al MIME type e alla categoria.

Factory Method DocumentoEvento.crea()
public static DocumentoEvento crea(
        Long eventoId, String titolo, String descrizione,
        CategoriaDocumento categoria, Long categoriaPersonalizzataId,
        String nomeFileOriginale, String mimeType,
        Long dimensioneBytes, Long caricatoDaId,
        String percorsoStorage, String hashContenuto) {

    validaParametriCreazione(eventoId, titolo, nomeFileOriginale,
                              mimeType, dimensioneBytes, caricatoDaId, percorsoStorage);

    DocumentoEvento doc = new DocumentoEvento();
    doc.eventoId = eventoId;
    doc.titolo = titolo.trim();
    doc.categoria = categoria != null ? categoria : CategoriaDocumento.ALTRO;
    doc.categoriaPersonalizzataId =
        doc.categoria == CategoriaDocumento.PERSONALIZZATA
            ? categoriaPersonalizzataId : null;
    doc.tipoCartella = TipoCartella.fromMimeTypeAndCategoria(mimeType, doc.categoria);
    doc.versioneCorrente = 1;

    // Crea la prima versione con relazione bidirezionale
    VersioneDocumento primaVersione = VersioneDocumento.crea(
        1, nomeFileOriginale, percorsoStorage,
        dimensioneBytes, mimeType, hashContenuto,
        caricatoDaId, "Versione iniziale"
    );
    primaVersione.setDocumento(doc);
    doc.versioni.add(primaVersione);

    return doc;
}

Le 11 Categorie Predefinite

Ogni documento caricato su Play The Event è associato a una delle 11 categorie predefinite nell'enum CategoriaDocumento. Ogni categoria include un nome visualizzabile e un'icona per l'interfaccia utente.

Enum CategoriaDocumento
public enum CategoriaDocumento {
    FATTURA("Fattura", "file-text"),
    SCONTRINO("Scontrino", "receipt"),
    PERMESSO("Permesso", "shield-check"),
    BANDO("Bando", "megaphone"),
    CONTRATTO("Contratto", "file-signature"),
    PREVENTIVO("Preventivo", "calculator"),
    ASSICURAZIONE("Assicurazione", "shield"),
    CERTIFICATO("Certificato", "award"),
    LOCANDINA("Locandina", "image"),
    PERSONALIZZATA("Categoria Personalizzata", "tag"),
    ALTRO("Altro", "file");

    private final String displayName;
    private final String icona;

    // Verifica se la categoria rappresenta una ricevuta
    public boolean isRicevuta() {
        return this == FATTURA || this == SCONTRINO;
    }

    // Suggerisce una categoria di default per un tipo MIME
    public static CategoriaDocumento fromMimeType(String mimeType) {
        if (mimeType != null && mimeType.startsWith("image/")) {
            return LOCANDINA;
        }
        return ALTRO;
    }
}

Le categorie coprono le esigenze tipiche della gestione eventi: documenti fiscali (FATTURA, SCONTRINO, PREVENTIVO), documenti legali (CONTRATTO, PERMESSO, BANDO, ASSICURAZIONE), documenti di qualifica (CERTIFICATO) e materiale promozionale (LOCANDINA). Il metodo isRicevuta() identifica fatture e scontrini per l'instradamento automatico nella cartella ricevute.

Categorie Personalizzate per Evento

Quando le 11 categorie predefinite non sono sufficienti, gli organizzatori possono creare categorie personalizzate specifiche per il proprio evento. L'entity CategoriaDocumentoPersonalizzata consente di definire nome, descrizione, icona, colore e ordine di visualizzazione per ogni categoria aggiuntiva.

Entity CategoriaDocumentoPersonalizzata
@Entity
@Table(name = "categorie_documento_evento",
    uniqueConstraints = @UniqueConstraint(
        name = "uq_categoria_evento_nome",
        columnNames = {"evento_id", "nome"}
    ))
public class CategoriaDocumentoPersonalizzata {

    private static final Pattern HEX_COLOR_PATTERN =
        Pattern.compile("^#[0-9A-Fa-f]{6}$");
    private static final String COLORE_DEFAULT = "#6b7280";
    private static final String ICONA_DEFAULT = "file-text";

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private Long eventoId;
    private String nome;         // max 50 caratteri, unico per evento
    private String descrizione;  // max 200 caratteri
    private String icona;        // nome icona Lucide
    private String colore;       // formato hex #RRGGBB
    private Integer ordine;      // ordine di visualizzazione
}

Il vincolo di unicità uq_categoria_evento_nome impedisce la creazione di categorie duplicate all'interno dello stesso evento. Il colore è validato con una regex che accetta esclusivamente il formato esadecimale #RRGGBB. Se non viene specificato un colore o un'icona, vengono applicati i valori di default.

Quando un documento utilizza la categoria PERSONALIZZATA, il campo categoriaPersonalizzataId del DocumentoEvento viene impostato con l'ID della categoria personalizzata corrispondente. Se la categoria non è PERSONALIZZATA, il campo viene automaticamente annullato.

Auto-Detection della Cartella

Il sistema organizza automaticamente i file in tre cartelle logiche in base al tipo MIME del file e alla categoria del documento. L'enum TipoCartella definisce la logica di instradamento.

Enum TipoCartella - Instradamento Automatico
public enum TipoCartella {
    DOCUMENTI("documenti"),
    IMMAGINI("immagini"),
    RICEVUTE("ricevute");

    private final String percorso;

    public static TipoCartella fromMimeTypeAndCategoria(
            String mimeType, CategoriaDocumento categoria) {

        // 1. Fatture e scontrini vanno SEMPRE nella cartella ricevute
        if (categoria != null && categoria.isRicevuta()) {
            return RICEVUTE;
        }

        // 2. I file con MIME type image/* vanno nella cartella immagini
        if (mimeType != null && mimeType.startsWith("image/")) {
            return IMMAGINI;
        }

        // 3. Tutto il resto va nella cartella documenti
        return DOCUMENTI;
    }
}

La priorità dell'instradamento è chiara: la categoria ha precedenza sul tipo MIME. Una fattura in formato PDF viene collocata nella cartella /ricevute e non in /documenti, perché la sua categoria (FATTURA) la identifica come ricevuta. Un'immagine JPEG senza categoria specifica di ricevuta finisce nella cartella /immagini.

Regole di Instradamento

  • /ricevute - Documenti con categoria FATTURA o SCONTRINO (qualsiasi formato file)
  • /immagini - File con MIME type image/* (JPEG, PNG, WebP, etc.)
  • /documenti - Tutti gli altri file (PDF, Word, Excel, testo, etc.)

Sistema di Versioning

Ogni documento mantiene una cronologia completa delle versioni. Quando un utente carica una nuova versione, il sistema incrementa il contatore versioneCorrente e crea un nuovo oggetto VersioneDocumento con tutti i metadati del file aggiornato.

Entity VersioneDocumento
@Entity
@Table(name = "versioni_documento",
    uniqueConstraints = @UniqueConstraint(
        name = "uq_documento_versione",
        columnNames = {"documento_id", "numero_versione"}
    ))
public class VersioneDocumento {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "documento_id", nullable = false)
    private DocumentoEvento documento;

    private Integer numeroVersione;     // progressivo, >= 1
    private String nomeFileOriginale;   // max 255 caratteri
    private String percorsoStorage;     // max 500 caratteri
    private Long dimensioneBytes;
    private String mimeType;
    private String hashContenuto;       // SHA-256, 64 caratteri hex
    private Long creatoDaId;
    private String notaVersione;        // max 500 caratteri
    private Instant creatoIl;
}

Aggiunta di una Nuova Versione

L'aggiunta di una versione è gestita interamente dall'Aggregate Root, che garantisce la consistenza dello stato interno. Il numero di versione viene incrementato automaticamente e i metadati del documento principale vengono aggiornati con i dati del nuovo file.

Metodo aggiungiVersione()
public VersioneDocumento aggiungiVersione(
        String nomeFileOriginale, String percorsoStorage,
        Long dimensioneBytes, String mimeType,
        String hashContenuto, Long caricatoDaId, String notaVersione) {

    validaParametriVersione(nomeFileOriginale, percorsoStorage,
                            dimensioneBytes, mimeType, caricatoDaId);

    this.versioneCorrente++;
    this.nomeFileOriginale = nomeFileOriginale;
    this.mimeType = mimeType;
    this.dimensioneBytes = dimensioneBytes;

    VersioneDocumento nuovaVersione = VersioneDocumento.crea(
        this.versioneCorrente, nomeFileOriginale, percorsoStorage,
        dimensioneBytes, mimeType, hashContenuto,
        caricatoDaId, notaVersione
    );
    nuovaVersione.setDocumento(this);
    this.versioni.add(nuovaVersione);

    return nuovaVersione;
}

Ripristino di una Versione Precedente

Il ripristino non sovrascrive la cronologia: crea una nuova versione con il contenuto della versione da ripristinare. In questo modo la storia rimane immutabile e ogni operazione è tracciata.

Metodo ripristinaVersione()
public VersioneDocumento ripristinaVersione(Integer numeroVersione, Long utenteId) {
    VersioneDocumento versioneDaRipristinare = versioni.stream()
        .filter(v -> v.getNumeroVersione().equals(numeroVersione))
        .findFirst()
        .orElseThrow(() -> new IllegalArgumentException(
            "Versione " + numeroVersione + " non trovata"
        ));

    // Crea una NUOVA versione con i dati della versione precedente
    return aggiungiVersione(
        versioneDaRipristinare.getNomeFileOriginale(),
        versioneDaRipristinare.getPercorsoStorage(),
        versioneDaRipristinare.getDimensioneBytes(),
        versioneDaRipristinare.getMimeType(),
        versioneDaRipristinare.getHashContenuto(),
        utenteId,
        "Ripristino versione " + numeroVersione
    );
}

Versioning Immutabile

Il ripristino di una versione precedente non elimina né modifica la cronologia esistente. Se un documento ha 3 versioni e si ripristina la versione 1, il risultato sarà una versione 4 con il contenuto identico alla versione 1. Questo approccio garantisce un audit trail completo e previene la perdita accidentale di dati.

Link di Condivisione Temporanei

I documenti possono essere condivisi esternamente tramite link temporanei che non richiedono autenticazione. L'entity LinkCondivisioneDocumento gestisce la generazione, la validazione e la revoca di questi link.

Entity LinkCondivisioneDocumento
@Entity
@Table(name = "link_condivisione_documento")
public class LinkCondivisioneDocumento {

    private static final Duration DURATA_MASSIMA = Duration.ofDays(30);

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "documento_id", nullable = false)
    private DocumentoEvento documento;

    private String token;            // 64 caratteri, UUID-based, unico
    private Instant scadenza;        // max 30 giorni dalla creazione
    private Long creatoDaId;
    private Integer numeroDownload;  // contatore incrementale
    private Integer maxDownload;     // limite opzionale (null = illimitato)
    private Boolean revocato;
    private Instant revocatoIl;
}

Validità Multi-Condizione

Un link è considerato valido solo se soddisfa tre condizioni simultanee: non è stato revocato, non è scaduto e non ha raggiunto il limite massimo di download.

Verifica Validità del Link
public boolean isValido() {
    // Condizione 1: non revocato manualmente
    if (revocato) {
        return false;
    }
    // Condizione 2: non scaduto temporalmente
    if (Instant.now().isAfter(scadenza)) {
        return false;
    }
    // Condizione 3: download rimanenti disponibili
    if (maxDownload != null && numeroDownload >= maxDownload) {
        return false;
    }
    return true;
}

public void registraDownload() {
    if (!isValido()) {
        throw new IllegalStateException(
            "Impossibile registrare download: link non valido");
    }
    this.numeroDownload++;
}

Il token viene generato combinando due UUID e troncando a 64 caratteri, garantendo unicità e imprevedibilità. La durata massima consentita per un link è di 30 giorni. La revoca è un'operazione idempotente: revocare un link già revocato non produce errori.

Generazione e Revoca dall'Aggregate Root

La creazione e la revoca dei link passano sempre dall'Aggregate Root, che mantiene il controllo completo sull'insieme dei link associati al documento.

Gestione Link di Condivisione
// Generazione di un nuovo link temporaneo
public LinkCondivisioneDocumento generaLinkCondivisione(
        Duration durata, Long creatoDaId, Integer maxDownload) {

    LinkCondivisioneDocumento link = LinkCondivisioneDocumento.crea(
        durata, creatoDaId, maxDownload
    );
    link.setDocumento(this);
    this.linkCondivisione.add(link);
    return link;
}

// Revoca di un link specifico tramite token
public void revocaLinkCondivisione(String token) {
    LinkCondivisioneDocumento link = linkCondivisione.stream()
        .filter(l -> l.getToken().equals(token))
        .findFirst()
        .orElseThrow(() -> new IllegalArgumentException(
            "Link non trovato: " + token));
    link.revoca();
}

// Query: tutti i link ancora attivi
public List<LinkCondivisioneDocumento> getLinkAttivi() {
    return linkCondivisione.stream()
        .filter(LinkCondivisioneDocumento::isValido)
        .collect(Collectors.toList());
}

Estrazione Automatica dei Metadati

Quando un file viene caricato, Play The Event estrae automaticamente i metadati disponibili utilizzando Apache Tika e la libreria metadata-extractor. I metadati estratti vengono salvati come JSON nel campo metadati del documento.

Interfaccia MetadatiDocumentoExtractor (Domain Port)
public interface MetadatiDocumentoExtractor {

    // Estrae tutti i metadati in base al tipo MIME
    Map<String, Object> estraiMetadati(
        InputStream inputStream, String nomeFile, String mimeType);

    // Specializzazioni per tipo di file
    Map<String, Object> estraiMetadatiImmagine(InputStream inputStream);
    Map<String, Object> estraiMetadatiPdf(InputStream inputStream);
    Map<String, Object> estraiMetadatiOffice(
        InputStream inputStream, String mimeType);

    boolean isFormatoSupportato(String mimeType);
}

Metadati per Immagini (EXIF, IPTC, XMP)

Per le immagini, il sistema estrae metadati da quattro directory principali: EXIF IFD0 (marca fotocamera, modello, software, orientamento), EXIF SubIFD (tempo di esposizione, apertura, ISO, lunghezza focale, flash), GPS (latitudine, longitudine, altitudine) e IPTC (titolo, descrizione, parole chiave, autore, città, paese).

Formati Immagine Supportati
// MIME types immagine con estrazione EXIF completa
"image/jpeg", "image/png", "image/gif", "image/webp",
"image/tiff", "image/bmp", "image/heic", "image/heif", "image/avif"

// Esempio di metadati estratti da una foto JPEG
{
  "exifBase": {
    "marca": "Canon",
    "modello": "EOS R5",
    "software": "Adobe Lightroom 14.0",
    "orientamento": "Top, left side (Horizontal)"
  },
  "exifTecnico": {
    "dataScatto": "2026:01:15 14:30:22",
    "tempoEsposizione": "1/250 sec",
    "apertura": "f/2.8",
    "iso": "400",
    "lunghezzaFocale": "70 mm",
    "flash": "Flash did not fire"
  },
  "gps": {
    "latitudine": 41.1171,
    "longitudine": 16.8719
  }
}

Metadati per PDF e Documenti Office

Per i file PDF, il sistema estrae titolo, autore, soggetto, parole chiave, numero di pagine, versione PDF e informazioni sulla crittografia. Per i documenti Office (Word, Excel, PowerPoint, OpenDocument), vengono estratte anche statistiche come numero di parole, caratteri, paragrafi, slide e tempo totale di modifica.

Formati Supportati per l'Estrazione Metadati
// PDF
"application/pdf"

// Office - Word
"application/msword"
"application/vnd.openxmlformats-officedocument.wordprocessingml.document"

// Office - Excel
"application/vnd.ms-excel"
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"

// Office - PowerPoint
"application/vnd.ms-powerpoint"
"application/vnd.openxmlformats-officedocument.presentationml.presentation"

// OpenDocument
"application/vnd.oasis.opendocument.text"
"application/vnd.oasis.opendocument.spreadsheet"
"application/vnd.oasis.opendocument.presentation"

// Altri
"text/plain", "text/html", "text/csv",
"application/rtf", "application/zip"

I metadati vengono salvati nel campo JSON del documento e possono essere consultati tramite i metodi getMetadato(chiave), aggiungiMetadato(chiave, valore) e haMetadati() dell'Aggregate Root.

Integrità dei File con SHA-256

Ogni versione di un documento include un hash SHA-256 del contenuto, calcolato al momento del caricamento. Questo permette di verificare in qualsiasi momento che il file non sia stato alterato o corrotto durante lo storage.

Calcolo e Verifica Hash SHA-256
// Calcolo hash al caricamento
public String calcolaHash(InputStream contenuto) {
    MessageDigest digest = MessageDigest.getInstance("SHA-256");
    byte[] buffer = new byte[8192];
    int bytesRead;
    while ((bytesRead = contenuto.read(buffer)) != -1) {
        digest.update(buffer, 0, bytesRead);
    }
    byte[] hash = digest.digest();
    return bytesToHex(hash);  // 64 caratteri esadecimali
}

// Verifica integrita' su richiesta
public boolean verificaIntegrita(String percorsoRelativo, String hashAtteso) {
    if (hashAtteso == null) {
        return true;  // Se non c'e' hash, considera valido
    }
    try (InputStream is = leggi(percorsoRelativo).orElse(null)) {
        if (is == null) return false;
        String hashEffettivo = calcolaHash(is);
        return hashAtteso.equals(hashEffettivo);
    }
}

Il limite massimo per file è di 50 MB, validato sia nel factory method di DocumentoEvento che nel metodo aggiungiVersione(). File che superano questo limite vengono rifiutati con un'eccezione esplicita.

Collegamento Documento-Spesa

I documenti possono essere collegati a una spesa dell'evento, creando una relazione bidirezionale tra il sistema documentale e il sistema di gestione finanziaria. Questo è particolarmente utile per allegare ricevute, fatture e scontrini alle spese corrispondenti.

Collegamento e Scollegamento da Spesa
// Collega il documento a una spesa
public void collegaASpesa(Long spesaId) {
    this.spesaId = spesaId;
}

// Rimuove il collegamento con una spesa
public void scollegaDaSpesa() {
    this.spesaId = null;
}

// Verifica se il documento e' collegato a una spesa
public boolean isCollegatoASpesa() {
    return spesaId != null;
}

// Verifica se il documento e' una ricevuta
public boolean isRicevuta() {
    return tipoCartella == TipoCartella.RICEVUTE;
}

L'indice idx_documento_spesa sulla colonna spesa_id ottimizza le query che recuperano tutti i documenti allegati a una determinata spesa. Questo permette di visualizzare rapidamente le ricevute associate a ogni voce del budget dell'evento.

Storage Organizzato su Filesystem

L'implementazione dello storage segue il pattern Hexagonal Architecture: il domain layer definisce l'interfaccia DocumentoStorageService, mentre l'infrastructure layer fornisce l'implementazione FileSystemDocumentoStorage. I file sono organizzati in una struttura gerarchica chiara.

Struttura Directory dello Storage
{basePath}/
  └── {eventoId}/
      ├── documenti/
      │   └── {documentoId}/
      │       ├── v1.pdf
      │       ├── v2.pdf
      │       └── v3.docx
      ├── immagini/
      │   └── {documentoId}/
      │       ├── v1.jpg
      │       └── v2.png
      └── ricevute/
          └── {documentoId}/
              ├── v1.pdf
              └── v2.jpg

// Esempio percorso completo:
// /storage/42/documenti/156/v2.pdf
// /storage/42/immagini/201/v1.jpg
// /storage/42/ricevute/189/v3.pdf

Il naming delle versioni segue la convenzione v{numero}.{estensione}, dove l'estensione viene estratta dal nome file originale. Il servizio di storage gestisce anche la pulizia delle directory vuote dopo l'eliminazione di documenti, risalendo ricorsivamente l'albero fino alla directory base.

Operazioni dello Storage Service
public interface DocumentoStorageService {

    // Salvataggio con percorso auto-generato
    String salva(Long eventoId, TipoCartella tipoCartella,
                 Long documentoId, Integer versione,
                 String nomeFile, InputStream contenuto);

    // Lettura file
    Optional<InputStream> leggi(String percorsoRelativo);

    // Eliminazione singola versione o tutte le versioni
    boolean elimina(String percorsoRelativo);
    void eliminaTutteVersioni(Long eventoId, TipoCartella tipoCartella,
                              Long documentoId);

    // Integrita' e verifica
    String calcolaHash(InputStream contenuto);
    boolean verificaIntegrita(String percorsoRelativo, String hashAtteso);

    // Monitoraggio spazio
    long getSpazioUtilizzato(Long eventoId);
    long getDimensione(String percorsoRelativo);
    boolean esiste(String percorsoRelativo);
}

Il metodo getSpazioUtilizzato() percorre ricorsivamente la directory dell'evento e somma le dimensioni di tutti i file, permettendo di monitorare lo spazio disco occupato per ciascun evento e di implementare eventuali limiti di quota.

Riepilogo del Sistema Documentale

  • Aggregate Root: DocumentoEvento con validazione completa e factory method
  • 11 categorie predefinite + categorie personalizzate per evento con icona e colore
  • Auto-detection cartella: /ricevute, /immagini, /documenti in base a MIME type e categoria
  • Versioning immutabile: ogni modifica e ripristino creano una nuova versione
  • Link temporanei: condivisione con scadenza (max 30 giorni), limite download e revoca
  • Metadati automatici: EXIF/IPTC/XMP per immagini, proprietà PDF e Office
  • Integrità SHA-256: hashing del contenuto con verifica su richiesta
  • Limite 50 MB: validazione dimensione in creazione e aggiornamento
  • Collegamento spese: documenti allegabili alle voci di budget
  • Storage gerarchico: organizzazione per evento, tipo cartella e documento

Il codice sorgente del sistema documentale è disponibile su GitHub. Nel prossimo articolo esploreremo un altro aspetto dell'architettura di Play The Event, analizzando come il sistema gestisce le interazioni tra i diversi bounded context del dominio.

Domande frequenti

Come funziona la gestione delle versioni dei documenti nel Sistema Documentale di Play The Event?

Ogni documento mantiene una cronologia completa delle versioni, con l'aggiunta di una nuova versione che incrementa il contatore e crea un nuovo oggetto con tutti i metadati del file aggiornato.

Come vengono estratti automaticamente i metadati dei documenti?

Utilizzando Apache Tika e la libreria metadata-extractor, i metadati disponibili vengono estratti automaticamente quando un file viene caricato, e sono salvati come JSON nel campo del documento.

Come funziona il sistema di condivisione dei documenti tramite link temporanei?

I documenti possono essere condivisi esternamente tramite link temporanei che non richiedono autenticazione, generati dall'entity che gestisce la validazione e la revoca di questi link.

Come vengono gestiti i file nel Sistema Documentale di Play The Event?

I file sono organizzati in una struttura gerarchica chiara seguendo il pattern Hexagonal Architecture, con il naming delle versioni che segue la convenzione e il servizio di storage che gestisce anche la pulizia delle directory vuote dopo l'eliminazione di documenti.

Commenti

Caricamento commenti...

Accedi per commentare

Discuti questo articolo nella community

Unisciti al gruppo Telegram per discutere con altri sviluppatori, fare domande e condividere le tue esperienze.

Ti è piaciuto questo articolo?

Esplora altri contenuti sul blog o scopri i miei progetti