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>
140 lines
4.7 KiB
PHP
140 lines
4.7 KiB
PHP
<?php
|
|
|
|
namespace App\Jobs;
|
|
|
|
use App\Livewire\Settings\XmlExchange;
|
|
use App\Services\BackupArchive;
|
|
use App\Services\BackupOperationState;
|
|
use App\Services\Piantine\PiantineBackup;
|
|
use Illuminate\Bus\Queueable;
|
|
use Illuminate\Contracts\Queue\ShouldQueue;
|
|
use Illuminate\Foundation\Bus\Dispatchable;
|
|
use Illuminate\Queue\InteractsWithQueue;
|
|
use Illuminate\Queue\SerializesModels;
|
|
use Illuminate\Support\Facades\Log;
|
|
|
|
/**
|
|
* Costruisce il pacchetto di backup in coda.
|
|
*
|
|
* In coda e non nella richiesta perche' un'installazione con qualche centinaio
|
|
* di PDF supera comodamente i 300 secondi di `fastcgi_read_timeout`: il browser
|
|
* si vedrebbe chiudere la connessione a meta' compressione, senza modo di
|
|
* distinguerlo da un errore.
|
|
*/
|
|
class CreateBackupArchive implements ShouldQueue
|
|
{
|
|
use Dispatchable;
|
|
use InteractsWithQueue;
|
|
use Queueable;
|
|
use SerializesModels;
|
|
|
|
public int $tries = 1;
|
|
|
|
/** Sotto il --timeout=600 del worker: se sfora, e' il worker a ucciderlo. */
|
|
public int $timeout = 570;
|
|
|
|
/**
|
|
* @param bool $perAzzeramento Quando true il job non chiude l'operazione
|
|
* ma la lascia in attesa del download: e'
|
|
* l'azzeramento che aspetta di avere
|
|
* l'archivio al sicuro prima di partire.
|
|
*/
|
|
public function __construct(
|
|
public string $operationId,
|
|
public bool $perAzzeramento = false,
|
|
) {
|
|
}
|
|
|
|
public function handle(BackupOperationState $stato, BackupArchive $archivio, XmlExchange $exchange, PiantineBackup $piantine): void
|
|
{
|
|
$stato->markRunning($this->operationId);
|
|
$stato->fase($this->operationId, 'Lettura dei dati gestionali');
|
|
|
|
$destinazione = self::percorso($this->operationId);
|
|
|
|
try {
|
|
$this->rimuoviArchiviVecchi($archivio);
|
|
|
|
$xml = $exchange->currentXmlBackup();
|
|
$datiPiantine = $piantine->esporta();
|
|
|
|
$stato->fase($this->operationId, 'Compressione dei file');
|
|
|
|
$archivio->create(
|
|
$xml,
|
|
$datiPiantine,
|
|
$destinazione,
|
|
fn (int $fatti, int $totale) => $stato->avanza($this->operationId, $fatti, $totale)
|
|
);
|
|
} catch (\Throwable $e) {
|
|
Log::error('Creazione del backup non riuscita.', ['exception' => $e]);
|
|
$stato->markFailed($this->operationId, $e->getMessage());
|
|
|
|
return;
|
|
}
|
|
|
|
$risultato = [
|
|
'file' => basename($destinazione),
|
|
'nome_download' => 'termanager2-backup-' . now()->format('Ymd-His') . '.zip',
|
|
'dimensione' => filesize($destinazione) ?: 0,
|
|
];
|
|
|
|
if ($this->perAzzeramento) {
|
|
$stato->log($this->operationId, 'Archivio pronto. In attesa che il download sia completato.');
|
|
$stato->markAttesaDownload($this->operationId, $risultato);
|
|
|
|
return;
|
|
}
|
|
|
|
$stato->log($this->operationId, 'Backup pronto per il download.');
|
|
$stato->markCompleted($this->operationId, $risultato);
|
|
}
|
|
|
|
/**
|
|
* Chiamato da Laravel quando il job muore per cause che handle() non puo'
|
|
* intercettare: il timeout del worker (che uccide il processo), un errore
|
|
* fatale, una dipendenza non risolvibile.
|
|
*
|
|
* Senza, l'operazione resterebbe "in corso" e terrebbe occupato il posto
|
|
* unico fino alla scadenza del puntatore: un'ora in cui nessuno puo'
|
|
* avviare un backup e la pagina mostra una barra che non avanzera' mai.
|
|
*/
|
|
public function failed(?\Throwable $e): void
|
|
{
|
|
app(BackupOperationState::class)->markFailed(
|
|
$this->operationId,
|
|
$e?->getMessage() ?: 'Creazione del backup interrotta (timeout o errore del worker).'
|
|
);
|
|
|
|
@unlink(self::percorso($this->operationId));
|
|
}
|
|
|
|
public static function percorso(string $operationId): string
|
|
{
|
|
return storage_path('app/backups/' . $operationId . '.zip');
|
|
}
|
|
|
|
/**
|
|
* Gli archivi restano su disco finche' il browser non li scarica, quindi un
|
|
* download mai completato lascerebbe un file per sempre. Si ripulisce
|
|
* all'inizio di ogni nuovo backup invece di aggiungere un task pianificato:
|
|
* il momento in cui serve spazio e' esattamente questo.
|
|
*/
|
|
private function rimuoviArchiviVecchi(BackupArchive $archivio): void
|
|
{
|
|
$cartella = storage_path('app/backups');
|
|
|
|
if (! is_dir($cartella)) {
|
|
return;
|
|
}
|
|
|
|
$limite = now()->subDay()->getTimestamp();
|
|
|
|
foreach (glob($cartella . '/*.zip') ?: [] as $file) {
|
|
if (filemtime($file) < $limite) {
|
|
@unlink($file);
|
|
}
|
|
}
|
|
}
|
|
}
|