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:
@@ -2,6 +2,7 @@
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use Illuminate\Contracts\Cache\LockTimeoutException;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
|
||||
/**
|
||||
@@ -23,6 +24,18 @@ class BackupOperationState
|
||||
public const TIPO_RESTORE = 'restore';
|
||||
public const TIPO_RESET = 'reset';
|
||||
|
||||
/**
|
||||
* Gli import non sono operazioni di backup, ma occupano lo stesso posto:
|
||||
* l'import XML svuota e riscrive le tabelle, l'import PDF sostituisce file
|
||||
* e righe dei territori. Uno dei due in parallelo a un ripristino (o a un
|
||||
* backup che sta copiando quegli stessi file) produce uno stato che nessuno
|
||||
* ha chiesto. Il loro avanzamento non sta qui: l'import PDF ha
|
||||
* TerritorioPdfImportState, l'import XML e' sincrono. Qui c'e' solo
|
||||
* l'occupazione del posto.
|
||||
*/
|
||||
public const TIPO_IMPORT_PDF = 'import-pdf';
|
||||
public const TIPO_IMPORT_XML = 'import-xml';
|
||||
|
||||
/** L'archivio e' pronto ma l'azzeramento aspetta che venga scaricato. */
|
||||
public const FASE_ATTESA_DOWNLOAD = 'attesa-download';
|
||||
|
||||
@@ -48,6 +61,16 @@ class BackupOperationState
|
||||
private const CHIAVE_CORRENTE = 'backup-op:corrente';
|
||||
private const TTL_CORRENTE = 3600;
|
||||
|
||||
/**
|
||||
* Mutex attorno a "il posto e' libero? allora lo prendo".
|
||||
*
|
||||
* corrente() seguito da initialize() sono due passi: due richieste che
|
||||
* arrivano insieme vedono entrambe il posto libero, ed entrambe lo prendono
|
||||
* — la seconda sovrascrive il puntatore della prima, e i due lavori girano
|
||||
* in parallelo proprio come il posto unico doveva impedire.
|
||||
*/
|
||||
private const CHIAVE_AVVIO = 'backup-op:avvio';
|
||||
|
||||
public function initialize(string $id, string $tipo, string $fase, int $total = 0): array
|
||||
{
|
||||
$state = [
|
||||
@@ -99,6 +122,66 @@ class BackupOperationState
|
||||
return $id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Prende il posto unico se e' libero, in un passo solo.
|
||||
*
|
||||
* Restituisce null se il posto e' stato preso (e l'operazione inizializzata
|
||||
* come farebbe initialize()), altrimenti l'id dell'operazione che lo occupa.
|
||||
* Tutte le porte d'ingresso passano da qui proprio per il mutex: un
|
||||
* controllo con corrente() e un initialize() separati lasciano aperta la
|
||||
* finestra fra i due.
|
||||
*
|
||||
* $ttlPosto serve all'import XML, che e' sincrono: se il processo PHP muore a
|
||||
* meta' (timeout, errore fatale) nessun failed() chiude l'operazione, e il
|
||||
* posto resterebbe occupato per l'ora intera del TTL ordinario.
|
||||
*/
|
||||
public function occupa(string $id, string $tipo, string $fase, ?int $ttlPosto = null): ?string
|
||||
{
|
||||
try {
|
||||
return Cache::lock(self::CHIAVE_AVVIO, 10)->block(5, function () use ($id, $tipo, $fase, $ttlPosto) {
|
||||
$inCorso = $this->corrente();
|
||||
|
||||
if ($inCorso !== null) {
|
||||
return $inCorso;
|
||||
}
|
||||
|
||||
$this->initialize($id, $tipo, $fase);
|
||||
|
||||
if ($ttlPosto !== null) {
|
||||
Cache::put(self::CHIAVE_CORRENTE, $id, $ttlPosto);
|
||||
}
|
||||
|
||||
return null;
|
||||
});
|
||||
} catch (LockTimeoutException) {
|
||||
// Qualcun altro sta prendendo il posto proprio adesso: il mutex
|
||||
// copre pochi millisecondi, quindi cinque secondi d'attesa vogliono
|
||||
// dire che il posto non e' disponibile, non che va forzato.
|
||||
return $this->corrente() ?? 'avvio-in-corso';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Il rifiuto spiegato: chi si vede negare un import deve sapere che cosa lo
|
||||
* blocca, perche' l'operazione in corso puo' essere stata avviata da un
|
||||
* altro amministratore in un'altra zona della pagina.
|
||||
*/
|
||||
public function messaggioOccupato(string $idInCorso): string
|
||||
{
|
||||
$tipo = $this->get($idInCorso)['tipo'] ?? null;
|
||||
|
||||
$cosa = match ($tipo) {
|
||||
self::TIPO_BACKUP => ' (un backup)',
|
||||
self::TIPO_RESTORE => ' (un ripristino)',
|
||||
self::TIPO_RESET => ' (un azzeramento)',
|
||||
self::TIPO_IMPORT_PDF => ' (un import dei PDF dei territori)',
|
||||
self::TIPO_IMPORT_XML => ' (un import XML)',
|
||||
default => '',
|
||||
};
|
||||
|
||||
return 'C\'è già un\'operazione in corso' . $cosa . ': attendi che finisca prima di avviarne un\'altra.';
|
||||
}
|
||||
|
||||
public function get(string $id): ?array
|
||||
{
|
||||
return Cache::get($this->key($id));
|
||||
|
||||
Reference in New Issue
Block a user