feat: add scheduled daily backup, stored backup listing and download

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
GuavaPopper
2026-06-04 14:51:07 +07:00
parent 1535c75138
commit f77fc725a8
6 changed files with 146 additions and 2 deletions
@@ -0,0 +1,70 @@
<?php
namespace App\Console\Commands;
use App\Models\ActivityLog;
use Illuminate\Console\Command;
use Symfony\Component\Process\Process;
class BackupDatabaseCommand extends Command
{
protected $signature = 'backup:database';
protected $description = 'Backup database ke storage/app/backups/';
public function handle(): int
{
$db = config('database.connections.mysql');
$dir = storage_path('app/backups');
if (!is_dir($dir)) {
mkdir($dir, 0755, true);
}
$filename = 'backup-webgis-' . now()->format('Ymd-His') . '.sql';
$path = $dir . '/' . $filename;
$process = new Process([
'mysqldump',
'-h', $db['host'],
'-P', (string) $db['port'],
'-u', $db['username'],
'--single-transaction',
'--skip-lock-tables',
'--no-tablespaces',
'--result-file=' . $path,
$db['database'],
], null, ['MYSQL_PWD' => $db['password']]);
$process->setTimeout(120);
$process->run();
if (!$process->isSuccessful()) {
$this->error('Backup gagal: ' . $process->getErrorOutput());
ActivityLog::create([
'user_id' => null,
'user_name' => 'Sistem',
'action' => 'backup',
'description' => 'Backup terjadwal GAGAL: ' . $process->getErrorOutput(),
'ip_address' => '127.0.0.1',
]);
return self::FAILURE;
}
// Prune: hapus backup lebih dari 7 file terlama
$files = glob($dir . '/backup-webgis-*.sql');
usort($files, fn($a, $b) => filemtime($b) - filemtime($a));
foreach (array_slice($files, 7) as $old) {
unlink($old);
}
$this->info("Backup disimpan: {$filename}");
ActivityLog::create([
'user_id' => null,
'user_name' => 'Sistem',
'action' => 'backup',
'description' => "Backup terjadwal berhasil: {$filename}",
'ip_address' => '127.0.0.1',
]);
return self::SUCCESS;
}
}
+25
View File
@@ -38,4 +38,29 @@ class BackupController extends Controller
'Content-Disposition' => "attachment; filename=\"{$filename}\"",
]);
}
public function listBackups()
{
$dir = storage_path('app/backups');
$files = is_dir($dir) ? glob($dir . '/backup-webgis-*.sql') : [];
usort($files, fn($a, $b) => filemtime($b) - filemtime($a));
$list = array_map(fn($f) => [
'name' => basename($f),
'size_kb' => round(filesize($f) / 1024, 1),
'created_at' => date('d/m/Y H:i', filemtime($f)),
], $files);
return response()->json($list);
}
public function downloadStored(string $filename)
{
if (!preg_match('/^backup-webgis-[\d-]+\.sql$/', $filename)) {
abort(400, 'Nama file tidak valid');
}
$path = storage_path('app/backups/' . $filename);
if (!file_exists($path)) abort(404);
return response()->download($path);
}
}