Revisione blocchi 2 e 3: file protetti, ripristino atomico, import sicuri
Blocco 2 - Esiti delle azioni Livewire come avvisi (trait Avvisa) invece di flash che comparivano alla pagina dopo. - PDF, miniature e versioni dei territori non più serviti da nginx su /storage: TerritorioFileController con login, permessi e cache privata. - Ripristino tutto o niente: verifica dell'archivio, una transazione con import, piantine, blocco account e scambio dei file annullabile. - Backup formato 2 con piantine, storico PDF e area KML. - Posto unico preso in un passo (Cache::lock) e rispettato anche da import XML e PDF. - Import PDF da cartella: abbinamento corretto (1-A, 12 B, ambigui), caricamento a lotti, file non PDF ignorati, PDF sostituito nello storico. - L'import XML elimina i file dei territori che non usa più; comando territori:file-orfani per quelli accumulati prima. Blocco 3 - Password attuale richiesta al cambio volontario; AuthenticateSession chiude le sessioni aperte altrove. - Niente nomi dei proclamatori in chiaro nel registro attività. - Export XML senza remember_token; chi riceve l'hash dal file deve cambiare password. - Campagne: ultimo giorno incluso, percentuale sui territori distinti. - Coda: retry_after 960 s, stop_grace_period 16m, Redis volatile-lru, impostazioni rilette a ogni job. - Azzeramento che cancella anche piantine, area KML e anteprime. - Pulsanti profilo e menu che non si nascondevano; icona del sito. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -20,8 +20,15 @@ use ZipArchive;
|
||||
*
|
||||
* manifest.json formato, data, conteggi (letto per validare il file)
|
||||
* data.xml lo stesso XML dell'export "Esporta XML"
|
||||
* piantine.json disegni e storico dei PDF (Piantine\PiantineBackup)
|
||||
* storage/<percorso> ogni file del disco `public`, con lo stesso percorso
|
||||
* relativo che sta in pdf_path/thumbnail_path/logo_path
|
||||
* piantine/<percorso> l'area KML delle piantine (storage/app/piantine),
|
||||
* che sta accanto al disco `public`, non dentro
|
||||
*
|
||||
* piantine.json e piantine/ esistono dal formato 2. Un archivio di formato 1
|
||||
* si ripristina lo stesso: le tabelle delle piantine restano vuote e l'area
|
||||
* KML in uso non si tocca (vedi scambiaFile()).
|
||||
*/
|
||||
class BackupArchive
|
||||
{
|
||||
@@ -29,11 +36,23 @@ class BackupArchive
|
||||
* Versione del formato. L'import rifiuta i pacchetti piu' recenti di
|
||||
* quello che sa leggere invece di ripristinarli a meta'.
|
||||
*/
|
||||
public const FORMAT_VERSION = 1;
|
||||
public const FORMAT_VERSION = 2;
|
||||
|
||||
private const ENTRY_MANIFEST = 'manifest.json';
|
||||
private const ENTRY_DATA = 'data.xml';
|
||||
private const ENTRY_PIANTINE = 'piantine.json';
|
||||
private const PREFIX_STORAGE = 'storage/';
|
||||
private const PREFIX_PIANTINE = 'piantine/';
|
||||
|
||||
/**
|
||||
* Sottocartelle dello staging, una per ogni cartella che il ripristino
|
||||
* sostituisce (vedi radiciDaSostituire()).
|
||||
*/
|
||||
private const STAGING_PUBBLICO = 'pubblico';
|
||||
private const STAGING_PIANTINE = 'piantine';
|
||||
|
||||
/** Cartella dell'area KML sul disco `local` (vedi PiantineArea). */
|
||||
private const CARTELLA_PIANTINE = 'piantine';
|
||||
|
||||
/**
|
||||
* Limiti sull'archivio in ingresso. Un .zip e' un formato compresso: senza
|
||||
@@ -65,12 +84,32 @@ class BackupArchive
|
||||
* Costruisce il pacchetto in $destinazione e ne restituisce il percorso.
|
||||
*
|
||||
* @param string $xml Contenuto di data.xml (l'export XML gia' pronto).
|
||||
* @param array $piantine Contenuto di piantine.json (PiantineBackup::esporta()).
|
||||
* Obbligatorio e non facoltativo di proposito: un
|
||||
* chiamante che se ne dimentica produrrebbe archivi
|
||||
* che al ripristino svuotano le piantine.
|
||||
* @param callable|null $onProgress fn(int $fatti, int $totale): void
|
||||
*/
|
||||
public function create(string $xml, string $destinazione, ?callable $onProgress = null): string
|
||||
public function create(string $xml, array $piantine, string $destinazione, ?callable $onProgress = null): string
|
||||
{
|
||||
$disk = Storage::disk('public');
|
||||
$files = $disk->allFiles();
|
||||
$local = Storage::disk('local');
|
||||
|
||||
// Due cartelle sorgente, un solo elenco di terne: percorso relativo
|
||||
// (per i messaggi), percorso sul disco, nome dell'entry.
|
||||
$files = [];
|
||||
foreach ($disk->allFiles() as $relativePath) {
|
||||
$files[] = [$relativePath, $disk->path($relativePath), self::PREFIX_STORAGE . $relativePath];
|
||||
}
|
||||
$filePubblici = count($files);
|
||||
|
||||
// allFiles('piantine') restituisce gia' "piantine/area.kml": il
|
||||
// percorso relativo al disco coincide con il nome dell'entry.
|
||||
foreach ($local->allFiles(self::CARTELLA_PIANTINE) as $relativePath) {
|
||||
$files[] = [$relativePath, $local->path($relativePath), $relativePath];
|
||||
}
|
||||
$filePiantine = count($files) - $filePubblici;
|
||||
|
||||
$totale = count($files);
|
||||
|
||||
$cartella = dirname($destinazione);
|
||||
@@ -84,12 +123,14 @@ class BackupArchive
|
||||
}
|
||||
|
||||
$zip->addFromString(self::ENTRY_DATA, $xml);
|
||||
$zip->addFromString(self::ENTRY_PIANTINE, json_encode($piantine, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_THROW_ON_ERROR));
|
||||
$zip->addFromString(self::ENTRY_MANIFEST, json_encode([
|
||||
'format_version' => self::FORMAT_VERSION,
|
||||
'application' => 'TerManager2',
|
||||
'generated_at' => now()->toIso8601String(),
|
||||
'app_url' => (string) config('app.url'),
|
||||
'files_count' => $totale,
|
||||
'files_count' => $filePubblici,
|
||||
'piantine_files_count' => $filePiantine,
|
||||
], JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES));
|
||||
|
||||
try {
|
||||
@@ -110,12 +151,12 @@ class BackupArchive
|
||||
|
||||
try {
|
||||
$this->conArchivioAperto($destinazione, function (ZipArchive $zip) use (
|
||||
$disk, $files, $totale, $perLotto, $onProgress, $destinazione
|
||||
$files, $totale, $perLotto, $onProgress, $destinazione
|
||||
) {
|
||||
$nelLotto = 0;
|
||||
$fatti = 0;
|
||||
|
||||
foreach ($files as $relativePath) {
|
||||
foreach ($files as [$relativePath, $sorgente, $entry]) {
|
||||
// addFile() legge dal disco durante close(): i PDF non
|
||||
// passano mai interi per la memoria del processo.
|
||||
//
|
||||
@@ -133,7 +174,7 @@ class BackupArchive
|
||||
// raggiunto mai, e l'amministratore leggerebbe la stringa
|
||||
// grezza di PHP. Stesso problema, e stessa scelta, del
|
||||
// close() avvolto in chiudi().
|
||||
if (! @$zip->addFile($disk->path($relativePath), self::PREFIX_STORAGE . $relativePath)) {
|
||||
if (! @$zip->addFile($sorgente, $entry)) {
|
||||
throw new RuntimeException(
|
||||
'Impossibile aggiungere "' . $relativePath . '" al backup: '
|
||||
. 'il file non e\' leggibile o e\' stato rimosso durante la creazione.'
|
||||
@@ -157,7 +198,7 @@ class BackupArchive
|
||||
}
|
||||
|
||||
try {
|
||||
$this->verificaArchivio($destinazione, $totale);
|
||||
$this->verificaArchivio($destinazione, $filePubblici, $filePiantine);
|
||||
} catch (\Throwable $e) {
|
||||
@unlink($destinazione);
|
||||
|
||||
@@ -178,7 +219,7 @@ class BackupArchive
|
||||
* non dei contenuti — quindi e' un controllo a buon mercato che copre
|
||||
* troncamenti, chiusure a meta' e lotti persi.
|
||||
*/
|
||||
private function verificaArchivio(string $path, int $fileAttesi): void
|
||||
private function verificaArchivio(string $path, int $fileAttesi, int $piantineAttesi): void
|
||||
{
|
||||
$zip = new ZipArchive();
|
||||
|
||||
@@ -187,27 +228,31 @@ class BackupArchive
|
||||
}
|
||||
|
||||
$trovati = 0;
|
||||
$piantineTrovati = 0;
|
||||
for ($i = 0; $i < $zip->numFiles; $i++) {
|
||||
$nome = $zip->getNameIndex($i);
|
||||
if ($nome !== false && str_starts_with($nome, self::PREFIX_STORAGE)) {
|
||||
$trovati++;
|
||||
} elseif ($nome !== false && str_starts_with($nome, self::PREFIX_PIANTINE)) {
|
||||
$piantineTrovati++;
|
||||
}
|
||||
}
|
||||
|
||||
$haDati = $zip->locateName(self::ENTRY_DATA) !== false;
|
||||
$haManifest = $zip->locateName(self::ENTRY_MANIFEST) !== false;
|
||||
$haPiantine = $zip->locateName(self::ENTRY_PIANTINE) !== false;
|
||||
|
||||
$zip->close();
|
||||
|
||||
if (! $haDati || ! $haManifest) {
|
||||
throw new RuntimeException('Il backup appena creato e\' incompleto: manca data.xml o manifest.json.');
|
||||
if (! $haDati || ! $haManifest || ! $haPiantine) {
|
||||
throw new RuntimeException('Il backup appena creato e\' incompleto: manca data.xml, piantine.json o manifest.json.');
|
||||
}
|
||||
|
||||
if ($trovati !== $fileAttesi) {
|
||||
if ($trovati !== $fileAttesi || $piantineTrovati !== $piantineAttesi) {
|
||||
throw new RuntimeException(sprintf(
|
||||
'Il backup appena creato contiene %d file invece di %d: archivio scartato.',
|
||||
$trovati,
|
||||
$fileAttesi
|
||||
$trovati + $piantineTrovati,
|
||||
$fileAttesi + $piantineAttesi
|
||||
));
|
||||
}
|
||||
}
|
||||
@@ -294,8 +339,15 @@ class BackupArchive
|
||||
* l'archivio e' corrotto o supera i limiti, il fallimento avviene mentre i
|
||||
* dati correnti sono ancora intatti.
|
||||
*
|
||||
* E' anche la verifica completa dell'archivio: ogni entry si estrae per
|
||||
* intero e se ne confrontano dimensione e CRC con quelli dichiarati. Un
|
||||
* file troncato, un'entry corrotta o il disco che si riempie durante
|
||||
* l'estrazione fermano tutto qui — mentre lo staging sta sullo stesso
|
||||
* volume dei file veri, quindi se c'e' posto per estrarre c'e' posto
|
||||
* anche per lo scambio, che e' solo una serie di rename().
|
||||
*
|
||||
* @param callable|null $onProgress fn(int $fatti, int $totale): void
|
||||
* @return array{xml: string, staging: string, files: int}
|
||||
* @return array{xml: string, piantine: array|null, formato: int, staging: string, pubblico: string, files: int, file_piantine: int}
|
||||
*/
|
||||
public function extract(string $zipPath, ?callable $onProgress = null): array
|
||||
{
|
||||
@@ -334,12 +386,35 @@ class BackupArchive
|
||||
throw new RuntimeException('Archivio incompleto: manca ' . self::ENTRY_DATA . '.');
|
||||
}
|
||||
|
||||
// Dal formato 2 piantine.json e' obbligatorio: la sua assenza non
|
||||
// e' "nessuna piantina" ma un archivio monco, e ripristinarlo
|
||||
// svuoterebbe disegni e storico senza che nessuno l'abbia chiesto.
|
||||
// Null resta riservato al formato 1, che le piantine non le aveva.
|
||||
$piantine = null;
|
||||
if ($formatVersion >= 2) {
|
||||
$piantineRaw = $zip->getFromName(self::ENTRY_PIANTINE);
|
||||
$piantine = $piantineRaw !== false ? json_decode($piantineRaw, true) : null;
|
||||
|
||||
if (! is_array($piantine)) {
|
||||
throw new RuntimeException('Archivio incompleto: manca ' . self::ENTRY_PIANTINE . ' o non e\' leggibile.');
|
||||
}
|
||||
}
|
||||
|
||||
// Le entry che si estraggono, prefisso => sottocartella dello
|
||||
// staging. L'area KML solo dal formato 2: un archivio vecchio non
|
||||
// la porta, e non deve poterla portare per caso.
|
||||
$prefissi = [self::PREFIX_STORAGE => self::STAGING_PUBBLICO];
|
||||
if ($formatVersion >= 2) {
|
||||
$prefissi[self::PREFIX_PIANTINE] = self::STAGING_PIANTINE;
|
||||
}
|
||||
|
||||
if ($zip->numFiles > self::MAX_ENTRIES) {
|
||||
throw new RuntimeException('L\'archivio contiene piu\' di ' . self::MAX_ENTRIES . ' file.');
|
||||
}
|
||||
|
||||
$totalBytes = 0;
|
||||
$extracted = 0;
|
||||
$estrattiPiantine = 0;
|
||||
|
||||
// Il totale si conta scorrendo i nomi, non come "numFiles meno le
|
||||
// due entry di servizio": un archivio con una entry in piu' (una
|
||||
@@ -348,7 +423,7 @@ class BackupArchive
|
||||
$daEstrarre = 0;
|
||||
for ($i = 0; $i < $zip->numFiles; $i++) {
|
||||
$nome = $zip->getNameIndex($i);
|
||||
if ($nome !== false && str_starts_with($nome, self::PREFIX_STORAGE) && ! str_ends_with($nome, '/')) {
|
||||
if ($nome !== false && ! str_ends_with($nome, '/') && $this->prefissoDi($nome, $prefissi) !== null) {
|
||||
$daEstrarre++;
|
||||
}
|
||||
}
|
||||
@@ -361,11 +436,12 @@ class BackupArchive
|
||||
continue;
|
||||
}
|
||||
|
||||
if (! str_starts_with($entryName, self::PREFIX_STORAGE)) {
|
||||
$prefisso = $this->prefissoDi($entryName, $prefissi);
|
||||
if ($prefisso === null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$relativePath = substr($entryName, strlen(self::PREFIX_STORAGE));
|
||||
$relativePath = substr($entryName, strlen($prefisso));
|
||||
$safePath = $this->safeRelativePath($relativePath);
|
||||
|
||||
if ($safePath === null) {
|
||||
@@ -375,7 +451,8 @@ class BackupArchive
|
||||
// La dimensione dichiarata si legge *prima* di estrarre: e'
|
||||
// l'unico momento in cui si puo' rifiutare un'entry che, una
|
||||
// volta decompressa, riempirebbe il disco.
|
||||
$size = (int) ($zip->statIndex($index)['size'] ?? 0);
|
||||
$stat = $zip->statIndex($index);
|
||||
$size = (int) ($stat['size'] ?? 0);
|
||||
|
||||
if ($size > self::MAX_ENTRY_BYTES) {
|
||||
throw new RuntimeException(sprintf(
|
||||
@@ -390,7 +467,7 @@ class BackupArchive
|
||||
throw new RuntimeException('L\'archivio decompresso supera il limite consentito.');
|
||||
}
|
||||
|
||||
$destination = $staging . '/' . $safePath;
|
||||
$destination = $staging . '/' . $prefissi[$prefisso] . '/' . $safePath;
|
||||
$directory = dirname($destination);
|
||||
|
||||
if (! is_dir($directory) && ! mkdir($directory, 0775, true) && ! is_dir($directory)) {
|
||||
@@ -409,12 +486,40 @@ class BackupArchive
|
||||
throw new RuntimeException('Impossibile scrivere "' . $safePath . '" durante il ripristino.');
|
||||
}
|
||||
|
||||
stream_copy_to_stream($stream, $target);
|
||||
// Il risultato della copia si controlla, ed e' qui che il
|
||||
// ripristino diventa sicuro: prima un disco pieno o un'entry
|
||||
// corrotta lasciavano nello staging un file troncato senza
|
||||
// nessun errore, e quel file finiva al posto del PDF buono.
|
||||
// La @ per lo stesso motivo di addFile(): il warning di PHP
|
||||
// ("CRC error") arriverebbe all'amministratore al posto del
|
||||
// messaggio che dice quale file.
|
||||
$copiati = @stream_copy_to_stream($stream, $target);
|
||||
fclose($stream);
|
||||
fclose($target);
|
||||
$chiuso = fclose($target);
|
||||
|
||||
$extracted++;
|
||||
$onProgress && $onProgress($extracted, $daEstrarre);
|
||||
if ($copiati === false || $copiati !== $size || ! $chiuso) {
|
||||
throw new RuntimeException(sprintf(
|
||||
'Impossibile estrarre "%s" per intero (%s byte su %d): l\'archivio e\' danneggiato o lo spazio su disco e\' esaurito. Nessun dato e\' stato modificato.',
|
||||
$safePath,
|
||||
$copiati === false ? '0' : (string) $copiati,
|
||||
$size
|
||||
));
|
||||
}
|
||||
|
||||
// Il CRC lo controlla gia' libzip a fine lettura, ma solo se la
|
||||
// lettura arriva in fondo e come warning: ricalcolarlo sul file
|
||||
// scritto costa una rilettura e non dipende da come libzip
|
||||
// segnala l'errore.
|
||||
if (isset($stat['crc']) && hash_file('crc32b', $destination) !== sprintf('%08x', $stat['crc'] & 0xFFFFFFFF)) {
|
||||
throw new RuntimeException('Il file "' . $safePath . '" nell\'archivio e\' danneggiato (CRC non corrispondente). Nessun dato e\' stato modificato.');
|
||||
}
|
||||
|
||||
if ($prefissi[$prefisso] === self::STAGING_PIANTINE) {
|
||||
$estrattiPiantine++;
|
||||
} else {
|
||||
$extracted++;
|
||||
}
|
||||
$onProgress && $onProgress($extracted + $estrattiPiantine, $daEstrarre);
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
$zip->close();
|
||||
@@ -425,63 +530,180 @@ class BackupArchive
|
||||
|
||||
$zip->close();
|
||||
|
||||
return ['xml' => $xml, 'staging' => $staging, 'files' => $extracted];
|
||||
return [
|
||||
'xml' => $xml,
|
||||
'piantine' => $piantine,
|
||||
'formato' => $formatVersion,
|
||||
'staging' => $staging,
|
||||
'pubblico' => $staging . '/' . self::STAGING_PUBBLICO,
|
||||
'files' => $extracted,
|
||||
'file_piantine' => $estrattiPiantine,
|
||||
];
|
||||
}
|
||||
|
||||
/** Il prefisso fra quelli ammessi con cui comincia l'entry, o null. */
|
||||
private function prefissoDi(string $entryName, array $prefissi): ?string
|
||||
{
|
||||
foreach (array_keys($prefissi) as $prefisso) {
|
||||
if (str_starts_with($entryName, $prefisso)) {
|
||||
return $prefisso;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sostituisce il contenuto del disco `public` con quello estratto.
|
||||
* Mette i file estratti al posto di quelli in uso, tenendo da parte i
|
||||
* vecchi finche' il ripristino non e' confermato.
|
||||
*
|
||||
* La sostituzione e' totale, non un merge: i percorsi dei file vivono nelle
|
||||
* righe del database, che il ripristino ha appena riscritto per intero.
|
||||
* Tenere i file precedenti lascerebbe sul volume PDF non piu' referenziati
|
||||
* da nulla, che nessuna schermata mostra e nessuna cancellazione raggiunge.
|
||||
*
|
||||
* @param callable|null $onProgress fn(int $fatti, int $totale): void
|
||||
* Prima si svuotava il disco e poi si ricopiava dallo staging file per
|
||||
* file: un errore a meta' (disco pieno, permessi) lasciava il database
|
||||
* nuovo, una parte dei file nuovi e nessuno dei vecchi — irrecuperabile.
|
||||
* Adesso sono solo rename() sullo stesso volume, per voce di primo livello
|
||||
* (territori-pdf/, territori-mappe/, logo...): istantanei, senza bisogno
|
||||
* di spazio, e ognuno annotato in un registro che annullaScambio()
|
||||
* ripercorre a ritroso. Se uno fallisce, quelli gia' fatti si disfano qui
|
||||
* e si solleva: il chiamante annulla la transazione e l'installazione
|
||||
* resta com'era.
|
||||
*
|
||||
* Si spostano le voci e non la cartella radice: a quella del disco
|
||||
* `public` punta il symlink public/storage.
|
||||
*
|
||||
* @param array $estratto Il risultato di extract().
|
||||
* @return array{file: int, accantonati: string, spostamenti: list<array{0: string, 1: string}>}
|
||||
* Da passare a confermaScambio() a database confermato, o ad
|
||||
* annullaScambio() se qualcosa fallisce dopo.
|
||||
*/
|
||||
public function restoreFiles(string $staging, ?callable $onProgress = null): int
|
||||
public function scambiaFile(array $estratto): array
|
||||
{
|
||||
$disk = Storage::disk('public');
|
||||
// Fuori da backup-restore/ di proposito: quella cartella la ripulisce
|
||||
// backup:cleanup dopo un giorno, e qui dentro, se l'annullamento
|
||||
// stesso fallisse, resterebbero gli unici file buoni.
|
||||
$scambio = [
|
||||
'file' => $estratto['files'] + $estratto['file_piantine'],
|
||||
'accantonati' => storage_path('app/backup-precedenti/' . Str::uuid()),
|
||||
'spostamenti' => [],
|
||||
];
|
||||
|
||||
$this->svuotaDiscoPubblico();
|
||||
try {
|
||||
foreach ($this->radiciDaSostituire($estratto['formato']) as $nome => $radice) {
|
||||
$this->preparaCartella($radice);
|
||||
$this->preparaCartella($scambio['accantonati'] . '/' . $nome);
|
||||
|
||||
if (! is_dir($staging)) {
|
||||
return 0;
|
||||
foreach ($this->vociDi($radice) as $voce) {
|
||||
$this->sposta($radice . '/' . $voce, $scambio['accantonati'] . '/' . $nome . '/' . $voce, $scambio);
|
||||
}
|
||||
|
||||
// Una sottocartella assente nello staging vuol dire "nessun
|
||||
// file": la radice resta vuota, come nell'installazione
|
||||
// salvata.
|
||||
$nuovi = $estratto['staging'] . '/' . $nome;
|
||||
foreach ($this->vociDi($nuovi) as $voce) {
|
||||
$this->sposta($nuovi . '/' . $voce, $radice . '/' . $voce, $scambio);
|
||||
}
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
$this->annullaScambio($scambio, $e);
|
||||
|
||||
throw $e;
|
||||
}
|
||||
|
||||
$restored = 0;
|
||||
$totale = iterator_count(new \RecursiveIteratorIterator(
|
||||
new \RecursiveDirectoryIterator($staging, \FilesystemIterator::SKIP_DOTS)
|
||||
));
|
||||
$onProgress && $onProgress(0, $totale);
|
||||
return $scambio;
|
||||
}
|
||||
|
||||
$iterator = new \RecursiveIteratorIterator(
|
||||
new \RecursiveDirectoryIterator($staging, \FilesystemIterator::SKIP_DOTS)
|
||||
);
|
||||
/** Il ripristino e' confermato: i file precedenti non servono piu'. */
|
||||
public function confermaScambio(array $scambio): void
|
||||
{
|
||||
$this->deleteDirectory($scambio['accantonati']);
|
||||
}
|
||||
|
||||
foreach ($iterator as $file) {
|
||||
/** @var \SplFileInfo $file */
|
||||
if (! $file->isFile()) {
|
||||
continue;
|
||||
/**
|
||||
* Rimette ogni file dov'era prima di scambiaFile(), ripercorrendo gli
|
||||
* spostamenti a ritroso: i nuovi tornano nello staging, i vecchi al loro
|
||||
* posto.
|
||||
*
|
||||
* Se anche questo fallisce non si cancella niente: i file precedenti
|
||||
* restano nella cartella degli accantonati, e il messaggio dice dove, cosi'
|
||||
* si possono rimettere a mano invece di darli per persi.
|
||||
*/
|
||||
public function annullaScambio(array $scambio, ?\Throwable $causa = null): void
|
||||
{
|
||||
try {
|
||||
foreach (array_reverse($scambio['spostamenti']) as [$da, $a]) {
|
||||
if (! @rename($a, $da)) {
|
||||
throw new RuntimeException('impossibile rimettere "' . $da . '"');
|
||||
}
|
||||
}
|
||||
|
||||
$relativePath = str_replace('\\', '/', substr($file->getPathname(), strlen($staging) + 1));
|
||||
|
||||
$handle = fopen($file->getPathname(), 'rb');
|
||||
if ($handle === false) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$disk->writeStream($relativePath, $handle);
|
||||
|
||||
if (is_resource($handle)) {
|
||||
fclose($handle);
|
||||
}
|
||||
|
||||
$restored++;
|
||||
$onProgress && $onProgress($restored, $totale);
|
||||
} catch (\Throwable $e) {
|
||||
throw new RuntimeException(
|
||||
($causa ? rtrim($causa->getMessage(), '. ') . '. ' : '')
|
||||
. 'Ripristino dei file precedenti non riuscito (' . $e->getMessage() . '): '
|
||||
. 'li trovi in ' . $scambio['accantonati'] . ' e vanno rimessi a mano.',
|
||||
0,
|
||||
$causa ?? $e
|
||||
);
|
||||
}
|
||||
|
||||
return $restored;
|
||||
$this->deleteDirectory($scambio['accantonati']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Le cartelle che il ripristino sostituisce: sottocartella dello staging
|
||||
* => cartella vera.
|
||||
*
|
||||
* L'area KML solo con un archivio di formato 2, che la porta sempre (anche
|
||||
* vuota, se l'installazione salvata non ne aveva). Un archivio di formato
|
||||
* 1 non sa niente dell'area, e svuotarla per questo vorrebbe dire
|
||||
* buttare un file che con i dati ripristinati non ha nessun legame: nessuna
|
||||
* riga lo cita, e' la mappa della citta'.
|
||||
*
|
||||
* @return array<string, string>
|
||||
*/
|
||||
private function radiciDaSostituire(int $formato): array
|
||||
{
|
||||
$radici = [self::STAGING_PUBBLICO => rtrim(Storage::disk('public')->path(''), '/\\')];
|
||||
|
||||
if ($formato >= 2) {
|
||||
$radici[self::STAGING_PIANTINE] = rtrim(Storage::disk('local')->path(self::CARTELLA_PIANTINE), '/\\');
|
||||
}
|
||||
|
||||
return $radici;
|
||||
}
|
||||
|
||||
/** @return list<string> Le voci di primo livello di una cartella (vuoto se non esiste). */
|
||||
private function vociDi(string $cartella): array
|
||||
{
|
||||
if (! is_dir($cartella)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return array_values(array_diff(scandir($cartella) ?: [], ['.', '..']));
|
||||
}
|
||||
|
||||
private function preparaCartella(string $cartella): void
|
||||
{
|
||||
if (! is_dir($cartella) && ! @mkdir($cartella, 0775, true) && ! is_dir($cartella)) {
|
||||
throw new RuntimeException('Impossibile preparare la cartella "' . $cartella . '" per il ripristino.');
|
||||
}
|
||||
}
|
||||
|
||||
private function sposta(string $da, string $a, array &$scambio): void
|
||||
{
|
||||
if (! @rename($da, $a)) {
|
||||
throw new RuntimeException(sprintf(
|
||||
'Impossibile spostare "%s" durante il ripristino. Di solito e\' un problema di permessi: '
|
||||
. 'controlla che il volume storage appartenga a www-data. Nessun dato e\' stato modificato.',
|
||||
basename($da)
|
||||
));
|
||||
}
|
||||
|
||||
$scambio['spostamenti'][] = [$da, $a];
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user