- Migration: add is_system column to violation_types, mark alpha & terlambat - DestroyType guard: system types cannot be deleted - UpdateType guard: slug tetap walau nama diubah untuk system types - UI: delete button hidden, badge lock untuk system types - Seeder: ViolationTypeDefaultsSeeder untuk fresh deploy - AttendanceController: lookup by slug (alpha/terlambat) instead of fuzzy match - Alpha auto-violation: poin proporsional per jam (round((points/10)*count), min 1) - Terlambat auto-violation: poin fix sesuai jenis (2 poin) - Also handles both A (alpha) and T (terlambat) statuses for auto-violations
195 lines
6.8 KiB
PHP
195 lines
6.8 KiB
PHP
<?php
|
|
|
|
namespace App\Http\Controllers\Admin;
|
|
|
|
use App\Http\Controllers\Controller;
|
|
use Illuminate\Http\RedirectResponse;
|
|
use Illuminate\Http\Request;
|
|
use Illuminate\Support\Facades\Storage;
|
|
use Illuminate\View\View;
|
|
|
|
class BackupController extends Controller
|
|
{
|
|
protected string $backupDir = 'backups';
|
|
|
|
public function index(): View
|
|
{
|
|
abort_unless(auth()->user()?->role === 'admin', 403, 'Hanya admin yang dapat mengakses fitur backup.');
|
|
|
|
$backups = collect();
|
|
|
|
if (Storage::disk('local')->exists($this->backupDir)) {
|
|
$files = Storage::disk('local')->files($this->backupDir);
|
|
|
|
$backups = collect($files)
|
|
->filter(fn($f) => str_ends_with($f, '.sql.gz'))
|
|
->map(function ($path) {
|
|
$fullPath = Storage::disk('local')->path($path);
|
|
return (object) [
|
|
'filename' => basename($path),
|
|
'path' => $path,
|
|
'size' => $this->formatBytes(filesize($fullPath)),
|
|
'size_bytes' => filesize($fullPath),
|
|
'date' => date('Y-m-d H:i:s', filemtime($fullPath)),
|
|
'timestamp' => filemtime($fullPath),
|
|
];
|
|
})
|
|
->sortByDesc('timestamp')
|
|
->values();
|
|
}
|
|
|
|
$dbSize = $this->getDatabaseSize();
|
|
$stats = [
|
|
'total_backups' => $backups->count(),
|
|
'latest_backup' => $backups->first()?->date ?? 'Belum pernah',
|
|
'total_size' => $this->formatBytes($backups->sum('size_bytes')),
|
|
'db_size' => $dbSize,
|
|
];
|
|
|
|
return view('settings.backup', compact('backups', 'stats'));
|
|
}
|
|
|
|
public function create(): RedirectResponse
|
|
{
|
|
abort_unless(auth()->user()?->role === 'admin', 403);
|
|
|
|
$db = config('database.connections.mysql');
|
|
$filename = 'aplikasi_ketertiban_' . now()->format('Y-m-d_His') . '.sql.gz';
|
|
$outputPath = Storage::disk('local')->path($this->backupDir . '/' . $filename);
|
|
|
|
if (!is_dir(dirname($outputPath))) {
|
|
mkdir(dirname($outputPath), 0755, true);
|
|
}
|
|
|
|
$command = sprintf(
|
|
'mysqldump --host=%s --port=%s --user=%s --password=%s %s --routines --events --single-transaction --quick | gzip > %s 2>&1',
|
|
escapeshellarg($db['host']),
|
|
escapeshellarg($db['port']),
|
|
escapeshellarg($db['username']),
|
|
escapeshellarg($db['password']),
|
|
escapeshellarg($db['database']),
|
|
escapeshellarg($outputPath)
|
|
);
|
|
|
|
$output = null;
|
|
$exitCode = null;
|
|
exec($command, $output, $exitCode);
|
|
|
|
if ($exitCode !== 0 || !file_exists($outputPath)) {
|
|
return redirect()->route('settings.backup')
|
|
->with('error', 'Backup gagal: ' . implode("\n", $output));
|
|
}
|
|
|
|
return redirect()->route('settings.backup')
|
|
->with('success', 'Backup berhasil: ' . $filename);
|
|
}
|
|
|
|
public function download(string $filename)
|
|
{
|
|
abort_unless(auth()->user()?->role === 'admin', 403);
|
|
|
|
$path = $this->backupDir . '/' . basename($filename);
|
|
|
|
if (!Storage::disk('local')->exists($path)) {
|
|
abort(404, 'File backup tidak ditemukan.');
|
|
}
|
|
|
|
return Storage::disk('local')->download($path, $filename);
|
|
}
|
|
|
|
public function destroy(string $filename)
|
|
{
|
|
abort_unless(auth()->user()?->role === 'admin', 403);
|
|
|
|
$path = $this->backupDir . '/' . basename($filename);
|
|
|
|
if (!Storage::disk('local')->exists($path)) {
|
|
return redirect()->route('settings.backup')
|
|
->with('error', 'File backup tidak ditemukan.');
|
|
}
|
|
|
|
Storage::disk('local')->delete($path);
|
|
|
|
return redirect()->route('settings.backup')
|
|
->with('success', 'Backup ' . $filename . ' berhasil dihapus.');
|
|
}
|
|
|
|
public function restore(Request $request): RedirectResponse
|
|
{
|
|
abort_unless(auth()->user()?->role === 'admin', 403);
|
|
|
|
$filename = basename($request->input('filename'));
|
|
$path = $this->backupDir . '/' . $filename;
|
|
|
|
if (!Storage::disk('local')->exists($path)) {
|
|
return redirect()->route('settings.backup')
|
|
->with('error', 'File backup tidak ditemukan.');
|
|
}
|
|
|
|
$fullPath = Storage::disk('local')->path($path);
|
|
$db = config('database.connections.mysql');
|
|
|
|
// Backup otomatis sebelum restore
|
|
$autoBackup = 'pre_restore_' . now()->format('Y-m-d_His') . '.sql.gz';
|
|
$autoPath = Storage::disk('local')->path($this->backupDir . '/' . $autoBackup);
|
|
exec(sprintf(
|
|
'mysqldump --host=%s --port=%s --user=%s --password=%s %s --routines --events --single-transaction --quick | gzip > %s',
|
|
escapeshellarg($db['host']),
|
|
escapeshellarg($db['port']),
|
|
escapeshellarg($db['username']),
|
|
escapeshellarg($db['password']),
|
|
escapeshellarg($db['database']),
|
|
escapeshellarg($autoPath)
|
|
));
|
|
|
|
// Restore
|
|
$command = sprintf(
|
|
'gunzip -c %s | mysql --host=%s --port=%s --user=%s --password=%s %s 2>&1',
|
|
escapeshellarg($fullPath),
|
|
escapeshellarg($db['host']),
|
|
escapeshellarg($db['port']),
|
|
escapeshellarg($db['username']),
|
|
escapeshellarg($db['password']),
|
|
escapeshellarg($db['database'])
|
|
);
|
|
|
|
$output = null;
|
|
$exitCode = null;
|
|
exec($command, $output, $exitCode);
|
|
|
|
if ($exitCode !== 0) {
|
|
return redirect()->route('settings.backup')
|
|
->with('error', 'Restore gagal: ' . implode("\n", $output));
|
|
}
|
|
|
|
return redirect()->route('settings.backup')
|
|
->with('success', 'Database berhasil direstore dari ' . $filename . '. Backup otomatis sebelum restore: ' . $autoBackup);
|
|
}
|
|
|
|
private function formatBytes(int $bytes): string
|
|
{
|
|
$units = ['B', 'KB', 'MB', 'GB'];
|
|
$i = 0;
|
|
while ($bytes >= 1024 && $i < count($units) - 1) {
|
|
$bytes /= 1024;
|
|
$i++;
|
|
}
|
|
return round($bytes, 2) . ' ' . $units[$i];
|
|
}
|
|
|
|
private function getDatabaseSize(): string
|
|
{
|
|
$db = config('database.connections.mysql');
|
|
$command = sprintf(
|
|
"mysql --host=%s --port=%s --user=%s --password=%s -e \"SELECT ROUND(SUM(data_length + index_length) / 1024 / 1024, 2) AS size_mb FROM information_schema.tables WHERE table_schema = '%s'\" -sN 2>/dev/null",
|
|
escapeshellarg($db['host']),
|
|
escapeshellarg($db['port']),
|
|
escapeshellarg($db['username']),
|
|
escapeshellarg($db['password']),
|
|
escapeshellarg($db['database'])
|
|
);
|
|
$size = trim(shell_exec($command));
|
|
return $size ? $size . ' MB' : '—';
|
|
}
|
|
}
|