++ fix backup

This commit is contained in:
2026-07-29 18:08:36 +02:00
parent 8dee7b1f44
commit c1dc47b3fd

View File

@@ -20,153 +20,182 @@
}; };
@endphp @endphp
{{--
Il comportamento del riquadro di ripristino sta in un componente Alpine
registrato piu' sotto (@script), non in un x-data lungo.
Il motivo e' concreto: un attributo HTML finisce alla prima virgoletta
doppia, comprese quelle dentro un commento. Con il codice nell'attributo, un
banale // un generico "errore" ha chiuso l'attributo a meta' e ha riversato
tutto il resto del JavaScript a video come testo. Dentro un blocco <script>
virgolette e apostrofi non hanno alcun potere di rompere il markup.
Le stringhe usano comunque i backtick: e' la forma in cui un apostrofo
italiano non richiede alcun accorgimento.
--}}
<div class="space-y-6" <div class="space-y-6"
x-data="{ x-data="{
zipSelezionato: '', {{-- maxUploadKb() e' in kilobyte; file.size e' in byte. --}}
uploadInCorso: false, maxByte: {{ \App\Http\Controllers\Settings\BackupController::maxUploadKb() }} * 1000,
uploadPercento: 0, zipSelezionato: '',
erroreUpload: '', uploadInCorso: false,
passwordAzzeramento: '', uploadPercento: 0,
erroreUpload: '',
passwordAzzeramento: '',
azzera() { azzera() {
if (!this.passwordAzzeramento) { if (!this.passwordAzzeramento) {
return;
}
if (!confirm('Hai verificato il backup scaricato? Confermando, i dati vengono cancellati definitivamente.')) {
return;
}
const password = this.passwordAzzeramento;
// Svuotata subito: se e' sbagliata si ridigita, e intanto non resta
// nel campo di una scheda lasciata aperta.
this.passwordAzzeramento = '';
this.$wire.confermaAzzeramento(password);
},
/**
* Superato il tetto, e' nginx a chiudere la connessione a meta'
* trasferimento: la barra si pianta a una percentuale qualsiasi e non
* arriva nessun evento utile, quindi la pagina resta muta con l'aria di
* essersi bloccata. Controllare prima di partire rende quel caso un
* messaggio immediato invece di un'attesa senza fine.
*/
zipTroppoGrande(file) {
if (!file || file.size <= this.maxByte) {
return false;
}
const mb = (n) => (n / 1000000).toFixed(0);
this.erroreUpload = `Il pacchetto pesa ${mb(file.size)} MB e supera il limite di `
+ `${mb(this.maxByte)} MB accettato dal server. Per archivi più grandi alza MAX_UPLOAD_MB `
+ `nel .env (serve un redeploy) oppure usa il ripristino dai volumi descritto nel README.`;
return true;
},
fileScelto(event) {
this.erroreUpload = '';
this.uploadPercento = 0;
const file = event.target.files[0];
this.zipSelezionato = file
? `${file.name} (${(file.size / 1000000).toFixed(1)} MB)`
: '';
if (file) {
this.zipTroppoGrande(file);
}
},
inviaRipristino(event) {
const form = event.target;
const file = form.querySelector('input[name=backupZip]').files[0];
if (this.zipTroppoGrande(file)) {
return;
}
this.erroreUpload = '';
this.uploadInCorso = true;
this.uploadPercento = 0;
const xhr = new XMLHttpRequest();
xhr.open('POST', form.action);
xhr.setRequestHeader('Accept', 'application/json');
xhr.setRequestHeader('X-Requested-With', 'XMLHttpRequest');
// Un caricamento fermo da cinque minuti e' fermo, non lento: senza
// questo non scatterebbe piu' alcun evento e la pagina resterebbe
// in attesa per sempre.
xhr.timeout = 300000;
xhr.upload.addEventListener('progress', (e) => {
if (e.lengthComputable) {
this.uploadPercento = Math.round((e.loaded / e.total) * 100);
}
});
xhr.addEventListener('load', () => {
this.uploadInCorso = false;
let payload = {};
try {
payload = JSON.parse(xhr.responseText || '{}');
} catch (e) {
payload = {};
}
if (xhr.status >= 200 && xhr.status < 300 && payload.operation_id) {
this.uploadPercento = 100;
this.zipSelezionato = '';
form.reset();
// Da qui in poi l'avanzamento arriva dal job, non dall'upload.
this.$wire.seguiOperazione(payload.operation_id);
return; return;
} }
if (!confirm('Hai verificato il backup scaricato? Confermando, i dati vengono cancellati definitivamente.')) { // La barra torna a zero: lasciarla dov'era farebbe credere che
return; // il ripristino sia partito.
}
const password = this.passwordAzzeramento;
// Svuotato subito: se la password è sbagliata si ridigita, e nel
// frattempo non resta nel campo di una scheda lasciata aperta.
this.passwordAzzeramento = '';
// this.$wire e non $wire: dentro un metodo di x-data la forma
// esplicita e' l'unica su cui si possa contare.
this.$wire.confermaAzzeramento(password);
},
/**
* Il file e' troppo grande? Si scopre qui, prima di caricare.
*
* Superato il tetto, e' nginx a chiudere la connessione a meta'
* trasferimento: la barra si pianta a una percentuale qualsiasi e non
* arriva nessun evento utile, quindi la pagina resta muta con l'aria di
* essersi bloccata. Controllare la dimensione prima di partire rende
* quel caso un messaggio immediato invece di un'attesa senza fine.
*/
zipTroppoGrande(file) {
const maxByte = {{ \App\Http\Controllers\Settings\BackupController::maxUploadKb() }} * 1000;
if (!file || file.size <= maxByte) {
return false;
}
const mb = (n) => (n / 1000000).toFixed(0);
this.erroreUpload = `Il pacchetto pesa ${mb(file.size)} MB e supera il limite di ${mb(maxByte)} MB accettato dal server. `
+ `Per archivi piu' grandi usa il ripristino dai volumi descritto nel README, oppure chiedi di alzare il limite.`;
return true;
},
inviaRipristino(event) {
const form = event.target;
const file = form.querySelector('input[name=backupZip]').files[0];
if (this.zipTroppoGrande(file)) {
return;
}
const formData = new FormData(form);
this.erroreUpload = '';
this.uploadInCorso = true;
this.uploadPercento = 0; this.uploadPercento = 0;
this.erroreUpload = this.messaggioErrore(xhr.status, payload);
});
const xhr = new XMLHttpRequest(); xhr.addEventListener('error', () => {
xhr.open('POST', form.action); this.uploadInCorso = false;
xhr.setRequestHeader('Accept', 'application/json'); this.uploadPercento = 0;
xhr.setRequestHeader('X-Requested-With', 'XMLHttpRequest'); this.erroreUpload = `Errore di rete durante il caricamento: connessione interrotta dal `
+ `server. Succede tipicamente quando il pacchetto supera il limite accettato.`;
});
// Un caricamento che non si muove per cinque minuti e' fermo, non xhr.addEventListener('timeout', () => {
// lento: senza questo non scatterebbe piu' nessun evento e la this.uploadInCorso = false;
// pagina resterebbe in attesa per sempre. this.uploadPercento = 0;
xhr.timeout = 300000; this.erroreUpload = `Il caricamento non ha risposto entro 5 minuti ed è stato interrotto.`;
});
xhr.upload.addEventListener('progress', (e) => { xhr.addEventListener('abort', () => {
if (e.lengthComputable) { this.uploadInCorso = false;
this.uploadPercento = Math.round((e.loaded / e.total) * 100); this.uploadPercento = 0;
} this.erroreUpload = `Caricamento annullato.`;
}); });
xhr.addEventListener('load', () => { xhr.send(new FormData(form));
this.uploadInCorso = false; },
let payload = {};
try { payload = JSON.parse(xhr.responseText || '{}'); } catch (e) { payload = {}; }
if (xhr.status >= 200 && xhr.status < 300 && payload.operation_id) { /**
this.uploadPercento = 100; * 413 e 419 non hanno un corpo JSON: senza il codice HTTP nel messaggio
this.zipSelezionato = ''; * resterebbe un generico errore che non dice cosa fare.
form.reset(); */
// Da qui in poi l'avanzamento arriva dal job, non dall'upload. messaggioErrore(status, payload) {
this.$wire.seguiOperazione(payload.operation_id); if (payload.message) {
return; return payload.message;
} }
// Qualsiasi esito diverso: la barra torna a zero, perche' if (payload.errors && payload.errors.backupZip) {
// lasciarla dov'era farebbe credere che il ripristino sia return payload.errors.backupZip[0];
// partito. Il codice HTTP entra nel messaggio: 413 e 419 non }
// producono un corpo JSON, quindi senza di esso resterebbe solo
// un generico "errore" che non dice cosa fare.
this.uploadPercento = 0;
this.erroreUpload = this.messaggioErrore(xhr.status, payload);
});
xhr.addEventListener('error', () => { if (status === 413) {
this.uploadInCorso = false; return `Il pacchetto è troppo grande per il server (413). Alza MAX_UPLOAD_MB nel .env e rifai il deploy, oppure usa il ripristino dai volumi.`;
this.uploadPercento = 0; }
this.erroreUpload = 'Errore di rete durante il caricamento: connessione interrotta dal server. '
+ 'Succede tipicamente quando il pacchetto supera il limite accettato.';
});
xhr.addEventListener('timeout', () => { if (status === 419) {
this.uploadInCorso = false; return `Sessione scaduta durante il caricamento. Ricarica la pagina e riprova.`;
this.uploadPercento = 0; }
this.erroreUpload = 'Il caricamento non ha risposto entro 5 minuti ed e\\' stato interrotto.';
});
xhr.addEventListener('abort', () => { if (status === 0) {
this.uploadInCorso = false; return `Il server ha chiuso la connessione durante il caricamento, senza rispondere. Di solito significa che il pacchetto supera il limite accettato.`;
this.uploadPercento = 0; }
this.erroreUpload = 'Caricamento annullato.';
});
xhr.send(formData); return `Errore durante il caricamento del pacchetto (HTTP ${status}).`;
}, },
messaggioErrore(status, payload) {
if (payload.message) {
return payload.message;
}
if (payload.errors && payload.errors.backupZip) {
return payload.errors.backupZip[0];
}
if (status === 413) {
return 'Il pacchetto e\\' troppo grande per il server (413). Alza client_max_body_size in nginx e post_max_size in PHP, oppure usa il ripristino dai volumi.';
}
if (status === 419) {
return 'Sessione scaduta durante il caricamento. Ricarica la pagina e riprova.';
}
if (status === 0) {
return 'Il server ha chiuso la connessione durante il caricamento, senza rispondere. Di solito significa che il pacchetto supera il limite accettato.';
}
return `Errore durante il caricamento del pacchetto (HTTP ${status}).`;
},
}" }"
x-on:scarica-backup.window="window.location.href = $event.detail.url"> x-on:scarica-backup.window="window.location.href = $event.detail.url">
@@ -341,13 +370,7 @@
subito che il file e' fuori limite evita di aspettare un subito che il file e' fuori limite evita di aspettare un
caricamento destinato a essere tagliato. --}} caricamento destinato a essere tagliato. --}}
<input name="backupZip" type="file" accept=".zip,application/zip" <input name="backupZip" type="file" accept=".zip,application/zip"
x-on:change=" x-on:change="fileScelto($event)"
erroreUpload = '';
uploadPercento = 0;
const f = $event.target.files[0];
zipSelezionato = f ? f.name + ' (' + (f.size / 1000000).toFixed(1) + ' MB)' : '';
if (f) zipTroppoGrande(f);
"
class="block w-full text-sm text-gray-600 file:mr-3 file:py-2 file:px-4 file:rounded-lg file:border-0 file:text-sm file:font-medium file:bg-red-50 file:text-red-700 hover:file:bg-red-100"> class="block w-full text-sm text-gray-600 file:mr-3 file:py-2 file:px-4 file:rounded-lg file:border-0 file:text-sm file:font-medium file:bg-red-50 file:text-red-700 hover:file:bg-red-100">
<p class="mt-1 text-xs text-gray-500"> <p class="mt-1 text-xs text-gray-500">
Dimensione massima accettata: {{ \App\Http\Controllers\Settings\BackupController::maxUploadMb() }} MB. Dimensione massima accettata: {{ \App\Http\Controllers\Settings\BackupController::maxUploadMb() }} MB.