++ 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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user