Backup Restore - luckydeva03/barbershop_app GitHub Wiki
đž Backup & Restore
Panduan komprehensif untuk strategi backup, restore, dan disaster recovery barbershop management system.
đ¯ Backup Strategy Overview
Komponen backup yang dicakup:
- Database Backup: MySQL data dan struktur
- File Backup: Storage files, uploads, assets
- Configuration Backup: Environment files, configs
- Code Backup: Git repository dan versioning
- Automated Backup: Scheduled backup tasks
đī¸ Database Backup
1. Automated Database Backup Service
app/Services/DatabaseBackupService.php
)
Database Backup Service (<?php
namespace App\Services;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Facades\Artisan;
use Carbon\Carbon;
class DatabaseBackupService
{
protected $backupDisk = 'backups';
protected $retentionDays = 30;
/**
* Create full database backup
*/
public function createFullBackup($description = null)
{
$timestamp = Carbon::now()->format('Y-m-d_H-i-s');
$filename = "full_backup_{$timestamp}.sql";
// Get database configuration
$config = config('database.connections.' . config('database.default'));
// Create mysqldump command
$command = sprintf(
'mysqldump --host=%s --port=%s --user=%s --password=%s --single-transaction --routines --triggers %s > %s',
$config['host'],
$config['port'] ?? 3306,
$config['username'],
$config['password'],
$config['database'],
storage_path("app/backups/{$filename}")
);
// Execute backup
$result = exec($command, $output, $returnCode);
if ($returnCode === 0) {
// Compress backup
$compressedFilename = $this->compressBackup($filename);
// Store backup info in database
$backupInfo = [
'filename' => $compressedFilename,
'type' => 'full',
'size' => Storage::disk($this->backupDisk)->size($compressedFilename),
'description' => $description,
'created_at' => now(),
];
DB::table('backup_logs')->insert($backupInfo);
// Clean old backups
$this->cleanOldBackups();
return $backupInfo;
}
throw new \Exception("Backup failed with return code: {$returnCode}");
}
/**
* Create incremental backup (only changed tables)
*/
public function createIncrementalBackup()
{
$timestamp = Carbon::now()->format('Y-m-d_H-i-s');
$filename = "incremental_backup_{$timestamp}.sql";
// Get tables that have been modified since last backup
$lastBackup = DB::table('backup_logs')
->where('type', 'incremental')
->latest('created_at')
->first();
$since = $lastBackup ? $lastBackup->created_at : now()->subDay();
$modifiedTables = $this->getModifiedTables($since);
if (empty($modifiedTables)) {
return ['message' => 'No changes detected since last backup'];
}
$config = config('database.connections.' . config('database.default'));
$backupPath = storage_path("app/backups/{$filename}");
// Create backup of modified tables only
$command = sprintf(
'mysqldump --host=%s --port=%s --user=%s --password=%s --single-transaction --where="updated_at >= \'%s\'" %s %s > %s',
$config['host'],
$config['port'] ?? 3306,
$config['username'],
$config['password'],
$since,
$config['database'],
implode(' ', $modifiedTables),
$backupPath
);
exec($command, $output, $returnCode);
if ($returnCode === 0) {
$compressedFilename = $this->compressBackup($filename);
$backupInfo = [
'filename' => $compressedFilename,
'type' => 'incremental',
'size' => Storage::disk($this->backupDisk)->size($compressedFilename),
'tables' => json_encode($modifiedTables),
'created_at' => now(),
];
DB::table('backup_logs')->insert($backupInfo);
return $backupInfo;
}
throw new \Exception("Incremental backup failed");
}
/**
* Create table-specific backup
*/
public function backupTable($tableName, $where = null)
{
$timestamp = Carbon::now()->format('Y-m-d_H-i-s');
$filename = "table_{$tableName}_{$timestamp}.sql";
$config = config('database.connections.' . config('database.default'));
$backupPath = storage_path("app/backups/{$filename}");
$whereClause = $where ? "--where=\"{$where}\"" : '';
$command = sprintf(
'mysqldump --host=%s --port=%s --user=%s --password=%s --single-transaction %s %s %s > %s',
$config['host'],
$config['port'] ?? 3306,
$config['username'],
$config['password'],
$whereClause,
$config['database'],
$tableName,
$backupPath
);
exec($command, $output, $returnCode);
if ($returnCode === 0) {
$compressedFilename = $this->compressBackup($filename);
return [
'filename' => $compressedFilename,
'table' => $tableName,
'size' => Storage::disk($this->backupDisk)->size($compressedFilename),
];
}
throw new \Exception("Table backup failed for: {$tableName}");
}
/**
* Compress backup file
*/
protected function compressBackup($filename)
{
$sourcePath = storage_path("app/backups/{$filename}");
$compressedFilename = str_replace('.sql', '.sql.gz', $filename);
$compressedPath = storage_path("app/backups/{$compressedFilename}");
// Compress using gzip
$command = "gzip -c {$sourcePath} > {$compressedPath}";
exec($command);
// Remove original uncompressed file
unlink($sourcePath);
return $compressedFilename;
}
/**
* Get tables modified since given date
*/
protected function getModifiedTables($since)
{
$tables = ['users', 'stores', 'reviews', 'history_points', 'reedem_codes'];
$modifiedTables = [];
foreach ($tables as $table) {
$count = DB::table($table)
->where('updated_at', '>=', $since)
->count();
if ($count > 0) {
$modifiedTables[] = $table;
}
}
return $modifiedTables;
}
/**
* Clean old backup files
*/
protected function cleanOldBackups()
{
$cutoffDate = Carbon::now()->subDays($this->retentionDays);
// Get old backup records
$oldBackups = DB::table('backup_logs')
->where('created_at', '<', $cutoffDate)
->get();
foreach ($oldBackups as $backup) {
// Delete file
if (Storage::disk($this->backupDisk)->exists($backup->filename)) {
Storage::disk($this->backupDisk)->delete($backup->filename);
}
// Delete record
DB::table('backup_logs')->where('id', $backup->id)->delete();
}
}
/**
* Get backup statistics
*/
public function getBackupStats()
{
return [
'total_backups' => DB::table('backup_logs')->count(),
'total_size' => DB::table('backup_logs')->sum('size'),
'last_full_backup' => DB::table('backup_logs')
->where('type', 'full')
->latest('created_at')
->first(),
'last_incremental_backup' => DB::table('backup_logs')
->where('type', 'incremental')
->latest('created_at')
->first(),
'oldest_backup' => DB::table('backup_logs')
->oldest('created_at')
->first(),
];
}
}
2. Backup Command
app/Console/Commands/DatabaseBackup.php
)
Backup Artisan Command (<?php
namespace App\Console\Commands;
use Illuminate\Console\Command;
use App\Services\DatabaseBackupService;
class DatabaseBackup extends Command
{
protected $signature = 'backup:database
{--type=full : Type of backup (full, incremental, table)}
{--table= : Specific table to backup}
{--description= : Backup description}
{--compress : Compress backup file}';
protected $description = 'Create database backup';
public function handle(DatabaseBackupService $backupService)
{
$type = $this->option('type');
$table = $this->option('table');
$description = $this->option('description');
$this->info('đ Starting database backup...');
try {
switch ($type) {
case 'full':
$result = $backupService->createFullBackup($description);
$this->info("â
Full backup created: {$result['filename']}");
$this->line("Size: " . $this->formatBytes($result['size']));
break;
case 'incremental':
$result = $backupService->createIncrementalBackup();
if (isset($result['message'])) {
$this->warn($result['message']);
} else {
$this->info("â
Incremental backup created: {$result['filename']}");
$this->line("Size: " . $this->formatBytes($result['size']));
}
break;
case 'table':
if (!$table) {
$this->error('Table name is required for table backup');
return 1;
}
$result = $backupService->backupTable($table);
$this->info("â
Table backup created: {$result['filename']}");
$this->line("Size: " . $this->formatBytes($result['size']));
break;
default:
$this->error('Invalid backup type. Use: full, incremental, or table');
return 1;
}
// Show backup statistics
$this->showBackupStats($backupService);
return 0;
} catch (\Exception $e) {
$this->error("Backup failed: " . $e->getMessage());
return 1;
}
}
protected function showBackupStats(DatabaseBackupService $backupService)
{
$stats = $backupService->getBackupStats();
$this->newLine();
$this->info('đ Backup Statistics:');
$this->table(
['Metric', 'Value'],
[
['Total Backups', $stats['total_backups']],
['Total Size', $this->formatBytes($stats['total_size'])],
['Last Full Backup', $stats['last_full_backup']->created_at ?? 'Never'],
['Last Incremental', $stats['last_incremental_backup']->created_at ?? 'Never'],
]
);
}
protected function formatBytes($bytes, $precision = 2)
{
$units = ['B', 'KB', 'MB', 'GB', 'TB'];
for ($i = 0; $bytes > 1024 && $i < count($units) - 1; $i++) {
$bytes /= 1024;
}
return round($bytes, $precision) . ' ' . $units[$i];
}
}
đ File System Backup
1. File Backup Service
app/Services/FileBackupService.php
)
File Backup Service (<?php
namespace App\Services;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Facades\File;
use Carbon\Carbon;
use ZipArchive;
class FileBackupService
{
protected $backupDisk = 'backups';
protected $sourceDisk = 'public';
/**
* Create complete file backup
*/
public function createFileBackup($directories = null)
{
$timestamp = Carbon::now()->format('Y-m-d_H-i-s');
$zipFilename = "files_backup_{$timestamp}.zip";
$zipPath = storage_path("app/backups/{$zipFilename}");
$directories = $directories ?: [
'storage/app/public',
'public/images',
'public/assets',
'resources/views',
'config',
];
$zip = new ZipArchive();
if ($zip->open($zipPath, ZipArchive::CREATE) === TRUE) {
foreach ($directories as $directory) {
$this->addDirectoryToZip($zip, $directory, basename($directory));
}
$zip->close();
$backupInfo = [
'filename' => $zipFilename,
'type' => 'files',
'size' => filesize($zipPath),
'directories' => json_encode($directories),
'created_at' => now(),
];
\DB::table('backup_logs')->insert($backupInfo);
return $backupInfo;
}
throw new \Exception('Failed to create file backup');
}
/**
* Backup specific storage directory
*/
public function backupStorageDirectory($directory = 'images')
{
$timestamp = Carbon::now()->format('Y-m-d_H-i-s');
$zipFilename = "storage_{$directory}_{$timestamp}.zip";
$zipPath = storage_path("app/backups/{$zipFilename}");
$sourcePath = Storage::disk($this->sourceDisk)->path($directory);
if (!File::isDirectory($sourcePath)) {
throw new \Exception("Directory not found: {$directory}");
}
$zip = new ZipArchive();
if ($zip->open($zipPath, ZipArchive::CREATE) === TRUE) {
$this->addDirectoryToZip($zip, $sourcePath, $directory);
$zip->close();
return [
'filename' => $zipFilename,
'directory' => $directory,
'size' => filesize($zipPath),
'files_count' => $this->countFilesInDirectory($sourcePath),
];
}
throw new \Exception('Failed to create storage backup');
}
/**
* Backup uploaded files by date range
*/
public function backupFilesByDateRange($startDate, $endDate, $directory = 'images')
{
$timestamp = Carbon::now()->format('Y-m-d_H-i-s');
$zipFilename = "files_range_{$startDate}_to_{$endDate}_{$timestamp}.zip";
$zipPath = storage_path("app/backups/{$zipFilename}");
$sourcePath = Storage::disk($this->sourceDisk)->path($directory);
$zip = new ZipArchive();
if ($zip->open($zipPath, ZipArchive::CREATE) === TRUE) {
$files = File::allFiles($sourcePath);
$addedFiles = 0;
foreach ($files as $file) {
$fileTime = Carbon::createFromTimestamp($file->getMTime());
if ($fileTime->between($startDate, $endDate)) {
$relativePath = str_replace($sourcePath . '/', '', $file->getPathname());
$zip->addFile($file->getPathname(), $directory . '/' . $relativePath);
$addedFiles++;
}
}
$zip->close();
if ($addedFiles === 0) {
unlink($zipPath);
return ['message' => 'No files found in the specified date range'];
}
return [
'filename' => $zipFilename,
'date_range' => "{$startDate} to {$endDate}",
'files_count' => $addedFiles,
'size' => filesize($zipPath),
];
}
throw new \Exception('Failed to create date range backup');
}
/**
* Add directory to ZIP recursively
*/
protected function addDirectoryToZip($zip, $sourcePath, $zipPath)
{
if (File::isDirectory($sourcePath)) {
$files = File::allFiles($sourcePath);
foreach ($files as $file) {
$relativePath = str_replace($sourcePath . '/', '', $file->getPathname());
$zip->addFile($file->getPathname(), $zipPath . '/' . $relativePath);
}
// Add empty directories
$directories = File::directories($sourcePath);
foreach ($directories as $directory) {
$relativePath = str_replace($sourcePath . '/', '', $directory);
$zip->addEmptyDir($zipPath . '/' . $relativePath);
$this->addDirectoryToZip($zip, $directory, $zipPath . '/' . $relativePath);
}
}
}
/**
* Count files in directory
*/
protected function countFilesInDirectory($path)
{
return count(File::allFiles($path));
}
/**
* Sync files to remote storage
*/
public function syncToRemoteStorage($localPath, $remoteDisk = 's3')
{
$files = Storage::disk('local')->allFiles($localPath);
$syncedFiles = 0;
foreach ($files as $file) {
$content = Storage::disk('local')->get($file);
$remotePath = 'backups/' . basename($file);
if (Storage::disk($remoteDisk)->put($remotePath, $content)) {
$syncedFiles++;
}
}
return [
'total_files' => count($files),
'synced_files' => $syncedFiles,
'remote_disk' => $remoteDisk,
];
}
/**
* Clean old file backups
*/
public function cleanOldFileBackups($days = 30)
{
$cutoffDate = Carbon::now()->subDays($days);
$backupPath = storage_path('app/backups');
$files = File::glob($backupPath . '/files_backup_*.zip');
$deletedCount = 0;
foreach ($files as $file) {
$fileTime = Carbon::createFromTimestamp(File::lastModified($file));
if ($fileTime->lt($cutoffDate)) {
File::delete($file);
$deletedCount++;
}
}
return $deletedCount;
}
}
2. File Backup Command
app/Console/Commands/FileBackup.php
)
File Backup Command (<?php
namespace App\Console\Commands;
use Illuminate\Console\Command;
use App\Services\FileBackupService;
use Carbon\Carbon;
class FileBackup extends Command
{
protected $signature = 'backup:files
{--type=full : Type of backup (full, storage, range)}
{--directory= : Specific directory to backup}
{--start-date= : Start date for range backup (YYYY-MM-DD)}
{--end-date= : End date for range backup (YYYY-MM-DD)}
{--sync : Sync to remote storage}';
protected $description = 'Create file system backup';
public function handle(FileBackupService $backupService)
{
$type = $this->option('type');
$directory = $this->option('directory');
$startDate = $this->option('start-date');
$endDate = $this->option('end-date');
$sync = $this->option('sync');
$this->info('đ Starting file backup...');
try {
switch ($type) {
case 'full':
$result = $backupService->createFileBackup();
$this->info("â
Full file backup created: {$result['filename']}");
$this->line("Size: " . $this->formatBytes($result['size']));
break;
case 'storage':
$directory = $directory ?: 'images';
$result = $backupService->backupStorageDirectory($directory);
$this->info("â
Storage backup created: {$result['filename']}");
$this->line("Directory: {$result['directory']}");
$this->line("Files: {$result['files_count']}");
$this->line("Size: " . $this->formatBytes($result['size']));
break;
case 'range':
if (!$startDate || !$endDate) {
$this->error('Start date and end date are required for range backup');
return 1;
}
$start = Carbon::createFromFormat('Y-m-d', $startDate);
$end = Carbon::createFromFormat('Y-m-d', $endDate);
$dir = $directory ?: 'images';
$result = $backupService->backupFilesByDateRange($start, $end, $dir);
if (isset($result['message'])) {
$this->warn($result['message']);
} else {
$this->info("â
Date range backup created: {$result['filename']}");
$this->line("Date range: {$result['date_range']}");
$this->line("Files: {$result['files_count']}");
$this->line("Size: " . $this->formatBytes($result['size']));
}
break;
default:
$this->error('Invalid backup type. Use: full, storage, or range');
return 1;
}
// Sync to remote storage if requested
if ($sync && isset($result['filename'])) {
$this->info('đŠī¸ Syncing to remote storage...');
$syncResult = $backupService->syncToRemoteStorage('backups');
$this->info("â
Synced {$syncResult['synced_files']} files to {$syncResult['remote_disk']}");
}
return 0;
} catch (\Exception $e) {
$this->error("File backup failed: " . $e->getMessage());
return 1;
}
}
protected function formatBytes($bytes, $precision = 2)
{
$units = ['B', 'KB', 'MB', 'GB', 'TB'];
for ($i = 0; $bytes > 1024 && $i < count($units) - 1; $i++) {
$bytes /= 1024;
}
return round($bytes, $precision) . ' ' . $units[$i];
}
}
đ Restore Operations
1. Database Restore Service
app/Services/DatabaseRestoreService.php
)
Database Restore Service (<?php
namespace App\Services;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Facades\Artisan;
class DatabaseRestoreService
{
protected $backupDisk = 'backups';
/**
* Restore database from backup file
*/
public function restoreFromBackup($backupFilename, $options = [])
{
$backupPath = storage_path("app/backups/{$backupFilename}");
if (!file_exists($backupPath)) {
throw new \Exception("Backup file not found: {$backupFilename}");
}
// Decompress if needed
if (str_ends_with($backupFilename, '.gz')) {
$decompressedPath = $this->decompressBackup($backupPath);
} else {
$decompressedPath = $backupPath;
}
// Create backup of current database before restore
if ($options['create_backup'] ?? true) {
$this->createPreRestoreBackup();
}
// Disable foreign key checks
DB::statement('SET FOREIGN_KEY_CHECKS=0;');
try {
$config = config('database.connections.' . config('database.default'));
$command = sprintf(
'mysql --host=%s --port=%s --user=%s --password=%s %s < %s',
$config['host'],
$config['port'] ?? 3306,
$config['username'],
$config['password'],
$config['database'],
$decompressedPath
);
exec($command, $output, $returnCode);
if ($returnCode === 0) {
// Re-enable foreign key checks
DB::statement('SET FOREIGN_KEY_CHECKS=1;');
// Log restore operation
DB::table('restore_logs')->insert([
'backup_filename' => $backupFilename,
'restore_type' => 'full',
'status' => 'success',
'created_at' => now(),
]);
// Clear application cache
$this->clearApplicationCache();
return [
'status' => 'success',
'backup_file' => $backupFilename,
'restored_at' => now(),
];
}
throw new \Exception("Restore failed with return code: {$returnCode}");
} catch (\Exception $e) {
// Re-enable foreign key checks
DB::statement('SET FOREIGN_KEY_CHECKS=1;');
// Log failed restore
DB::table('restore_logs')->insert([
'backup_filename' => $backupFilename,
'restore_type' => 'full',
'status' => 'failed',
'error_message' => $e->getMessage(),
'created_at' => now(),
]);
throw $e;
} finally {
// Clean up decompressed file if it was created
if ($decompressedPath !== $backupPath && file_exists($decompressedPath)) {
unlink($decompressedPath);
}
}
}
/**
* Restore specific table from backup
*/
public function restoreTable($backupFilename, $tableName, $options = [])
{
$backupPath = storage_path("app/backups/{$backupFilename}");
if (!file_exists($backupPath)) {
throw new \Exception("Backup file not found: {$backupFilename}");
}
// Create table-specific backup
if ($options['create_backup'] ?? true) {
$this->backupTable($tableName);
}
// Extract table data from backup
$tableData = $this->extractTableFromBackup($backupPath, $tableName);
if (empty($tableData)) {
throw new \Exception("Table {$tableName} not found in backup");
}
// Disable foreign key checks
DB::statement('SET FOREIGN_KEY_CHECKS=0;');
try {
// Truncate table if requested
if ($options['truncate'] ?? false) {
DB::table($tableName)->truncate();
}
// Import table data
DB::unprepared($tableData);
// Re-enable foreign key checks
DB::statement('SET FOREIGN_KEY_CHECKS=1;');
// Log restore operation
DB::table('restore_logs')->insert([
'backup_filename' => $backupFilename,
'restore_type' => 'table',
'table_name' => $tableName,
'status' => 'success',
'created_at' => now(),
]);
return [
'status' => 'success',
'table' => $tableName,
'backup_file' => $backupFilename,
];
} catch (\Exception $e) {
// Re-enable foreign key checks
DB::statement('SET FOREIGN_KEY_CHECKS=1;');
throw $e;
}
}
/**
* Create pre-restore backup
*/
protected function createPreRestoreBackup()
{
$timestamp = now()->format('Y-m-d_H-i-s');
$filename = "pre_restore_backup_{$timestamp}.sql";
$config = config('database.connections.' . config('database.default'));
$command = sprintf(
'mysqldump --host=%s --port=%s --user=%s --password=%s --single-transaction %s > %s',
$config['host'],
$config['port'] ?? 3306,
$config['username'],
$config['password'],
$config['database'],
storage_path("app/backups/{$filename}")
);
exec($command);
return $filename;
}
/**
* Backup specific table
*/
protected function backupTable($tableName)
{
$timestamp = now()->format('Y-m-d_H-i-s');
$filename = "pre_restore_{$tableName}_{$timestamp}.sql";
$config = config('database.connections.' . config('database.default'));
$command = sprintf(
'mysqldump --host=%s --port=%s --user=%s --password=%s --single-transaction %s %s > %s',
$config['host'],
$config['port'] ?? 3306,
$config['username'],
$config['password'],
$config['database'],
$tableName,
storage_path("app/backups/{$filename}")
);
exec($command);
return $filename;
}
/**
* Decompress backup file
*/
protected function decompressBackup($compressedPath)
{
$decompressedPath = str_replace('.gz', '', $compressedPath);
$command = "gunzip -c {$compressedPath} > {$decompressedPath}";
exec($command);
return $decompressedPath;
}
/**
* Extract specific table from backup
*/
protected function extractTableFromBackup($backupPath, $tableName)
{
$content = file_get_contents($backupPath);
// Find table creation and data
$pattern = "/DROP TABLE IF EXISTS `{$tableName}`;.*?UNLOCK TABLES;/s";
preg_match($pattern, $content, $matches);
return $matches[0] ?? '';
}
/**
* Clear application cache after restore
*/
protected function clearApplicationCache()
{
Artisan::call('cache:clear');
Artisan::call('config:clear');
Artisan::call('route:clear');
Artisan::call('view:clear');
}
/**
* List available backups
*/
public function listAvailableBackups()
{
return DB::table('backup_logs')
->orderBy('created_at', 'desc')
->get()
->map(function ($backup) {
$backup->size_formatted = $this->formatBytes($backup->size);
return $backup;
});
}
/**
* Validate backup file integrity
*/
public function validateBackup($backupFilename)
{
$backupPath = storage_path("app/backups/{$backupFilename}");
if (!file_exists($backupPath)) {
return ['valid' => false, 'error' => 'File not found'];
}
// Basic file checks
$fileSize = filesize($backupPath);
if ($fileSize === 0) {
return ['valid' => false, 'error' => 'Empty file'];
}
// Check if it's a valid SQL file (basic check)
$handle = fopen($backupPath, 'r');
$firstLine = fgets($handle);
fclose($handle);
if (!str_contains($firstLine, 'mysqldump') && !str_contains($firstLine, 'CREATE') && !str_contains($firstLine, 'INSERT')) {
return ['valid' => false, 'error' => 'Invalid SQL file format'];
}
return [
'valid' => true,
'size' => $fileSize,
'size_formatted' => $this->formatBytes($fileSize),
];
}
protected function formatBytes($bytes, $precision = 2)
{
$units = ['B', 'KB', 'MB', 'GB', 'TB'];
for ($i = 0; $bytes > 1024 && $i < count($units) - 1; $i++) {
$bytes /= 1024;
}
return round($bytes, $precision) . ' ' . $units[$i];
}
}
2. Restore Command
app/Console/Commands/DatabaseRestore.php
)
Restore Command (<?php
namespace App\Console\Commands;
use Illuminate\Console\Command;
use App\Services\DatabaseRestoreService;
class DatabaseRestore extends Command
{
protected $signature = 'backup:restore
{backup? : Backup filename to restore}
{--table= : Restore specific table only}
{--list : List available backups}
{--validate= : Validate backup file}
{--no-backup : Skip creating backup before restore}
{--truncate : Truncate table before restore (for table restore)}';
protected $description = 'Restore database from backup';
public function handle(DatabaseRestoreService $restoreService)
{
// List available backups
if ($this->option('list')) {
$this->listBackups($restoreService);
return 0;
}
// Validate backup file
if ($validateFile = $this->option('validate')) {
$this->validateBackup($restoreService, $validateFile);
return 0;
}
$backupFile = $this->argument('backup');
if (!$backupFile) {
$this->error('Please provide a backup filename or use --list to see available backups');
return 1;
}
// Confirm restore operation
if (!$this->confirmRestore($backupFile)) {
$this->info('Restore operation cancelled');
return 0;
}
$this->info('đ Starting restore operation...');
try {
$options = [
'create_backup' => !$this->option('no-backup'),
'truncate' => $this->option('truncate'),
];
if ($table = $this->option('table')) {
// Restore specific table
$result = $restoreService->restoreTable($backupFile, $table, $options);
$this->info("â
Table '{$table}' restored successfully from {$backupFile}");
} else {
// Full database restore
$result = $restoreService->restoreFromBackup($backupFile, $options);
$this->info("â
Database restored successfully from {$backupFile}");
}
return 0;
} catch (\Exception $e) {
$this->error("Restore failed: " . $e->getMessage());
return 1;
}
}
protected function listBackups(DatabaseRestoreService $restoreService)
{
$backups = $restoreService->listAvailableBackups();
if ($backups->isEmpty()) {
$this->warn('No backups found');
return;
}
$this->info('đ Available Backups:');
$tableData = $backups->map(function ($backup) {
return [
$backup->filename,
$backup->type,
$backup->size_formatted,
$backup->created_at,
];
})->toArray();
$this->table(
['Filename', 'Type', 'Size', 'Created'],
$tableData
);
}
protected function validateBackup(DatabaseRestoreService $restoreService, $filename)
{
$this->info("đ Validating backup: {$filename}");
$validation = $restoreService->validateBackup($filename);
if ($validation['valid']) {
$this->info("â
Backup file is valid");
$this->line("Size: {$validation['size_formatted']}");
} else {
$this->error("â Backup file is invalid: {$validation['error']}");
}
}
protected function confirmRestore($backupFile)
{
$this->warn('â ī¸ WARNING: This operation will overwrite your current database!');
$this->line("Backup file: {$backupFile}");
return $this->confirm('Are you sure you want to proceed with the restore?');
}
}
đ Automated Backup Scheduling
1. Backup Scheduler
app/Console/Kernel.php
)
Backup Schedule (<?php
namespace App\Console;
use Illuminate\Console\Scheduling\Schedule;
use Illuminate\Foundation\Console\Kernel as ConsoleKernel;
class Kernel extends ConsoleKernel
{
protected function schedule(Schedule $schedule)
{
// Daily full database backup at 2 AM
$schedule->command('backup:database --type=full --description="Daily automated backup"')
->dailyAt('02:00')
->withoutOverlapping()
->onFailure(function () {
// Send notification on failure
\Log::error('Daily database backup failed');
});
// Hourly incremental backup during business hours
$schedule->command('backup:database --type=incremental')
->hourly()
->between('08:00', '18:00')
->weekdays()
->withoutOverlapping();
// Weekly file backup on Sundays at 3 AM
$schedule->command('backup:files --type=full --sync')
->weekly()
->sundays()
->at('03:00')
->withoutOverlapping();
// Daily cleanup of old backups
$schedule->call(function () {
app(\App\Services\DatabaseBackupService::class)->cleanOldBackups();
app(\App\Services\FileBackupService::class)->cleanOldFileBackups();
})->dailyAt('04:00');
// Monthly full system backup
$schedule->command('backup:full-system')
->monthlyOn(1, '01:00')
->withoutOverlapping();
}
}
2. Full System Backup Command
app/Console/Commands/FullSystemBackup.php
)
Full System Backup (<?php
namespace App\Console\Commands;
use Illuminate\Console\Command;
use App\Services\DatabaseBackupService;
use App\Services\FileBackupService;
class FullSystemBackup extends Command
{
protected $signature = 'backup:full-system {--sync : Sync to remote storage}';
protected $description = 'Create complete system backup (database + files)';
public function handle(DatabaseBackupService $dbBackup, FileBackupService $fileBackup)
{
$this->info('đ Starting full system backup...');
$timestamp = now()->format('Y-m-d_H-i-s');
$results = [];
try {
// Database backup
$this->line('đ Creating database backup...');
$dbResult = $dbBackup->createFullBackup("Full system backup - {$timestamp}");
$results['database'] = $dbResult;
$this->info("â
Database backup: {$dbResult['filename']}");
// File backup
$this->line('đ Creating file backup...');
$fileResult = $fileBackup->createFileBackup();
$results['files'] = $fileResult;
$this->info("â
File backup: {$fileResult['filename']}");
// Sync to remote if requested
if ($this->option('sync')) {
$this->line('đŠī¸ Syncing to remote storage...');
$syncResult = $fileBackup->syncToRemoteStorage('backups');
$results['sync'] = $syncResult;
$this->info("â
Synced {$syncResult['synced_files']} files");
}
// Summary
$this->newLine();
$this->info('đ Backup Summary:');
$this->table(
['Component', 'Filename', 'Size'],
[
['Database', $dbResult['filename'], $this->formatBytes($dbResult['size'])],
['Files', $fileResult['filename'], $this->formatBytes($fileResult['size'])],
]
);
$totalSize = $dbResult['size'] + $fileResult['size'];
$this->info("Total backup size: " . $this->formatBytes($totalSize));
return 0;
} catch (\Exception $e) {
$this->error("Full system backup failed: " . $e->getMessage());
return 1;
}
}
protected function formatBytes($bytes, $precision = 2)
{
$units = ['B', 'KB', 'MB', 'GB', 'TB'];
for ($i = 0; $bytes > 1024 && $i < count($units) - 1; $i++) {
$bytes /= 1024;
}
return round($bytes, $precision) . ' ' . $units[$i];
}
}
đ§ Configuration & Setup
1. Backup Configuration
config/backup.php
)
Backup Config (<?php
return [
'backup' => [
'name' => env('BACKUP_NAME', env('APP_NAME', 'barbershop')),
'source' => [
'files' => [
'include' => [
base_path(),
],
'exclude' => [
base_path('vendor'),
base_path('node_modules'),
base_path('storage/logs'),
base_path('storage/framework/cache'),
base_path('storage/framework/sessions'),
base_path('storage/framework/views'),
],
'follow_links' => false,
'ignore_unreadable_directories' => false,
'relative_path' => null,
],
'databases' => [
'mysql',
],
],
'database_dump_compressor' => \Spatie\DbDumper\Compressors\GzipCompressor::class,
'database_dump_file_extension' => '',
'destination' => [
'filename_prefix' => '',
'disks' => [
'local',
's3', // If using cloud storage
],
],
'temporary_directory' => storage_path('app/backup-temp'),
'password' => env('BACKUP_PASSWORD'),
'encryption' => 'default',
],
'notifications' => [
'notifications' => [
\Spatie\Backup\Notifications\Notifications\BackupHasFailed::class => ['mail'],
\Spatie\Backup\Notifications\Notifications\UnhealthyBackupWasFound::class => ['mail'],
\Spatie\Backup\Notifications\Notifications\CleanupHasFailed::class => ['mail'],
\Spatie\Backup\Notifications\Notifications\BackupWasSuccessful::class => ['mail'],
\Spatie\Backup\Notifications\Notifications\HealthyBackupWasFound::class => ['mail'],
\Spatie\Backup\Notifications\Notifications\CleanupWasSuccessful::class => ['mail'],
],
'notifiable' => \Spatie\Backup\Notifications\Notifiable::class,
'mail' => [
'to' => env('BACKUP_MAIL_TO', '[email protected]'),
'from' => [
'address' => env('MAIL_FROM_ADDRESS', '[email protected]'),
'name' => env('MAIL_FROM_NAME', 'Example'),
],
],
'slack' => [
'webhook_url' => env('BACKUP_SLACK_WEBHOOK_URL'),
'channel' => env('BACKUP_SLACK_CHANNEL'),
'username' => env('BACKUP_SLACK_USERNAME'),
'icon' => env('BACKUP_SLACK_ICON'),
],
'discord' => [
'webhook_url' => env('BACKUP_DISCORD_WEBHOOK_URL'),
'username' => env('BACKUP_DISCORD_USERNAME'),
'avatar_url' => env('BACKUP_DISCORD_AVATAR_URL'),
],
],
'monitor_backups' => [
[
'name' => env('BACKUP_NAME', env('APP_NAME', 'barbershop')),
'disks' => ['local'],
'health_checks' => [
\Spatie\Backup\Tasks\Monitor\HealthChecks\MaximumAgeInDays::class => 1,
\Spatie\Backup\Tasks\Monitor\HealthChecks\MaximumStorageInMegabytes::class => 5000,
],
],
],
'cleanup' => [
'strategy' => \Spatie\Backup\Tasks\Cleanup\Strategies\DefaultStrategy::class,
'default_strategy' => [
'keep_all_backups_for_days' => 7,
'keep_daily_backups_for_days' => 16,
'keep_weekly_backups_for_weeks' => 8,
'keep_monthly_backups_for_months' => 4,
'keep_yearly_backups_for_years' => 2,
'delete_oldest_backups_when_using_more_megabytes_than' => 5000,
],
],
];
2. Database Migration for Backup Logs
database/migrations/create_backup_logs_table.php
)
Backup Logs Migration (<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up()
{
Schema::create('backup_logs', function (Blueprint $table) {
$table->id();
$table->string('filename');
$table->enum('type', ['full', 'incremental', 'files', 'table']);
$table->bigInteger('size')->unsigned();
$table->text('description')->nullable();
$table->json('tables')->nullable();
$table->json('directories')->nullable();
$table->enum('status', ['success', 'failed', 'in_progress'])->default('success');
$table->text('error_message')->nullable();
$table->timestamps();
$table->index(['type', 'created_at']);
$table->index(['status', 'created_at']);
});
Schema::create('restore_logs', function (Blueprint $table) {
$table->id();
$table->string('backup_filename');
$table->enum('restore_type', ['full', 'table', 'partial']);
$table->string('table_name')->nullable();
$table->enum('status', ['success', 'failed', 'in_progress']);
$table->text('error_message')->nullable();
$table->json('options')->nullable();
$table->timestamps();
$table->index(['status', 'created_at']);
$table->index(['backup_filename']);
});
}
public function down()
{
Schema::dropIfExists('restore_logs');
Schema::dropIfExists('backup_logs');
}
};
Next: Monitoring & Logging untuk sistem monitoring dan logging aplikasi.