++ consolidamento funzionalità Backup
This commit is contained in:
91
HANDOFF.md
91
HANDOFF.md
@@ -324,6 +324,35 @@ Scelte non ovvie:
|
||||
sarebbe solo un rallentamento. Il blocco è sull'operazione, non sull'account:
|
||||
nessuno resta chiuso fuori dall'applicazione.
|
||||
|
||||
- **Una sola operazione alla volta, con blocco che si autoripara.** Backup,
|
||||
ripristino e azzeramento lavorano tutti sullo stesso database e sullo stesso
|
||||
disco: due in parallelo — due schede, due amministratori — non si rallentano
|
||||
soltanto, si sovrascrivono. `BackupOperationState::corrente()` fa da posto
|
||||
unico; lo controllano sia il componente Livewire sia
|
||||
`BackupController::upload()`, che sono due porte d'ingresso separate (la
|
||||
seconda risponde 409). Il blocco non è eterno: ha un TTL di un'ora, e
|
||||
`corrente()` lo considera libero se l'operazione puntata non esiste più o è
|
||||
terminata. In più i tre job hanno un `failed()` — Laravel lo chiama quando il
|
||||
worker uccide il job per timeout o errore fatale, cioè proprio i casi che
|
||||
`handle()` non può intercettare — che chiude l'operazione e libera il posto
|
||||
invece di lasciare una barra ferma per un'ora.
|
||||
|
||||
- **La pagina si riaggancia all'operazione in corso.** `BackupManager::mount()`
|
||||
legge `corrente()`. Senza, ricaricare la pagina durante un backup lungo
|
||||
faceva sparire la barra, e — più grave — un azzeramento fermo in attesa di
|
||||
conferma diventava irraggiungibile: nessuno poteva più né confermarlo né
|
||||
annullarlo, e il posto restava occupato fino alla scadenza.
|
||||
|
||||
- **L'esito del ripristino si legge senza consumarlo.** Il job non ha una
|
||||
sessione in cui lasciare un flash, quindi deposita il messaggio in cache a
|
||||
nome di chi ha avviato il ripristino e la schermata di cambio password lo
|
||||
mostra. Deve essere un `Cache::get` + `forget` a password cambiata, **non** un
|
||||
`pull`: quando è il middleware a dirottare una richiesta di Livewire, il
|
||||
client segue il 302 via `fetch` — ed è quella `fetch` a bruciare il messaggio
|
||||
— per poi rifare la stessa GET con `window.location`, che troverebbe la cache
|
||||
già vuota. Con il `pull` l'amministratore non vedeva **mai** il riepilogo del
|
||||
proprio ripristino.
|
||||
|
||||
- **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
|
||||
@@ -335,6 +364,32 @@ Scelte non ovvie:
|
||||
worker da www-data) perché l'operazione si dichiari riuscita lasciando il
|
||||
disco popolato.
|
||||
|
||||
- **L'archivio si controlla prima di consegnarlo.** Un backup è l'unico file di
|
||||
cui non ci si accorge che era rotto finché non serve, quindi
|
||||
`BackupArchive::create()` non si fida di aver scritto bene: controlla il
|
||||
valore di ritorno di ogni `addFile()` (che torna `false`, senza sollevare, se
|
||||
il file è sparito o è illeggibile — l'archivio finirebbe con un buco
|
||||
silenzioso), e alla fine riapre l'archivio e verifica che contenga
|
||||
`data.xml`, `manifest.json` e *esattamente* i file attesi. Se qualcosa non
|
||||
torna, l'archivio viene cancellato e l'operazione fallisce: meglio nessun
|
||||
backup che un backup che sembra buono.
|
||||
|
||||
Attenzione a una trappola di `ZipArchive`: dopo un `close()` fallito
|
||||
l'oggetto resta inutilizzabile e ogni metodo successivo solleva `Invalid or
|
||||
uninitialized Zip object`. Una chiusura di cortesia dentro un `catch`
|
||||
sostituiva il messaggio vero con quello, e l'amministratore leggeva una
|
||||
stringa incomprensibile al posto di «permesso negato su questo file». Da qui
|
||||
`chiudi()`, che cattura `getStatusString()` prima che vada perso, e i
|
||||
`close()` di pulizia avvolti in un try/catch che non può mascherare l'errore
|
||||
originale.
|
||||
|
||||
- **Eliminare definitivamente un territorio porta via anche i suoi file.**
|
||||
`TerritorioCestino::forceDelete()` cancellava la riga e lasciava mappa e
|
||||
miniatura sul volume: invisibili in ogni schermata, non più raggiungibili da
|
||||
nessuna cancellazione, e dentro ogni backup successivo. I file si rimuovono
|
||||
*prima* della riga, così se la cancellazione fallisce il territorio resta nel
|
||||
cestino e l'operazione si può ritentare.
|
||||
|
||||
- **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
|
||||
@@ -459,13 +514,37 @@ In ordine di priorità.
|
||||
|
||||
### 1. Nessun test automatico
|
||||
|
||||
`composer.json` richiede PHPUnit ma **la cartella `tests/` non esiste**. Ogni
|
||||
verifica è manuale. È il debito più rilevante: le regole di business (anno
|
||||
teocratico, stato territorio, priorità automatica, percentuale campagne) sono
|
||||
esattamente il tipo di logica che va protetta da test.
|
||||
`composer.json` richiede PHPUnit ma **la cartella `tests/` non esiste** e non
|
||||
c'è `phpunit.xml`. Ogni verifica è manuale. È il debito più rilevante: le regole
|
||||
di business (anno teocratico, stato territorio, priorità automatica, percentuale
|
||||
campagne) sono esattamente il tipo di logica che va protetta da test — e a
|
||||
maggior ragione backup, ripristino e azzeramento, dove un errore non si vede
|
||||
finché non è troppo tardi.
|
||||
|
||||
Nota: `config/database.php` ridefinisce `connections` e lascia solo `mysql`.
|
||||
Per usare SQLite in memoria nei test va riaggiunta la connessione `sqlite`.
|
||||
Tre blocchi concreti da rimuovere prima di poter scrivere il primo test:
|
||||
|
||||
1. **PHPUnit non è nell'immagine.** `docker/php/Dockerfile` installa con
|
||||
`--no-dev` (giusto, per la produzione), quindi `vendor/phpunit` nel container
|
||||
non c'è e `docker compose exec app ./vendor/bin/phpunit` non funziona.
|
||||
Servono o un `composer install` locale sull'host, o uno stage `dev` nel
|
||||
Dockerfile che tenga le dipendenze di sviluppo.
|
||||
2. **Manca la connessione SQLite.** `config/database.php` ridefinisce
|
||||
`connections` e lascia solo `mysql`. Per usare SQLite in memoria va
|
||||
riaggiunta la connessione `sqlite`.
|
||||
3. **Manca `phpunit.xml`.** L'autoload `Tests\` in `composer.json` c'è già, il
|
||||
resto no.
|
||||
|
||||
Dove conviene cominciare, in ordine di rapporto valore/attrito — i primi due
|
||||
non hanno bisogno del database e quindi si sbloccano risolvendo solo il punto 1:
|
||||
|
||||
- `Services/BackupArchive` con `Storage::fake('public')`: round-trip
|
||||
crea → verifica → estrai, rifiuto dello zip slip, rifiuto del formato futuro,
|
||||
fallimento su archivio incompleto.
|
||||
- `Services/BackupOperationState` con il driver di cache `array`: il posto
|
||||
unico, il rilascio alla chiusura, il puntatore orfano, il non-rilascio da
|
||||
parte di un'operazione vecchia.
|
||||
- `Livewire/Registro::righeS13()`: il ciclo da quattro e la colonna del
|
||||
riporto, con i casi 2/3/4/5/8/9 assegnazioni (questi sì, con database).
|
||||
|
||||
### 2. Race su doppia assegnazione
|
||||
|
||||
|
||||
Reference in New Issue
Block a user