system-locked violation types (alpha & terlambat) + proporsional poin alpha per jam
- 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
This commit is contained in:
@@ -0,0 +1,194 @@
|
||||
<?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' : '—';
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user