stato->get($operationId); abort_if($operazione === null, 404, 'Backup non piu\' disponibile: rigeneralo.'); $percorso = CreateBackupArchive::percorso($operationId); abort_unless(is_file($percorso), 404, 'Archivio di backup non trovato: rigeneralo.'); $nome = $operazione['result']['nome_download'] ?? 'termanager2-backup.zip'; $this->stato->update($operationId, function (array $state) { $state['result']['servito_at'] = now()->toDateTimeString(); return $state; }); return response() ->download($percorso, $nome, [ 'Content-Type' => 'application/zip', 'Content-Length' => (string) filesize($percorso), ]); } /** * Riceve il pacchetto da ripristinare e mette in coda il lavoro. * * Risponde JSON perche' il form viene inviato via XMLHttpRequest: e' l'unico * modo di avere una barra di caricamento reale sull'upload, che per un * archivio di centinaia di MB e' la parte piu' lunga dell'operazione. */ public function upload(Request $request) { $request->validate([ 'backupZip' => ['required', 'file', 'mimes:zip', 'max:' . self::maxUploadKb()], ], [ 'backupZip.required' => 'Seleziona il pacchetto di backup da ripristinare.', 'backupZip.mimes' => 'Il pacchetto di backup deve essere un file .zip.', 'backupZip.max' => 'Il pacchetto supera i ' . self::maxUploadMb() . ' MB accettati dal server.', ]); $operationId = (string) Str::uuid(); // Stesso blocco del componente Livewire, ripetuto qui perche' questa e' // una porta d'ingresso separata: un ripristino avviato mentre gira un // azzeramento riscriverebbe le tabelle che l'altro sta cancellando. // Il posto si prende subito e in un passo solo (occupa()): controllarlo // e prenderlo solo dopo aver salvato il file lasciava una finestra in cui // un import o un secondo ripristino passavano entrambi. $inCorso = $this->stato->occupa($operationId, BackupOperationState::TIPO_RESTORE, 'In coda'); if ($inCorso !== null) { return response()->json([ 'message' => $this->stato->messaggioOccupato($inCorso), ], 409); } try { // Il file dell'upload vive in una cartella temporanea che sparisce a // fine richiesta: il job lo troverebbe gia' cancellato. $percorso = $request->file('backupZip')->storeAs( 'backup-restore-in', $operationId . '.zip', 'local' ); } catch (\Throwable $e) { // Il posto e' gia' preso: senza chiuderlo resterebbe occupato da un // ripristino che non partira' mai, per l'ora intera del TTL. $this->stato->markFailed($operationId, 'Archivio non salvato sul server: ' . $e->getMessage()); throw $e; } if ($percorso === false) { $this->stato->markFailed($operationId, 'Archivio non salvato sul server.'); return response()->json([ 'message' => 'Il server non è riuscito a salvare il pacchetto ricevuto (spazio o permessi sul disco).', ], 500); } $this->stato->log($operationId, 'Archivio ricevuto. In attesa del worker di coda.'); RestoreBackupArchive::dispatch( $operationId, storage_path('app/' . $percorso), (int) auth()->id() ); return response()->json(['operation_id' => $operationId]); } }