++ fix e controlli
This commit is contained in:
294
app/Livewire/Concerns/SegueOperazioneBackup.php
Normal file
294
app/Livewire/Concerns/SegueOperazioneBackup.php
Normal file
@@ -0,0 +1,294 @@
|
||||
<?php
|
||||
|
||||
namespace App\Livewire\Concerns;
|
||||
|
||||
use App\Services\BackupOperationState;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
/**
|
||||
* Parte comune ai componenti che avviano e seguono un'operazione lunga di
|
||||
* backup: il posto unico, l'aggancio a quella in corso, il download automatico
|
||||
* dell'archivio appena e' pronto, e le righe di log spedite alla console.
|
||||
*
|
||||
* Esiste perche' le operazioni sono avviate da due punti della pagina distanti
|
||||
* fra loro — il backup dalla sua sezione, l'azzeramento dalla zona pericolosa in
|
||||
* fondo — ma sono la stessa cosa vista dallo stesso stato condiviso. Duplicare
|
||||
* postoLibero() significherebbe duplicare la regola su cui si regge il fatto che
|
||||
* due operazioni non si sovrascrivano: e' l'ultimo posto dove ha senso avere due
|
||||
* copie.
|
||||
*
|
||||
* Il resto dello stato vive in cache (BackupOperationState), non nel componente,
|
||||
* quindi due componenti che seguono la stessa operazione restano coerenti senza
|
||||
* doversi parlare.
|
||||
*/
|
||||
trait SegueOperazioneBackup
|
||||
{
|
||||
/** Operazione seguita in questo momento; null quando non c'e' nulla in corso. */
|
||||
public ?string $operationId = null;
|
||||
|
||||
public array $operazione = [];
|
||||
|
||||
/** Messaggio mostrato quando un avvio viene rifiutato. */
|
||||
public string $erroreOperazione = '';
|
||||
|
||||
/**
|
||||
* Quante righe di log sono gia' state spedite al browser.
|
||||
*
|
||||
* La console tiene il conto per conto suo, ma senza questo il componente
|
||||
* rispedirebbe l'intera coda a ogni poll: la console la scarterebbe, ma
|
||||
* intanto viaggerebbe dentro ogni risposta, sessanta righe alla volta,
|
||||
* poco meno di due volte al secondo.
|
||||
*/
|
||||
public int $logSpediti = 0;
|
||||
|
||||
/**
|
||||
* Riaggancia l'operazione gia' in corso.
|
||||
*
|
||||
* Senza, ricaricare la pagina durante un backup lungo farebbe sparire ogni
|
||||
* traccia, e un azzeramento fermo in attesa di conferma diventerebbe
|
||||
* irraggiungibile: nessuno potrebbe piu' ne' confermarlo ne' annullarlo, e
|
||||
* il posto resterebbe occupato fino alla scadenza.
|
||||
*/
|
||||
protected function agganciaOperazioneCorrente(): void
|
||||
{
|
||||
$this->operationId = app(BackupOperationState::class)->corrente();
|
||||
$this->logSpediti = 0;
|
||||
$this->aggiorna();
|
||||
}
|
||||
|
||||
public function aggiorna(): void
|
||||
{
|
||||
if (! $this->operationId) {
|
||||
return;
|
||||
}
|
||||
|
||||
$stato = app(BackupOperationState::class)->get($this->operationId);
|
||||
|
||||
if (! $stato) {
|
||||
$this->operationId = null;
|
||||
$this->operazione = [];
|
||||
$this->logSpediti = 0;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$this->operazione = $stato;
|
||||
|
||||
$this->spedisciAllaConsole();
|
||||
|
||||
// Il download parte da solo appena l'archivio e' pronto: per il backup
|
||||
// e' una comodita', per l'azzeramento e' il passaggio che sblocca la
|
||||
// cancellazione, quindi non puo' dipendere da un clic.
|
||||
if ($this->scaricaOra()) {
|
||||
$this->dispatch('scarica-backup', url: route('backup.file', $this->operationId));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Tipi di operazione che questo componente racconta alla console.
|
||||
*
|
||||
* Serve a stabilire un proprietario per ogni operazione. Backup e zona
|
||||
* pericolosa seguono lo stesso stato condiviso, quindi senza questa
|
||||
* distinzione entrambi spedirebbero le stesse righe e la console le
|
||||
* mostrerebbe due volte — e a raddoppiare sarebbe anche il polling.
|
||||
*
|
||||
* @return array<int, string>
|
||||
*/
|
||||
abstract protected function tipiSeguiti(): array;
|
||||
|
||||
protected function seguoQuestaOperazione(): bool
|
||||
{
|
||||
return in_array((string) ($this->operazione['tipo'] ?? ''), $this->tipiSeguiti(), true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Manda alla console le righe nuove e lo stato di avanzamento.
|
||||
*
|
||||
* La console e' un componente Alpine fuori dal ciclo di Livewire (i suoi
|
||||
* contenuti non possono essere ri-renderizzati dal server senza cancellare
|
||||
* le righe scritte dal browser), quindi la comunicazione e' a eventi.
|
||||
*/
|
||||
protected function spedisciAllaConsole(): void
|
||||
{
|
||||
// Chi non e' il proprietario tace: si aggiorna solo per sapere che il
|
||||
// posto e' occupato e disabilitare i propri pulsanti.
|
||||
if (! $this->seguoQuestaOperazione()) {
|
||||
return;
|
||||
}
|
||||
|
||||
$logs = $this->operazione['logs'] ?? [];
|
||||
|
||||
// Coda accorciata: e' un'operazione nuova, si riparte da capo.
|
||||
if (count($logs) < $this->logSpediti) {
|
||||
$this->logSpediti = 0;
|
||||
}
|
||||
|
||||
$nuove = array_slice($logs, $this->logSpediti);
|
||||
$this->logSpediti = count($logs);
|
||||
|
||||
$this->dispatch(
|
||||
'console-operazione',
|
||||
sorgente: $this->sorgenteConsole(),
|
||||
righe: array_values($nuove),
|
||||
etichetta: $this->etichettaOperazione(),
|
||||
fase: (string) ($this->operazione['fase'] ?? ''),
|
||||
percento: $this->percentuale,
|
||||
stato: (string) ($this->operazione['status'] ?? ''),
|
||||
);
|
||||
}
|
||||
|
||||
protected function sorgenteConsole(): string
|
||||
{
|
||||
return match ($this->operazione['tipo'] ?? null) {
|
||||
BackupOperationState::TIPO_BACKUP => 'backup',
|
||||
BackupOperationState::TIPO_RESTORE => 'ripristino',
|
||||
BackupOperationState::TIPO_RESET => 'azzeramento',
|
||||
default => 'server',
|
||||
};
|
||||
}
|
||||
|
||||
protected function etichettaOperazione(): string
|
||||
{
|
||||
return match ($this->operazione['tipo'] ?? null) {
|
||||
BackupOperationState::TIPO_BACKUP => 'Backup',
|
||||
BackupOperationState::TIPO_RESTORE => 'Ripristino',
|
||||
BackupOperationState::TIPO_RESET => 'Azzeramento',
|
||||
default => '',
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Vero se non c'e' gia' un'operazione in corso.
|
||||
*
|
||||
* Backup, ripristino e azzeramento lavorano tutti sullo stesso database e
|
||||
* sullo stesso disco: farne partire due insieme — due schede, due
|
||||
* amministratori — non li rallenta soltanto, li fa sovrascrivere a vicenda.
|
||||
*/
|
||||
protected function postoLibero(): bool
|
||||
{
|
||||
$inCorso = app(BackupOperationState::class)->corrente();
|
||||
|
||||
if ($inCorso === null) {
|
||||
$this->erroreOperazione = '';
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// Si aggancia a quella in corso invece di limitarsi a rifiutare: chi ha
|
||||
// premuto il pulsante vuole comunque vedere a che punto e'.
|
||||
$this->operationId = $inCorso;
|
||||
$this->logSpediti = 0;
|
||||
$this->aggiorna();
|
||||
$this->erroreOperazione = 'C\'e\' gia\' un\'operazione in corso: attendi che finisca prima di avviarne un\'altra.';
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
protected function nuovaOperazione(string $tipo): string
|
||||
{
|
||||
$operationId = (string) Str::uuid();
|
||||
|
||||
$stato = app(BackupOperationState::class);
|
||||
$stato->initialize($operationId, $tipo, 'In coda');
|
||||
$stato->log($operationId, 'Operazione creata. In attesa del worker di coda.');
|
||||
|
||||
$this->logSpediti = 0;
|
||||
|
||||
return $operationId;
|
||||
}
|
||||
|
||||
public function chiudi(): void
|
||||
{
|
||||
$this->operationId = null;
|
||||
$this->operazione = [];
|
||||
$this->logSpediti = 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* True una volta sola per operazione: l'evento di download va emesso quando
|
||||
* l'archivio compare, non a ogni poll successivo.
|
||||
*
|
||||
* Il segno che l'evento e' gia' partito sta nello stato condiviso, ed e'
|
||||
* proprio per questo che solo il proprietario puo' arrivare fin qui: il
|
||||
* componente che segue l'operazione di riflesso lo consumerebbe per primo —
|
||||
* polla anche lui — spegnendo il download prima che il proprietario lo
|
||||
* emetta. L'archivio sarebbe pronto e non arriverebbe a nessuno, e per
|
||||
* l'azzeramento sarebbe pure il passo che sblocca la cancellazione.
|
||||
*/
|
||||
private function scaricaOra(): bool
|
||||
{
|
||||
if (! $this->seguoQuestaOperazione()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$pronto = ($this->operazione['result']['file'] ?? null) !== null;
|
||||
$giaInviato = $this->operazione['result']['download_inviato'] ?? false;
|
||||
|
||||
if (! $pronto || $giaInviato) {
|
||||
return false;
|
||||
}
|
||||
|
||||
app(BackupOperationState::class)->update($this->operationId, function (array $state) {
|
||||
$state['result']['download_inviato'] = true;
|
||||
|
||||
return $state;
|
||||
});
|
||||
|
||||
$this->operazione['result']['download_inviato'] = true;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/** Percentuale della fase corrente, 0-100. */
|
||||
public function getPercentualeProperty(): int
|
||||
{
|
||||
if (($this->operazione['status'] ?? null) === 'completed') {
|
||||
return 100;
|
||||
}
|
||||
|
||||
$total = (int) ($this->operazione['total'] ?? 0);
|
||||
|
||||
if ($total <= 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return (int) min(100, round((int) ($this->operazione['processed'] ?? 0) / $total * 100));
|
||||
}
|
||||
|
||||
/** Se true c'e' un'operazione aperta: i pulsanti di avvio restano disabilitati. */
|
||||
public function getInCorsoProperty(): bool
|
||||
{
|
||||
return $this->operationId !== null
|
||||
&& in_array($this->operazione['status'] ?? '', ['queued', 'running'], true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Ogni quanto reinterrogare lo stato.
|
||||
*
|
||||
* Tre regimi, per tre esigenze diverse:
|
||||
*
|
||||
* - 750ms per il proprietario mentre un job lavora: la barra deve muoversi e
|
||||
* le righe di log devono arrivare mentre succedono, non dopo.
|
||||
* - 5s in attesa-download, dove si aspetta una persona che puo' metterci
|
||||
* minuti: bussare ogni 750ms vorrebbe dire migliaia di richieste inutili se
|
||||
* qualcuno lascia la scheda aperta e se ne va.
|
||||
* - 3s per il componente che NON possiede l'operazione. Non ha righe da
|
||||
* raccontare, ma deve accorgersi che il posto si e' liberato: senza,
|
||||
* il suo pulsante resterebbe disabilitato fino al ricaricamento della
|
||||
* pagina, con l'aria di essersi rotto.
|
||||
*/
|
||||
public function getIntervalloPollProperty(): string
|
||||
{
|
||||
if (! $this->seguoQuestaOperazione()) {
|
||||
return '3s';
|
||||
}
|
||||
|
||||
return $this->attesaDownload ? '5s' : '750ms';
|
||||
}
|
||||
|
||||
/** Vero quando l'azzeramento ha l'archivio pronto e aspetta la conferma. */
|
||||
public function getAttesaDownloadProperty(): bool
|
||||
{
|
||||
return ($this->operazione['fase'] ?? null) === BackupOperationState::FASE_ATTESA_DOWNLOAD;
|
||||
}
|
||||
}
|
||||
@@ -115,14 +115,27 @@ class Home extends Component
|
||||
}
|
||||
|
||||
// Average assignment duration in months (current year only, matches old app "Media Percorrenza Congregazione")
|
||||
//
|
||||
// Il calcolo sta in PHP e non in SQL. La versione precedente usava
|
||||
// DATEDIFF() e YEAR(), che sono funzioni MySQL: rendevano questa pagina
|
||||
// — la piu' visitata dell'applicazione — l'unica non verificabile dalla
|
||||
// suite di test, che gira su SQLite. Il costo e' due colonne di date per
|
||||
// le assegnazioni chiuse di un anno di servizio: qualche centinaio di
|
||||
// righe, non un tavolo intero.
|
||||
//
|
||||
// I due filtri sono quelli di prima: le date assurde arrivate dalla
|
||||
// conversione del vecchio database, e le durate non positive, che
|
||||
// altrimenti abbasserebbero la media senza significare niente.
|
||||
$avgGiorni = null;
|
||||
if ($annoCorrente) {
|
||||
$avgGiorni = Assegnazione::where('anno_teocratico_id', $annoCorrente->id)
|
||||
$durate = Assegnazione::where('anno_teocratico_id', $annoCorrente->id)
|
||||
->whereNotNull('returned_at')
|
||||
->whereRaw('YEAR(assigned_at) >= 1900')
|
||||
->whereRaw('DATEDIFF(returned_at, assigned_at) > 0')
|
||||
->selectRaw('AVG(DATEDIFF(returned_at, assigned_at)) as media_giorni')
|
||||
->value('media_giorni');
|
||||
->get(['assigned_at', 'returned_at'])
|
||||
->filter(fn (Assegnazione $a) => $a->assigned_at?->year >= 1900)
|
||||
->map(fn (Assegnazione $a) => $a->assigned_at->diffInDays($a->returned_at, false))
|
||||
->filter(fn (float $giorni) => $giorni > 0);
|
||||
|
||||
$avgGiorni = $durate->isEmpty() ? null : $durate->avg();
|
||||
}
|
||||
$mediaDurataPercorrenzaMesi = $avgGiorni ? round($avgGiorni / 30.44, 1) : 0;
|
||||
|
||||
|
||||
@@ -3,64 +3,41 @@
|
||||
namespace App\Livewire\Settings;
|
||||
|
||||
use App\Jobs\CreateBackupArchive;
|
||||
use App\Jobs\ResetApplication;
|
||||
use App\Livewire\Concerns\SegueOperazioneBackup;
|
||||
use App\Services\BackupOperationState;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
use Illuminate\Support\Facades\RateLimiter;
|
||||
use Livewire\Component;
|
||||
|
||||
/**
|
||||
* Avvio e monitoraggio delle operazioni lunghe: backup, ripristino,
|
||||
* azzeramento.
|
||||
* Le due zone che si usano tutti i giorni: creare il pacchetto di backup e
|
||||
* ripristinarlo.
|
||||
*
|
||||
* Il lavoro vero sta nei job; qui si avvia e si interroga lo stato. La barra
|
||||
* mostra `processed/total` dell'operazione in corso, cioe' unita' di lavoro
|
||||
* davvero completate — file finiti nell'archivio, file estratti, tabelle
|
||||
* svuotate — non un'animazione a tempo.
|
||||
* Il lavoro vero sta nei job; qui si avvia e si interroga lo stato. L'avanzamento
|
||||
* e le righe di log non compaiono piu' in questa sezione: finiscono nella console
|
||||
* unica agganciata in fondo alla pagina, che le riceve per eventi (vedi il trait
|
||||
* SegueOperazioneBackup). Qui restano solo i comandi e l'esito.
|
||||
*
|
||||
* L'azzeramento sta in ZonaPericolosa, che e' un componente a se' perche' deve
|
||||
* comparire in fondo alla pagina, dopo tutte le altre zone: dentro questa
|
||||
* sezione finiva a meta' pagina, fra il ripristino e l'import.
|
||||
*/
|
||||
class BackupManager extends Component
|
||||
{
|
||||
/** Operazione seguita in questo momento; null quando non c'e' nulla in corso. */
|
||||
public ?string $operationId = null;
|
||||
|
||||
public array $operazione = [];
|
||||
|
||||
/** Frase da digitare per sbloccare l'azzeramento. */
|
||||
public const CONFERMA_AZZERAMENTO = 'AZZERA TUTTO';
|
||||
|
||||
/** Password sbagliate ammesse prima di bloccare l'azzeramento, e per quanto. */
|
||||
private const TENTATIVI_PASSWORD = 5;
|
||||
private const FINESTRA_TENTATIVI = 300;
|
||||
|
||||
public string $confermaReset = '';
|
||||
|
||||
/**
|
||||
* Esiste solo per dare un nome agli errori del campo password: Livewire
|
||||
* scarta dal messaggio di risposta ogni errore la cui chiave non
|
||||
* corrisponda a una proprieta' del componente (SupportValidation::dehydrate),
|
||||
* quindi senza questa riga "Password non corretta" non arriverebbe mai a
|
||||
* schermo e il pulsante sembrerebbe non fare nulla.
|
||||
*
|
||||
* NON collegarla al campo con wire:model: la password viaggia come
|
||||
* parametro di confermaAzzeramento() proprio per non finire nello
|
||||
* snapshot, che Livewire rimanda al browser a ogni risposta. Qui resta
|
||||
* sempre stringa vuota.
|
||||
*/
|
||||
public string $passwordConferma = '';
|
||||
|
||||
/** Messaggio mostrato in cima quando un avvio viene rifiutato. */
|
||||
public string $erroreOperazione = '';
|
||||
use SegueOperazioneBackup;
|
||||
|
||||
public function mount(): void
|
||||
{
|
||||
$this->authorize('settings.manage');
|
||||
|
||||
// Riaggancia un'operazione gia' in corso. Senza, ricaricare la pagina
|
||||
// durante un backup lungo farebbe sparire la barra, e un azzeramento
|
||||
// fermo in attesa di conferma diventerebbe irraggiungibile: nessuno
|
||||
// potrebbe piu' ne' confermarlo ne' annullarlo.
|
||||
$this->operationId = app(BackupOperationState::class)->corrente();
|
||||
$this->aggiorna();
|
||||
$this->agganciaOperazioneCorrente();
|
||||
}
|
||||
|
||||
/**
|
||||
* Backup e ripristino si raccontano da qui; l'azzeramento e' della zona
|
||||
* pericolosa. Vedi SegueOperazioneBackup::tipiSeguiti().
|
||||
*/
|
||||
protected function tipiSeguiti(): array
|
||||
{
|
||||
return [BackupOperationState::TIPO_BACKUP, BackupOperationState::TIPO_RESTORE];
|
||||
}
|
||||
|
||||
public function avviaBackup(): void
|
||||
@@ -76,303 +53,55 @@ class BackupManager extends Component
|
||||
CreateBackupArchive::dispatch($this->operationId, false);
|
||||
|
||||
$this->aggiorna();
|
||||
|
||||
$this->avvisaZonaPericolosa();
|
||||
}
|
||||
|
||||
/**
|
||||
* Vero se non c'e' gia' un'operazione in corso.
|
||||
*
|
||||
* Backup, ripristino e azzeramento lavorano tutti sullo stesso database e
|
||||
* sullo stesso disco: farne partire due insieme — due schede, due
|
||||
* amministratori — non li rallenta soltanto, li fa sovrascrivere a vicenda.
|
||||
*/
|
||||
private function postoLibero(): bool
|
||||
{
|
||||
$inCorso = app(BackupOperationState::class)->corrente();
|
||||
|
||||
if ($inCorso === null) {
|
||||
$this->erroreOperazione = '';
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// Si aggancia a quella in corso invece di limitarsi a rifiutare: chi ha
|
||||
// premuto il pulsante vuole comunque vedere a che punto e'.
|
||||
$this->operationId = $inCorso;
|
||||
$this->aggiorna();
|
||||
$this->erroreOperazione = 'C\'e\' gia\' un\'operazione in corso: attendi che finisca prima di avviarne un\'altra.';
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Primo tempo dell'azzeramento: si costruisce il backup e lo si scarica.
|
||||
* La cancellazione non parte da qui — serve il secondo tempo,
|
||||
* confermaAzzeramento(), a valle del download.
|
||||
*/
|
||||
public function avviaAzzeramento(): void
|
||||
{
|
||||
$this->authorize('settings.manage');
|
||||
|
||||
$this->validate(
|
||||
['confermaReset' => 'required|in:' . self::CONFERMA_AZZERAMENTO],
|
||||
['confermaReset.in' => 'Scrivi esattamente "' . self::CONFERMA_AZZERAMENTO . '" per confermare.',
|
||||
'confermaReset.required' => 'Scrivi "' . self::CONFERMA_AZZERAMENTO . '" per confermare.']
|
||||
);
|
||||
|
||||
if (! $this->postoLibero()) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->confermaReset = '';
|
||||
$this->operationId = $this->nuovaOperazione(BackupOperationState::TIPO_RESET);
|
||||
|
||||
CreateBackupArchive::dispatch($this->operationId, true);
|
||||
|
||||
$this->aggiorna();
|
||||
}
|
||||
|
||||
/**
|
||||
* Secondo tempo dell'azzeramento: parte solo su conferma esplicita, dopo
|
||||
* che l'archivio e' stato servito.
|
||||
*
|
||||
* La conferma umana non e' un doppione del `wire:confirm` iniziale: e' il
|
||||
* solo punto in cui si sa che il backup e' davvero sul computer di chi sta
|
||||
* cancellando. Il server puo' verificare di averlo *spedito*
|
||||
* (`servito_at`), non che sia arrivato: nginx bufferizza la risposta, il
|
||||
* processo PHP la considera scritta e un download annullato dal browser
|
||||
* resterebbe indistinguibile da uno riuscito. Chiedere all'operatore e'
|
||||
* l'unico controllo che copre anche quel caso.
|
||||
*/
|
||||
public function confermaAzzeramento(string $password = ''): void
|
||||
{
|
||||
$this->authorize('settings.manage');
|
||||
|
||||
$this->aggiorna();
|
||||
|
||||
if (($this->operazione['fase'] ?? null) !== BackupOperationState::FASE_ATTESA_DOWNLOAD) {
|
||||
$this->addError('confermaReset', 'Nessun azzeramento in attesa di conferma.');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (($this->operazione['result']['servito_at'] ?? null) === null) {
|
||||
$this->addError('confermaReset', 'Scarica prima il backup: senza, l\'azzeramento non parte.');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (! $this->passwordConfermata($password)) {
|
||||
return;
|
||||
}
|
||||
|
||||
app(BackupOperationState::class)->update($this->operationId, function (array $state) {
|
||||
$state['fase'] = 'Azzeramento in avvio';
|
||||
$state['processed'] = 0;
|
||||
$state['total'] = 0;
|
||||
|
||||
return $state;
|
||||
});
|
||||
|
||||
ResetApplication::dispatch($this->operationId, (int) auth()->id());
|
||||
|
||||
$this->aggiorna();
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifica la password dell'amministratore prima della cancellazione.
|
||||
*
|
||||
* A questo punto la sessione e' gia' autenticata, quindi la password non
|
||||
* serve a stabilire *chi* sia: serve a stabilire che davanti allo schermo
|
||||
* ci sia ancora quella persona. E' l'ultimo passo prima di
|
||||
* un'operazione irreversibile, e una scheda lasciata aperta su una
|
||||
* postazione condivisa e' lo scenario esatto contro cui protegge.
|
||||
*
|
||||
* La password arriva come parametro del metodo e non come proprieta' del
|
||||
* componente: le proprieta' pubbliche di Livewire vengono serializzate
|
||||
* nello snapshot e rispedite al browser a ogni risposta, e una password
|
||||
* non ha motivo di fare quel viaggio di ritorno.
|
||||
*/
|
||||
private function passwordConfermata(string $password): bool
|
||||
{
|
||||
$utente = auth()->user();
|
||||
$chiave = 'azzeramento-password:' . $utente->id;
|
||||
|
||||
// Senza un limite, la richiesta di password su una sessione dirottata
|
||||
// sarebbe solo un rallentamento: si proverebbero le password comuni a
|
||||
// raffica. Il blocco e' sull'azione, non sull'account, quindi nessuno
|
||||
// resta chiuso fuori dall'applicazione.
|
||||
if (RateLimiter::tooManyAttempts($chiave, self::TENTATIVI_PASSWORD)) {
|
||||
$this->addError('passwordConferma', sprintf(
|
||||
'Troppi tentativi. Riprova fra %d secondi.',
|
||||
RateLimiter::availableIn($chiave)
|
||||
));
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($password === '') {
|
||||
$this->addError('passwordConferma', 'Inserisci la tua password per confermare.');
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
if (! Hash::check($password, $utente->getAuthPassword())) {
|
||||
RateLimiter::hit($chiave, self::FINESTRA_TENTATIVI);
|
||||
$this->addError('passwordConferma', 'Password non corretta.');
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
RateLimiter::clear($chiave);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Annulla un azzeramento fermo in attesa di conferma. Il backup resta
|
||||
* scaricabile: e' comunque un backup valido.
|
||||
*/
|
||||
public function annullaAzzeramento(): void
|
||||
{
|
||||
$this->authorize('settings.manage');
|
||||
|
||||
if (($this->operazione['fase'] ?? null) !== BackupOperationState::FASE_ATTESA_DOWNLOAD) {
|
||||
return;
|
||||
}
|
||||
|
||||
app(BackupOperationState::class)->markCompleted($this->operationId, [
|
||||
'messaggio' => 'Azzeramento annullato. Il backup appena creato resta scaricabile.',
|
||||
]);
|
||||
|
||||
// Da qui in poi e' un backup come un altro: la scheda mostra il
|
||||
// pulsante di download, non piu' la conferma di cancellazione.
|
||||
app(BackupOperationState::class)->update($this->operationId, function (array $state) {
|
||||
$state['tipo'] = BackupOperationState::TIPO_BACKUP;
|
||||
|
||||
return $state;
|
||||
});
|
||||
|
||||
$this->aggiorna();
|
||||
}
|
||||
|
||||
/**
|
||||
* Aggancia un'operazione avviata fuori da Livewire (il ripristino, che
|
||||
* parte da un upload XHR e restituisce il suo id in JSON).
|
||||
* Aggancia un'operazione avviata fuori da Livewire: il ripristino parte da un
|
||||
* upload XHR e restituisce il proprio id in JSON.
|
||||
*/
|
||||
public function seguiOperazione(string $operationId): void
|
||||
{
|
||||
$this->authorize('settings.manage');
|
||||
|
||||
$this->operationId = $operationId;
|
||||
$this->logSpediti = 0;
|
||||
$this->aggiorna();
|
||||
}
|
||||
|
||||
public function aggiorna(): void
|
||||
{
|
||||
if (! $this->operationId) {
|
||||
return;
|
||||
}
|
||||
|
||||
$stato = app(BackupOperationState::class)->get($this->operationId);
|
||||
|
||||
if (! $stato) {
|
||||
$this->operationId = null;
|
||||
$this->operazione = [];
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$this->operazione = $stato;
|
||||
|
||||
// Il download parte da solo appena l'archivio e' pronto: per il backup
|
||||
// e' una comodita', per l'azzeramento e' il passaggio che sblocca la
|
||||
// cancellazione, quindi non puo' dipendere da un clic.
|
||||
if ($this->scaricaOra()) {
|
||||
$this->dispatch('scarica-backup', url: route('backup.file', $this->operationId));
|
||||
}
|
||||
}
|
||||
|
||||
public function chiudi(): void
|
||||
{
|
||||
$this->operationId = null;
|
||||
$this->operazione = [];
|
||||
$this->avvisaZonaPericolosa();
|
||||
}
|
||||
|
||||
/**
|
||||
* True una volta sola per operazione: l'evento di download va emesso quando
|
||||
* l'archivio compare, non a ogni poll successivo.
|
||||
*/
|
||||
private function scaricaOra(): bool
|
||||
{
|
||||
$pronto = ($this->operazione['result']['file'] ?? null) !== null;
|
||||
$giaInviato = $this->operazione['result']['download_inviato'] ?? false;
|
||||
|
||||
if (! $pronto || $giaInviato) {
|
||||
return false;
|
||||
}
|
||||
|
||||
app(BackupOperationState::class)->update($this->operationId, function (array $state) {
|
||||
$state['result']['download_inviato'] = true;
|
||||
|
||||
return $state;
|
||||
});
|
||||
|
||||
$this->operazione['result']['download_inviato'] = true;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private function nuovaOperazione(string $tipo): string
|
||||
{
|
||||
$operationId = (string) \Illuminate\Support\Str::uuid();
|
||||
|
||||
$stato = app(BackupOperationState::class);
|
||||
$stato->initialize($operationId, $tipo, 'In coda');
|
||||
$stato->log($operationId, 'Operazione creata. In attesa del worker di coda.');
|
||||
|
||||
return $operationId;
|
||||
}
|
||||
|
||||
/** Percentuale della fase corrente, 0-100. */
|
||||
public function getPercentualeProperty(): int
|
||||
{
|
||||
$stato = $this->operazione['status'] ?? null;
|
||||
|
||||
if ($stato === 'completed') {
|
||||
return 100;
|
||||
}
|
||||
|
||||
$total = (int) ($this->operazione['total'] ?? 0);
|
||||
|
||||
if ($total <= 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
$processed = (int) ($this->operazione['processed'] ?? 0);
|
||||
|
||||
return (int) min(100, round($processed / $total * 100));
|
||||
}
|
||||
|
||||
/** Se true la pagina continua a interrogare lo stato. */
|
||||
public function getInCorsoProperty(): bool
|
||||
{
|
||||
return $this->operationId !== null
|
||||
&& in_array($this->operazione['status'] ?? '', ['queued', 'running'], true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Ogni quanto reinterrogare lo stato.
|
||||
* Il posto e' uno solo: la zona pericolosa deve sapere che e' occupato,
|
||||
* altrimenti continuerebbe a offrire il proprio pulsante durante un backup o
|
||||
* un ripristino.
|
||||
*
|
||||
* Mentre un job lavora servono aggiornamenti fitti, la barra deve muoversi.
|
||||
* In attesa-download invece si aspetta una persona, che puo' metterci
|
||||
* minuti: continuare a bussare ogni 750ms vorrebbe dire migliaia di
|
||||
* richieste inutili se qualcuno lascia la scheda aperta e se ne va.
|
||||
* L'evento e' indirizzato, non gridato: senza ->to() tornerebbe anche a
|
||||
* questo componente, che si riaggancerebbe a un'operazione appena
|
||||
* agganciata — una richiesta in piu' a ogni avvio, per niente.
|
||||
*/
|
||||
public function getIntervalloPollProperty(): string
|
||||
private function avvisaZonaPericolosa(): void
|
||||
{
|
||||
return ($this->operazione['fase'] ?? null) === BackupOperationState::FASE_ATTESA_DOWNLOAD
|
||||
? '5s'
|
||||
: '750ms';
|
||||
$this->dispatch('operazione-avviata')->to(ZonaPericolosa::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* L'azzeramento avviato dalla zona pericolosa non passa da qui, ma occupa lo
|
||||
* stesso posto: senza riagganciarsi, questa sezione resterebbe convinta che
|
||||
* sia libero e offrirebbe il pulsante "Crea backup" durante una
|
||||
* cancellazione.
|
||||
*/
|
||||
#[\Livewire\Attributes\On('operazione-avviata')]
|
||||
public function agganciaAltrove(): void
|
||||
{
|
||||
$this->agganciaOperazioneCorrente();
|
||||
}
|
||||
|
||||
/** Vero quando l'archivio di questa operazione e' scaricabile. */
|
||||
public function getArchivioProntoProperty(): bool
|
||||
{
|
||||
return ($this->operazione['result']['file'] ?? null) !== null;
|
||||
}
|
||||
|
||||
public function render()
|
||||
|
||||
@@ -33,7 +33,15 @@ class XmlExchange extends Component
|
||||
public array $pdfImportIssues = [];
|
||||
public ?string $currentPdfImportId = null;
|
||||
public string $pdfImportStatus = 'idle';
|
||||
public string $pdfImportLogText = '';
|
||||
|
||||
/**
|
||||
* Quante righe di log dell'import sono gia' state spedite alla console.
|
||||
*
|
||||
* La coda dell'import non ha tetto (un import di duemila PDF fa duemila
|
||||
* righe): rispedirla intera a ogni poll significherebbe trascinarsi dietro
|
||||
* l'intera cronologia dentro ogni risposta, una volta al secondo.
|
||||
*/
|
||||
public int $logPdfSpediti = 0;
|
||||
|
||||
public function mount(): void
|
||||
{
|
||||
@@ -95,7 +103,57 @@ class XmlExchange extends Component
|
||||
$this->pdfImportStats = $state['stats'] ?? [];
|
||||
$this->pdfImportLogs = $state['logs'] ?? [];
|
||||
$this->pdfImportIssues = $state['issues'] ?? [];
|
||||
$this->pdfImportLogText = implode(PHP_EOL, $this->pdfImportLogs);
|
||||
|
||||
$this->spedisciImportAllaConsole();
|
||||
}
|
||||
|
||||
/**
|
||||
* Manda alla console unica le righe nuove dell'import PDF.
|
||||
*
|
||||
* La console e' un componente Alpine fuori dal ciclo di Livewire: i suoi
|
||||
* contenuti non possono essere ri-renderizzati dal server senza cancellare
|
||||
* le righe scritte dal browser, quindi si comunica a eventi. E' lo stesso
|
||||
* meccanismo di SegueOperazioneBackup, da cui questo componente non eredita
|
||||
* perche' l'import PDF ha uno stato tutto suo (TerritorioPdfImportState) e
|
||||
* non partecipa al posto unico: puo' girare mentre un backup lavora.
|
||||
*/
|
||||
protected function spedisciImportAllaConsole(): void
|
||||
{
|
||||
// Coda accorciata: e' un import nuovo, si riparte da capo.
|
||||
if (count($this->pdfImportLogs) < $this->logPdfSpediti) {
|
||||
$this->logPdfSpediti = 0;
|
||||
}
|
||||
|
||||
$nuove = array_slice($this->pdfImportLogs, $this->logPdfSpediti);
|
||||
$this->logPdfSpediti = count($this->pdfImportLogs);
|
||||
|
||||
$totale = (int) ($this->pdfImportStats['total'] ?? 0);
|
||||
$fatti = (int) ($this->pdfImportStats['processed'] ?? 0);
|
||||
|
||||
$this->dispatch(
|
||||
'console-operazione',
|
||||
sorgente: 'import-pdf',
|
||||
righe: array_values($nuove),
|
||||
etichetta: 'Import PDF',
|
||||
fase: $totale > 0 ? $fatti . ' / ' . $totale . ' file' : '',
|
||||
percento: $this->percentualeImportPdf($fatti, $totale),
|
||||
stato: $this->pdfImportStatus,
|
||||
);
|
||||
}
|
||||
|
||||
private function percentualeImportPdf(int $fatti, int $totale): int
|
||||
{
|
||||
if ($this->pdfImportStatus === 'completed') {
|
||||
return 100;
|
||||
}
|
||||
|
||||
return $totale > 0 ? (int) min(100, round($fatti / $totale * 100)) : 0;
|
||||
}
|
||||
|
||||
/** Vero mentre l'import PDF e' in coda o in esecuzione. */
|
||||
public function getImportPdfInCorsoProperty(): bool
|
||||
{
|
||||
return in_array($this->pdfImportStatus, ['queued', 'running'], true);
|
||||
}
|
||||
|
||||
protected function dispatchPdfImport(string $importId, array $storedFiles, string $initialLog): void
|
||||
@@ -108,9 +166,13 @@ class XmlExchange extends Component
|
||||
$this->pdfImportStats = $state['stats'] ?? [];
|
||||
$this->pdfImportLogs = $state['logs'] ?? [];
|
||||
$this->pdfImportIssues = $state['issues'] ?? [];
|
||||
// Import nuovo: il conto delle righe gia' raccontate riparte, altrimenti
|
||||
// le prime righe di questo import verrebbero scambiate per quelle del
|
||||
// precedente e non comparirebbero.
|
||||
$this->logPdfSpediti = 0;
|
||||
$this->refreshPdfImportStatus();
|
||||
|
||||
session()->flash('success', 'Import PDF avviato in background. I log si aggiorneranno automaticamente.');
|
||||
session()->flash('success', 'Import PDF avviato in background. L\'avanzamento è nella console in fondo alla pagina.');
|
||||
}
|
||||
|
||||
public function downloadImportLogPdf()
|
||||
|
||||
251
app/Livewire/Settings/ZonaPericolosa.php
Normal file
251
app/Livewire/Settings/ZonaPericolosa.php
Normal file
@@ -0,0 +1,251 @@
|
||||
<?php
|
||||
|
||||
namespace App\Livewire\Settings;
|
||||
|
||||
use App\Jobs\CreateBackupArchive;
|
||||
use App\Jobs\ResetApplication;
|
||||
use App\Livewire\Concerns\SegueOperazioneBackup;
|
||||
use App\Services\BackupOperationState;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
use Illuminate\Support\Facades\RateLimiter;
|
||||
use Livewire\Component;
|
||||
|
||||
/**
|
||||
* Azzeramento dell'applicazione: l'unica operazione irreversibile della pagina,
|
||||
* e per questo l'ultima zona, dopo tutte le altre.
|
||||
*
|
||||
* E' un componente separato da BackupManager per una ragione di sola
|
||||
* disposizione: la zona pericolosa deve stare in fondo alla pagina, mentre le
|
||||
* sezioni di backup e ripristino stanno in cima. Rendendola dallo stesso
|
||||
* componente finiva a meta' pagina, in mezzo alle sezioni di import, che e'
|
||||
* l'ultimo posto in cui si vuole trovare un pulsante che cancella tutto.
|
||||
*
|
||||
* Lo stato dell'operazione resta condiviso con BackupManager attraverso
|
||||
* BackupOperationState (il trait), quindi i due non hanno bisogno di parlarsi per
|
||||
* restare coerenti: il posto unico e' uno solo per entrambi.
|
||||
*
|
||||
* L'azzeramento ha due tempi, separati dal download dell'archivio:
|
||||
*
|
||||
* 1. avviaAzzeramento() crea il backup e si ferma in attesa-download
|
||||
* 2. confermaAzzeramento() cancella, solo su conferma umana e con la password
|
||||
*
|
||||
* Il primo tentativo faceva partire la cancellazione dalla coda dello
|
||||
* streamDownload, "dopo l'ultimo byte": non funziona. Con fastcgi_buffering
|
||||
* attivo (il default) nginx accetta l'intera risposta dal processo PHP, che
|
||||
* prosegue anche se il browser ha chiuso la connessione al primo byte, quindi un
|
||||
* download annullato azzerava lo stesso. Il server puo' verificare di aver
|
||||
* *spedito* l'archivio, non che sia arrivato: quella verifica e' dell'operatore,
|
||||
* e per questo la conferma e' umana.
|
||||
*/
|
||||
class ZonaPericolosa extends Component
|
||||
{
|
||||
use SegueOperazioneBackup;
|
||||
|
||||
/** Frase da digitare per sbloccare l'azzeramento. */
|
||||
public const CONFERMA_AZZERAMENTO = 'AZZERA TUTTO';
|
||||
|
||||
/** Password sbagliate ammesse prima di bloccare l'azzeramento, e per quanto. */
|
||||
private const TENTATIVI_PASSWORD = 5;
|
||||
private const FINESTRA_TENTATIVI = 300;
|
||||
|
||||
public string $confermaReset = '';
|
||||
|
||||
/**
|
||||
* Esiste solo per dare un nome agli errori del campo password: Livewire
|
||||
* scarta dal messaggio di risposta ogni errore la cui chiave non
|
||||
* corrisponda a una proprieta' del componente (SupportValidation::dehydrate),
|
||||
* quindi senza questa riga "Password non corretta" non arriverebbe mai a
|
||||
* schermo e il pulsante sembrerebbe non fare nulla.
|
||||
*
|
||||
* NON collegarla al campo con wire:model: la password viaggia come
|
||||
* parametro di confermaAzzeramento() proprio per non finire nello
|
||||
* snapshot, che Livewire rimanda al browser a ogni risposta. Qui resta
|
||||
* sempre stringa vuota.
|
||||
*/
|
||||
public string $passwordConferma = '';
|
||||
|
||||
public function mount(): void
|
||||
{
|
||||
$this->authorize('settings.manage');
|
||||
|
||||
$this->agganciaOperazioneCorrente();
|
||||
}
|
||||
|
||||
/**
|
||||
* Solo l'azzeramento: backup e ripristino li racconta BackupManager. Vedi
|
||||
* SegueOperazioneBackup::tipiSeguiti().
|
||||
*/
|
||||
protected function tipiSeguiti(): array
|
||||
{
|
||||
return [BackupOperationState::TIPO_RESET];
|
||||
}
|
||||
|
||||
/**
|
||||
* Primo tempo: si costruisce il backup e lo si scarica. La cancellazione non
|
||||
* parte da qui.
|
||||
*/
|
||||
public function avviaAzzeramento(): void
|
||||
{
|
||||
$this->authorize('settings.manage');
|
||||
|
||||
$this->validate(
|
||||
['confermaReset' => 'required|in:' . self::CONFERMA_AZZERAMENTO],
|
||||
['confermaReset.in' => 'Scrivi esattamente "' . self::CONFERMA_AZZERAMENTO . '" per confermare.',
|
||||
'confermaReset.required' => 'Scrivi "' . self::CONFERMA_AZZERAMENTO . '" per confermare.']
|
||||
);
|
||||
|
||||
if (! $this->postoLibero()) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->confermaReset = '';
|
||||
$this->operationId = $this->nuovaOperazione(BackupOperationState::TIPO_RESET);
|
||||
|
||||
CreateBackupArchive::dispatch($this->operationId, true);
|
||||
|
||||
$this->aggiorna();
|
||||
|
||||
// La sezione di backup deve sapere che il posto e' occupato, altrimenti
|
||||
// continuerebbe a offrire il proprio pulsante durante l'azzeramento.
|
||||
// Indirizzato e non gridato: senza ->to() l'evento tornerebbe anche qui.
|
||||
$this->dispatch('operazione-avviata')->to(BackupManager::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Secondo tempo: parte solo su conferma esplicita, dopo che l'archivio e'
|
||||
* stato servito.
|
||||
*/
|
||||
public function confermaAzzeramento(string $password = ''): void
|
||||
{
|
||||
$this->authorize('settings.manage');
|
||||
|
||||
$this->aggiorna();
|
||||
|
||||
if (! $this->attesaDownload) {
|
||||
$this->addError('confermaReset', 'Nessun azzeramento in attesa di conferma.');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (($this->operazione['result']['servito_at'] ?? null) === null) {
|
||||
$this->addError('confermaReset', 'Scarica prima il backup: senza, l\'azzeramento non parte.');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (! $this->passwordConfermata($password)) {
|
||||
return;
|
||||
}
|
||||
|
||||
app(BackupOperationState::class)->update($this->operationId, function (array $state) {
|
||||
$state['fase'] = 'Azzeramento in avvio';
|
||||
$state['processed'] = 0;
|
||||
$state['total'] = 0;
|
||||
|
||||
return $state;
|
||||
});
|
||||
|
||||
ResetApplication::dispatch($this->operationId, (int) auth()->id());
|
||||
|
||||
$this->aggiorna();
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifica la password dell'amministratore prima della cancellazione.
|
||||
*
|
||||
* A questo punto la sessione e' gia' autenticata, quindi la password non
|
||||
* serve a stabilire *chi* sia: serve a stabilire che davanti allo schermo
|
||||
* ci sia ancora quella persona. E' l'ultimo passo prima di un'operazione
|
||||
* irreversibile, e una scheda lasciata aperta su una postazione condivisa e'
|
||||
* lo scenario esatto contro cui protegge.
|
||||
*/
|
||||
private function passwordConfermata(string $password): bool
|
||||
{
|
||||
$utente = auth()->user();
|
||||
$chiave = 'azzeramento-password:' . $utente->id;
|
||||
|
||||
// Senza un limite, la richiesta di password su una sessione dirottata
|
||||
// sarebbe solo un rallentamento: si proverebbero le password comuni a
|
||||
// raffica. Il blocco e' sull'azione, non sull'account, quindi nessuno
|
||||
// resta chiuso fuori dall'applicazione.
|
||||
if (RateLimiter::tooManyAttempts($chiave, self::TENTATIVI_PASSWORD)) {
|
||||
$this->addError('passwordConferma', sprintf(
|
||||
'Troppi tentativi. Riprova fra %d secondi.',
|
||||
RateLimiter::availableIn($chiave)
|
||||
));
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($password === '') {
|
||||
$this->addError('passwordConferma', 'Inserisci la tua password per confermare.');
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
if (! Hash::check($password, $utente->getAuthPassword())) {
|
||||
RateLimiter::hit($chiave, self::FINESTRA_TENTATIVI);
|
||||
$this->addError('passwordConferma', 'Password non corretta.');
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
RateLimiter::clear($chiave);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Annulla un azzeramento fermo in attesa di conferma. Il backup resta
|
||||
* scaricabile: e' comunque un backup valido.
|
||||
*/
|
||||
public function annullaAzzeramento(): void
|
||||
{
|
||||
$this->authorize('settings.manage');
|
||||
|
||||
if (! $this->attesaDownload) {
|
||||
return;
|
||||
}
|
||||
|
||||
app(BackupOperationState::class)->markCompleted($this->operationId, [
|
||||
'messaggio' => 'Azzeramento annullato. Il backup appena creato resta scaricabile.',
|
||||
]);
|
||||
|
||||
// Da qui in poi e' un backup come un altro: la sezione di backup mostra il
|
||||
// pulsante di download, non piu' la conferma di cancellazione.
|
||||
app(BackupOperationState::class)->update($this->operationId, function (array $state) {
|
||||
$state['tipo'] = BackupOperationState::TIPO_BACKUP;
|
||||
|
||||
return $state;
|
||||
});
|
||||
|
||||
$this->aggiorna();
|
||||
$this->dispatch('operazione-avviata')->to(BackupManager::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Un azzeramento avviato altrove non esiste, ma il posto puo' essere occupato
|
||||
* da un backup o da un ripristino: la zona deve saperlo per non offrire un
|
||||
* pulsante che verrebbe rifiutato.
|
||||
*/
|
||||
#[\Livewire\Attributes\On('operazione-avviata')]
|
||||
public function agganciaAltrove(): void
|
||||
{
|
||||
$this->agganciaOperazioneCorrente();
|
||||
}
|
||||
|
||||
/**
|
||||
* Vero quando questa zona sta seguendo un azzeramento (non un backup o un
|
||||
* ripristino avviati altrove).
|
||||
*/
|
||||
public function getAzzeramentoInCorsoProperty(): bool
|
||||
{
|
||||
return $this->operationId !== null
|
||||
&& ($this->operazione['tipo'] ?? null) === BackupOperationState::TIPO_RESET;
|
||||
}
|
||||
|
||||
public function render()
|
||||
{
|
||||
return view('livewire.settings.zona-pericolosa');
|
||||
}
|
||||
}
|
||||
@@ -121,10 +121,19 @@ class BackupArchive
|
||||
//
|
||||
// Il valore di ritorno si controlla: se il file e' sparito
|
||||
// fra allFiles() e qui — cancellato da un'altra richiesta,
|
||||
// o illeggibile per i permessi — addFile() torna false
|
||||
// senza sollevare, e l'archivio finirebbe con un buco di
|
||||
// cui nessuno si accorge fino al giorno del ripristino.
|
||||
if (! $zip->addFile($disk->path($relativePath), self::PREFIX_STORAGE . $relativePath)) {
|
||||
// o illeggibile per i permessi — addFile() torna false, e
|
||||
// l'archivio finirebbe con un buco di cui nessuno si
|
||||
// accorge fino al giorno del ripristino.
|
||||
//
|
||||
// La @ non e' pigrizia. Prima di restituire false,
|
||||
// addFile() emette un warning di PHP ("No such file or
|
||||
// directory") che il gestore di Laravel trasforma in
|
||||
// ErrorException: senza sopprimerlo, il messaggio qui sotto
|
||||
// — quello che dice *quale* file e *perche'* — non verrebbe
|
||||
// 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)) {
|
||||
throw new RuntimeException(
|
||||
'Impossibile aggiungere "' . $relativePath . '" al backup: '
|
||||
. 'il file non e\' leggibile o e\' stato rimosso durante la creazione.'
|
||||
|
||||
Reference in New Issue
Block a user