++ add reset program

This commit is contained in:
2026-07-29 14:26:36 +02:00
parent c6abef977d
commit 588910b893
14 changed files with 1524 additions and 183 deletions

View File

@@ -248,7 +248,7 @@ Tre ritocchi minori ma con qualche implicazione:
`permission:settings.manage` e il link in sidebar è sotto
`@can('settings.manage')`.
### Backup completo `.zip`: dati + file, e cambio password forzato al restore
### Backup completo `.zip`, ripristino, azzeramento
L'export XML da solo non ricostruisce un'installazione: `pdf_path`,
`thumbnail_path` e `logo_path` sono percorsi sul disco `public`, quindi un
@@ -263,11 +263,65 @@ Scelte non ovvie:
- **Rotte HTTP, non Livewire.** Lo `streamDownload` di Livewire passa il file
base64 dentro la risposta JSON: un archivio con i PDF dei territori non ci
sta. `backup/download` e `backup/restore` sono due rotte normali nel gruppo
sta. `backup/file/{op}` e `backup/restore` sono due rotte normali nel gruppo
`settings.manage`. Per lo stesso motivo `ZipArchive::addFile()` (che legge
dal disco in `close()`) e gli stream in estrazione: nessun PDF passa intero
per la memoria del processo.
- **Tutto in coda, con avanzamento reale.** Backup, ripristino e azzeramento
sono job (`Jobs/CreateBackupArchive`, `RestoreBackupArchive`,
`ResetApplication`) che scrivono su `Services/BackupOperationState`, e
`Livewire/Settings/BackupManager` lo interroga con `wire:poll`. In coda
perché un'installazione con qualche centinaio di PDF supera i 300 secondi di
`fastcgi_read_timeout` e il browser vedrebbe cadere la connessione a metà
compressione, senza modo di distinguerlo da un errore.
La barra conta unità di lavoro completate, non tempo. Per la compressione
questo costa qualcosa: `ZipArchive` non scrive niente finché non si chiama
`close()`, quindi contare le `addFile()` porterebbe al 100% in un istante per
poi restare fermo per tutta la scrittura. `BackupArchive::create()` chiude e
riapre l'archivio a lotti (`FLUSH_MASSIMI = 20`): si paga una ricopiatura
delle entry già presenti — byte a byte, senza ricomprimerle — e in cambio
ogni avanzamento corrisponde a byte davvero finiti sul disco. Il lotto è
`totale/20`, così la ricopiatura resta limitata a ~10 volte la dimensione
dell'archivio quanti che siano i file. Estrazione e riscrittura dei file sono
cicli nostri, lì l'avanzamento è per-file senza artifici.
- **L'azzeramento ha due tempi, separati dal download.** Il pulsante nella zona
pericolosa (`avviaAzzeramento`) crea *solo* il backup e si ferma in
`attesa-download`; la cancellazione parte da un secondo clic esplicito
(`confermaAzzeramento`), che rifiuta se lo stato non registra un
`servito_at`. Il primo tentativo faceva partire il reset dalla coda dello
`streamDownload`, "dopo l'ultimo byte" — **non funziona**: con
`fastcgi_buffering on` (il default, e `docker/nginx/default.conf` non lo
disattiva) nginx accetta l'intera risposta dal processo PHP, che quindi
prosegue anche se il browser ha chiuso la connessione al primo byte. Un
download annullato azzerava lo stesso. Il server può verificare di aver
*spedito* l'archivio, non che sia arrivato: quella verifica è dell'operatore,
e per questo la conferma è umana.
- **Nell'azzeramento i file si cancellano prima del database.** Svuotare il
disco è l'unico passo che può fallire per cause esterne (permessi sul
volume), e fallire lì non ha ancora toccato niente. Nell'ordine inverso un
errore sui file lascerebbe database azzerato e disco pieno di PDF orfani.
Collegato: `svuotaDiscoPubblico()` ricontrolla il disco dopo la cancellazione
e solleva se qualcosa sopravvive, invece di restituire il conteggio iniziale.
Flysystem non solleva se una `unlink` fallisce, e basta un file scritto da un
processo con un altro utente (l'entrypoint lavora da root, php-fpm e il
worker da www-data) perché l'operazione si dichiari riuscita lasciando il
disco popolato.
- **L'azzeramento tiene in vita l'amministratore che l'ha chiesto.** Un'installazione
appena creata ha un amministratore; cancellarli tutti lascerebbe
l'applicazione senza nessuno che possa accedere fino al riavvio del container
(l'admin iniziale lo ricrea `entrypoint.sh` da `INITIAL_ADMIN_*`, che in
esecuzione possono non esserci). Gli altri utenti si eliminano uno alla volta
via Eloquent, non con un delete di massa: è il trait `HasRoles` di Spatie a
staccare ruoli e permessi nell'evento `deleting`, che un delete di massa non
fa scattare. Il registro attività si svuota **per ultimo** — quelle
cancellazioni scrivono righe nuove — e subito dopo si scrive l'unica riga che
sopravvive, quella dell'azzeramento stesso.
- **Ordine del restore: staging → database → file.** I file si estraggono
prima in `storage/app/backup-restore/<uuid>`, così un archivio corrotto o
fuori limite fallisce mentre i dati correnti sono ancora intatti; poi