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:
@@ -19,15 +19,106 @@ class TerritorioPdfImportDispatcher
|
||||
|
||||
public function __construct(
|
||||
protected TerritorioPdfImportState $stateService,
|
||||
protected BackupOperationState $postoUnico,
|
||||
) {
|
||||
}
|
||||
|
||||
public function dispatchStoredFiles(string $importId, array $storedFiles, ?int $actorId, string $initialLog): array
|
||||
/**
|
||||
* Prende il posto unico per questo import, o solleva spiegando chi lo occupa.
|
||||
*
|
||||
* L'import PDF sostituisce file e righe dei territori: in parallelo a un
|
||||
* ripristino scriverebbe su tabelle che l'altro sta riscrivendo, e durante
|
||||
* un backup l'archivio fotograferebbe meta' PDF vecchi e meta' nuovi (o
|
||||
* fallirebbe su un file cancellato fra l'elenco e la compressione). Il posto
|
||||
* si libera nel job, a fine lavoro o in failed().
|
||||
*/
|
||||
public function occupaPosto(string $importId): void
|
||||
{
|
||||
$inCorso = $this->postoUnico->occupa($importId, BackupOperationState::TIPO_IMPORT_PDF, 'In coda');
|
||||
|
||||
if ($inCorso !== null) {
|
||||
throw new RuntimeException($this->postoUnico->messaggioOccupato($inCorso));
|
||||
}
|
||||
}
|
||||
|
||||
/** Libera il posto preso da un import che non e' arrivato alla coda. */
|
||||
public function liberaPosto(string $importId, string $motivo): void
|
||||
{
|
||||
$this->postoUnico->markFailed($importId, $motivo);
|
||||
}
|
||||
|
||||
/**
|
||||
* Vero per i file che non vanno nemmeno considerati: quelli che non sono
|
||||
* PDF e quelli di sistema o nascosti.
|
||||
*
|
||||
* Una cartella vera non contiene solo PDF: Windows ci lascia Thumbs.db e
|
||||
* desktop.ini, macOS .DS_Store e — dentro gli ZIP — una copia "._nome.pdf"
|
||||
* di ogni file sotto __MACOSX/. Prima uno solo di questi faceva fallire la
|
||||
* validazione dell'intero caricamento; peggio, "._1.pdf" ha l'estensione
|
||||
* giusta e sarebbe finito come mappa del territorio 1.
|
||||
*/
|
||||
public static function daIgnorare(string $percorso): bool
|
||||
{
|
||||
$segmenti = array_values(array_filter(preg_split('#[/\\\\]+#', $percorso) ?: [], fn ($s) => $s !== ''));
|
||||
|
||||
if ($segmenti === []) {
|
||||
return true;
|
||||
}
|
||||
|
||||
foreach ($segmenti as $segmento) {
|
||||
if (str_starts_with($segmento, '.') || strcasecmp($segmento, '__MACOSX') === 0) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// "~$" e' il prefisso dei file temporanei di Office.
|
||||
$nome = end($segmenti);
|
||||
|
||||
return str_starts_with($nome, '~$')
|
||||
|| strtolower(pathinfo($nome, PATHINFO_EXTENSION)) !== 'pdf';
|
||||
}
|
||||
|
||||
/**
|
||||
* Mette in coda l'import. Il posto unico deve essere gia' stato preso con
|
||||
* occupaPosto(): se l'invio alla coda fallisce lo si libera qui.
|
||||
*
|
||||
* @param array<int, string> $ignorati nomi dei file scartati da daIgnorare(), solo per il log
|
||||
* @param array<int, array{file: string, message: string}> $scartati PDF rifiutati dalla validazione
|
||||
*/
|
||||
public function dispatchStoredFiles(string $importId, array $storedFiles, ?int $actorId, string $initialLog, array $ignorati = [], array $scartati = []): array
|
||||
{
|
||||
$state = $this->stateService->initialize($importId, count($storedFiles));
|
||||
$this->stateService->appendLog($importId, $initialLog);
|
||||
|
||||
ImportTerritoryPdfFolder::dispatch($importId, $storedFiles, $actorId);
|
||||
if ($ignorati !== []) {
|
||||
$elenco = array_slice($ignorati, 0, 10);
|
||||
$altri = count($ignorati) - count($elenco);
|
||||
$this->stateService->appendLog(
|
||||
$importId,
|
||||
'Ignorati ' . count($ignorati) . ' file che non sono PDF da importare (file di sistema, nascosti o di altro tipo): '
|
||||
. implode(', ', $elenco) . ($altri > 0 ? ' e altri ' . $altri : '') . '.'
|
||||
);
|
||||
}
|
||||
|
||||
foreach ($scartati as $scartato) {
|
||||
$this->stateService->appendLog($importId, '[SKIP] ' . $scartato['file'] . ' - ' . $scartato['message']);
|
||||
$this->stateService->addIssue($importId, [
|
||||
'file' => $scartato['file'],
|
||||
'type' => 'invalid-file',
|
||||
'message' => $scartato['message'],
|
||||
]);
|
||||
}
|
||||
|
||||
try {
|
||||
ImportTerritoryPdfFolder::dispatch($importId, $storedFiles, $actorId);
|
||||
} catch (\Throwable $exception) {
|
||||
// Il job non partira' mai, quindi non sara' lui a liberare il posto.
|
||||
$this->stateService->markFailed($importId);
|
||||
$this->liberaPosto($importId, 'Import PDF non messo in coda: ' . $exception->getMessage());
|
||||
Storage::disk('local')->deleteDirectory('bulk-territori-imports/' . $importId);
|
||||
|
||||
throw $exception;
|
||||
}
|
||||
|
||||
return $this->stateService->get($importId) ?? $state;
|
||||
}
|
||||
@@ -35,6 +126,27 @@ class TerritorioPdfImportDispatcher
|
||||
public function dispatchUploadedZip(UploadedFile $zipFile, ?int $actorId): string
|
||||
{
|
||||
$importId = (string) Str::uuid();
|
||||
|
||||
// Il posto si prende prima di salvare ed estrarre: e' la parte lenta, e
|
||||
// farla per poi sentirsi rispondere "c'e' un ripristino in corso" sarebbe
|
||||
// lavoro buttato.
|
||||
$this->occupaPosto($importId);
|
||||
|
||||
try {
|
||||
return $this->estraiEAccoda($zipFile, $importId, $actorId);
|
||||
} catch (\Throwable $exception) {
|
||||
// Se il job e' partito il posto e' suo e lo libera lui; altrimenti
|
||||
// resterebbe occupato da un import che non esiste.
|
||||
if ($this->stateService->get($importId) === null) {
|
||||
$this->liberaPosto($importId, 'Import PDF non avviato: ' . $exception->getMessage());
|
||||
}
|
||||
|
||||
throw $exception;
|
||||
}
|
||||
}
|
||||
|
||||
private function estraiEAccoda(UploadedFile $zipFile, string $importId, ?int $actorId): string
|
||||
{
|
||||
$zipStoredPath = $zipFile->storeAs(
|
||||
'bulk-territori-imports/' . $importId,
|
||||
'archivio-' . $importId . '.zip',
|
||||
@@ -45,10 +157,13 @@ class TerritorioPdfImportDispatcher
|
||||
$zip = new ZipArchive();
|
||||
|
||||
if ($zip->open($zipAbsolutePath) !== true) {
|
||||
Storage::disk('local')->deleteDirectory('bulk-territori-imports/' . $importId);
|
||||
|
||||
throw new RuntimeException('Impossibile aprire il file ZIP.');
|
||||
}
|
||||
|
||||
$storedFiles = [];
|
||||
$ignorati = [];
|
||||
$entryIndex = 0;
|
||||
|
||||
try {
|
||||
@@ -59,7 +174,8 @@ class TerritorioPdfImportDispatcher
|
||||
continue;
|
||||
}
|
||||
|
||||
if (strtolower(pathinfo($entryName, PATHINFO_EXTENSION)) !== 'pdf') {
|
||||
if (self::daIgnorare($entryName)) {
|
||||
$ignorati[] = basename($entryName);
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -111,6 +227,8 @@ class TerritorioPdfImportDispatcher
|
||||
}
|
||||
|
||||
if ($storedFiles === []) {
|
||||
Storage::disk('local')->deleteDirectory('bulk-territori-imports/' . $importId);
|
||||
|
||||
throw new RuntimeException('Lo ZIP non contiene file PDF validi.');
|
||||
}
|
||||
|
||||
@@ -118,7 +236,8 @@ class TerritorioPdfImportDispatcher
|
||||
$importId,
|
||||
$storedFiles,
|
||||
$actorId,
|
||||
'Archivio ZIP ricevuto: ' . count($storedFiles) . ' PDF estratti e messi in coda per l\'elaborazione.'
|
||||
'Archivio ZIP ricevuto: ' . count($storedFiles) . ' PDF estratti e messi in coda per l\'elaborazione.',
|
||||
$ignorati
|
||||
);
|
||||
|
||||
return $importId;
|
||||
|
||||
Reference in New Issue
Block a user