From b76284d8b7c4a38a652a3f78ba7dd6248f253f97 Mon Sep 17 00:00:00 2001 From: GuavaPopper Date: Thu, 4 Jun 2026 14:53:36 +0700 Subject: [PATCH] feat: add database restore from uploaded SQL file with RESTORE confirmation Co-Authored-By: Claude Sonnet 4.6 --- app/Http/Controllers/BackupController.php | 31 ++++++++++ resources/views/admin/users.blade.php | 72 +++++++++++++++++++++++ routes/web.php | 1 + 3 files changed, 104 insertions(+) diff --git a/app/Http/Controllers/BackupController.php b/app/Http/Controllers/BackupController.php index 6fcb6b2..51d23a5 100644 --- a/app/Http/Controllers/BackupController.php +++ b/app/Http/Controllers/BackupController.php @@ -63,4 +63,35 @@ class BackupController extends Controller if (!file_exists($path)) abort(404); return response()->download($path); } + + public function restore(\Illuminate\Http\Request $request) + { + $request->validate([ + 'file' => 'required|file|mimes:sql,txt|max:51200', + 'confirm' => 'required|in:RESTORE', + ]); + + $db = config('database.connections.mysql'); + $tmpPath = $request->file('file')->getRealPath(); + + $process = new Process([ + 'mysql', + '-h', $db['host'], + '-P', (string) $db['port'], + '-u', $db['username'], + $db['database'], + ], null, ['MYSQL_PWD' => $db['password']]); + + $process->setInput(file_get_contents($tmpPath)); + $process->setTimeout(300); + $process->run(); + + if (!$process->isSuccessful()) { + ActivityLog::record('restore', 'Restore database GAGAL: ' . $process->getErrorOutput()); + return response()->json(['message' => 'Restore gagal: ' . $process->getErrorOutput()], 500); + } + + ActivityLog::record('restore', 'Restore database berhasil dari file yang diunggah'); + return response()->json(['success' => true]); + } } diff --git a/resources/views/admin/users.blade.php b/resources/views/admin/users.blade.php index b8df32e..0d3f917 100644 --- a/resources/views/admin/users.blade.php +++ b/resources/views/admin/users.blade.php @@ -100,6 +100,9 @@ Backup Database + @@ -498,6 +501,75 @@ document.getElementById('toastBox').appendChild(t); setTimeout(() => t.remove(), 3000); } + + // ── RESTORE ───────────────────────────────────────────────── + function updateRestoreBtn() { + document.getElementById('btnRestoreSubmit').disabled = + document.getElementById('restore_confirm').value !== 'RESTORE'; + } + + async function submitRestore() { + const fileInput = document.getElementById('restore_file'); + if (!fileInput.files.length) { showToast('Pilih file .sql terlebih dahulu', 'error'); return; } + + const fd = new FormData(); + fd.append('file', fileInput.files[0]); + fd.append('confirm', document.getElementById('restore_confirm').value); + + const btn = document.getElementById('btnRestoreSubmit'); + btn.disabled = true; + btn.innerHTML = ' Memproses...'; + + const res = await fetch('{{ url('/api/admin/restore') }}', { + method: 'POST', + headers: { 'X-CSRF-TOKEN': csrfToken }, + body: fd, + }); + const json = await res.json(); + + document.getElementById('modal_restore').close(); + document.getElementById('restore_confirm').value = ''; + document.getElementById('restore_file').value = ''; + btn.disabled = true; + btn.innerHTML = ' Restore Sekarang'; + + if (json.success) { + showToast('Restore berhasil. Halaman akan dimuat ulang...', 'success'); + setTimeout(() => location.reload(), 2000); + } else { + showToast(json.message || 'Restore gagal', 'error'); + } + } + + + + + + diff --git a/routes/web.php b/routes/web.php index dff24f0..5aabcbb 100644 --- a/routes/web.php +++ b/routes/web.php @@ -85,6 +85,7 @@ Route::middleware('auth')->group(function () { Route::get('/backup/list', [BackupController::class, 'listBackups']); Route::get('/backup/download/{filename}', [BackupController::class, 'downloadStored']) ->where('filename', '[^/]+'); + Route::post('/restore', [BackupController::class, 'restore']); Route::get('/logs', [ActivityLogController::class, 'index']); Route::get('/stats', [ActivityLogController::class, 'stats']);