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:
@@ -61,15 +61,124 @@
|
||||
zipInCorso: false,
|
||||
zipPercento: 0,
|
||||
|
||||
{{-- La cartella viaggia a lotti: vedi XmlExchange::getFilePerLottoProperty().
|
||||
Il tetto in byte tiene ogni richiesta lontana dai 100 MB
|
||||
che Cloudflare accetta per un singolo caricamento. --}}
|
||||
fileCartella: [],
|
||||
cartellaInCorso: false,
|
||||
caricatiCartella: 0,
|
||||
filePerLotto: {{ $this->filePerLotto }},
|
||||
byteMassimiLotto: 40 * 1000 * 1000,
|
||||
|
||||
log(riga, livello) {
|
||||
this.$dispatch('console-riga', { testo: riga, livello: livello || 'info', sorgente: 'import-pdf' });
|
||||
},
|
||||
|
||||
/*
|
||||
* Stessa regola di TerritorioPdfImportDispatcher::daIgnorare():
|
||||
* Thumbs.db, desktop.ini, .DS_Store e gli altri file di sistema
|
||||
* o nascosti non partono nemmeno. Il server li scarta comunque,
|
||||
* ma caricarli sarebbe solo traffico.
|
||||
*/
|
||||
daIgnorare(file) {
|
||||
const segmenti = (file.webkitRelativePath || file.name).split('/').filter((s) => s !== '');
|
||||
if (segmenti.some((s) => s.startsWith('.') || s.toUpperCase() === '__MACOSX')) {
|
||||
return true;
|
||||
}
|
||||
const nome = segmenti[segmenti.length - 1] || '';
|
||||
return nome.startsWith('~$') || !nome.toLowerCase().endsWith('.pdf');
|
||||
},
|
||||
|
||||
cartellaScelta(event) {
|
||||
this.fileScelti = event.target.files.length;
|
||||
if (this.fileScelti > 0) {
|
||||
this.log(`Cartella scelta: ${this.fileScelti} file.`);
|
||||
this.log(`In attesa del caricamento temporaneo di Livewire...`, 'attesa');
|
||||
const tutti = Array.from(event.target.files);
|
||||
this.fileCartella = tutti.filter((file) => !this.daIgnorare(file));
|
||||
this.fileScelti = this.fileCartella.length;
|
||||
this.caricatiCartella = 0;
|
||||
const ignorati = tutti.length - this.fileCartella.length;
|
||||
if (tutti.length > 0) {
|
||||
this.log(`Cartella scelta: ${tutti.length} file, ${this.fileScelti} PDF da importare`
|
||||
+ (ignorati > 0 ? ` (${ignorati} ignorati: file di sistema, nascosti o non PDF).` : `.`));
|
||||
}
|
||||
},
|
||||
|
||||
lotti() {
|
||||
const lotti = [];
|
||||
let lotto = [];
|
||||
let byte = 0;
|
||||
for (const file of this.fileCartella) {
|
||||
if (lotto.length > 0 && (lotto.length >= this.filePerLotto || byte + file.size > this.byteMassimiLotto)) {
|
||||
lotti.push(lotto);
|
||||
lotto = [];
|
||||
byte = 0;
|
||||
}
|
||||
lotto.push(file);
|
||||
byte += file.size;
|
||||
}
|
||||
if (lotto.length > 0) {
|
||||
lotti.push(lotto);
|
||||
}
|
||||
return lotti;
|
||||
},
|
||||
|
||||
/*
|
||||
* Un lotto alla volta, aggiunto (append) a quelli gia' arrivati.
|
||||
* La guardia esiste perche' Livewire non ascolta gli errori di
|
||||
* rete del caricamento: una connessione caduta lascerebbe la
|
||||
* promessa appesa per sempre, con la pagina muta.
|
||||
*/
|
||||
caricaLotto(lotto) {
|
||||
return new Promise((ok, ko) => {
|
||||
let ultimoSegno = Date.now();
|
||||
const guardia = setInterval(() => {
|
||||
if (Date.now() - ultimoSegno > 120000) {
|
||||
clearInterval(guardia);
|
||||
this.$wire.cancelUpload('pdfFolder');
|
||||
ko(new Error(`nessun avanzamento da due minuti`));
|
||||
}
|
||||
}, 5000);
|
||||
this.$wire.uploadMultiple('pdfFolder', lotto,
|
||||
() => { clearInterval(guardia); ok(); },
|
||||
() => { clearInterval(guardia); ko(new Error(`il server ha rifiutato il lotto`)); },
|
||||
() => { ultimoSegno = Date.now(); },
|
||||
() => { clearInterval(guardia); ko(new Error(`caricamento annullato`)); },
|
||||
true);
|
||||
});
|
||||
},
|
||||
|
||||
async inviaCartella() {
|
||||
if (this.fileCartella.length === 0 || this.cartellaInCorso) {
|
||||
return;
|
||||
}
|
||||
this.$dispatch('console-apri');
|
||||
|
||||
if (!(await this.$wire.cartellaPuoPartire())) {
|
||||
this.log(`Import non avviato: c'è già un'operazione in corso.`, 'errore');
|
||||
return;
|
||||
}
|
||||
|
||||
this.cartellaInCorso = true;
|
||||
this.caricatiCartella = 0;
|
||||
try {
|
||||
await this.$wire.svuotaCartella();
|
||||
const lotti = this.lotti();
|
||||
this.log(`Caricamento di ${this.fileCartella.length} PDF in ${lotti.length} lotti...`);
|
||||
for (let i = 0; i < lotti.length; i++) {
|
||||
await this.caricaLotto(lotti[i]);
|
||||
this.caricatiCartella += lotti[i].length;
|
||||
this.log(`Lotto ${i + 1}/${lotti.length} caricato: ${this.caricatiCartella}/${this.fileCartella.length} file.`);
|
||||
}
|
||||
this.log(`Cartella caricata. Preparazione import sul server...`, 'ok');
|
||||
if (await this.$wire.importTerritoryPdfFolder()) {
|
||||
this.fileCartella = [];
|
||||
this.fileScelti = 0;
|
||||
this.$refs.cartella.value = '';
|
||||
} else {
|
||||
this.log(`Import non avviato: il motivo è nell'avviso a comparsa. La cartella resta scelta, puoi ripremere.`, 'errore');
|
||||
}
|
||||
} catch (errore) {
|
||||
this.log(`ERRORE durante il caricamento della cartella: ${errore.message}. Nessun import è partito: riprova.`, 'errore');
|
||||
} finally {
|
||||
this.cartellaInCorso = false;
|
||||
}
|
||||
},
|
||||
|
||||
@@ -139,17 +248,15 @@
|
||||
|
||||
xhr.send(new FormData(form));
|
||||
},
|
||||
}"
|
||||
x-on:livewire-upload-start="if ($event.detail.id === 'pdfFolder') { $dispatch('console-apri'); log('Caricamento della cartella avviato...'); }"
|
||||
x-on:livewire-upload-progress="if ($event.detail.id === 'pdfFolder') { log('Caricamento cartella: ' + $event.detail.progress + '%'); }"
|
||||
x-on:livewire-upload-finish="if ($event.detail.id === 'pdfFolder') { log('Cartella caricata. Preparazione import sul server...', 'ok'); }"
|
||||
x-on:livewire-upload-error="if ($event.detail.id === 'pdfFolder') { log('ERRORE durante il caricamento temporaneo dei file.', 'errore'); }">
|
||||
}">
|
||||
|
||||
<div>
|
||||
<h2 id="titolo-pdf" class="text-lg font-semibold text-gray-900">3 · Import PDF territori</h2>
|
||||
<p class="text-xs text-gray-500 mt-1">
|
||||
Associa i PDF ai territori già presenti e ne genera le miniature. Il nome del file può
|
||||
variare: basta che contenga il numero di un territorio esistente.
|
||||
Associa i PDF ai territori già presenti e ne genera le miniature. Il nome del file deve
|
||||
contenere il numero del territorio: una lettera subito dopo il numero ne fa parte («12 B»,
|
||||
«12B» e «12-b» sono tutti il 12B, mai il 12). I file che non si abbinano con
|
||||
certezza a un solo territorio vengono saltati e segnalati nell’esito.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -186,30 +293,32 @@
|
||||
</form>
|
||||
|
||||
{{-- Strada alternativa --}}
|
||||
<form wire:submit.prevent="importTerritoryPdfFolder" class="rounded-lg border border-gray-200 p-4 space-y-3">
|
||||
{{-- Niente wire:model sull'input: con wire:model Livewire spedisce
|
||||
tutta la cartella in una sola richiesta, e PHP ne tiene solo i
|
||||
primi max_file_uploads (20). La spedisce inviaCartella(), a lotti. --}}
|
||||
<form @submit.prevent="inviaCartella()" class="rounded-lg border border-gray-200 p-4 space-y-3">
|
||||
<div>
|
||||
<h3 class="text-sm font-semibold text-gray-900">Da cartella</h3>
|
||||
<p class="mt-1 text-xs text-gray-500">Comoda per pochi file. Ogni PDF viaggia come caricamento temporaneo a sé.</p>
|
||||
<p class="mt-1 text-xs text-gray-500">I PDF partono a gruppi di {{ $this->filePerLotto }}; i file che non sono PDF (Thumbs.db, desktop.ini…) vengono ignorati.</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label for="pdfFolder" class="sr-only">Cartella di PDF</label>
|
||||
<input wire:model="pdfFolder" id="pdfFolder" type="file" multiple webkitdirectory directory
|
||||
accept=".pdf,application/pdf"
|
||||
<input id="pdfFolder" type="file" multiple webkitdirectory directory
|
||||
x-ref="cartella"
|
||||
data-file-per-lotto="{{ $this->filePerLotto }}"
|
||||
x-on:change="cartellaScelta($event)"
|
||||
x-bind:disabled="cartellaInCorso"
|
||||
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-gray-100 file:text-gray-700 hover:file:bg-gray-200">
|
||||
@error('pdfFolder') <p class="text-red-600 text-xs mt-1" role="alert">{{ $message }}</p> @enderror
|
||||
@error('pdfFolder.*') <p class="text-red-600 text-xs mt-1" role="alert">{{ $message }}</p> @enderror
|
||||
</div>
|
||||
|
||||
<p class="text-xs text-gray-500" x-show="fileScelti > 0" x-cloak x-text="fileScelti + ' file selezionati'"></p>
|
||||
<p class="text-xs text-gray-500" x-show="fileScelti > 0" x-cloak
|
||||
x-text="cartellaInCorso ? caricatiCartella + ' / ' + fileScelti + ' PDF caricati' : fileScelti + ' PDF da importare'"></p>
|
||||
|
||||
<x-btn variante="secondary" type="submit"
|
||||
x-on:click="$dispatch('console-apri')"
|
||||
wire:loading.attr="disabled"
|
||||
{{-- Vedi la nota in components/btn.blade.php: dentro un
|
||||
tag di componente serve :disabled, non @disabled. --}}
|
||||
:disabled="empty($pdfFolder)">
|
||||
x-bind:disabled="fileScelti === 0 || cartellaInCorso">
|
||||
Importa cartella
|
||||
</x-btn>
|
||||
<span wire:loading wire:target="importTerritoryPdfFolder" class="text-xs text-indigo-700">Preparazione import...</span>
|
||||
@@ -329,7 +438,8 @@
|
||||
<h3 class="text-sm font-semibold text-gray-900">Importa</h3>
|
||||
<p class="text-xs text-gray-500">
|
||||
<strong>Sostituisce</strong> zone, tipologie, proclamatori, territori, anni, campagne,
|
||||
assegnazioni e impostazioni.
|
||||
assegnazioni e impostazioni. I PDF e le miniature dei territori sostituiti che il file non
|
||||
richiama più vengono eliminati.
|
||||
</p>
|
||||
|
||||
<form action="{{ route('xml.import-xml') }}" method="POST" enctype="multipart/form-data" class="space-y-3"
|
||||
|
||||
Reference in New Issue
Block a user