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

Database Backup Service (app/Services/DatabaseBackupService.php)

<?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

Backup Artisan Command (app/Console/Commands/DatabaseBackup.php)

<?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

File Backup Service (app/Services/FileBackupService.php)

<?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

File Backup Command (app/Console/Commands/FileBackup.php)

<?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

Database Restore Service (app/Services/DatabaseRestoreService.php)

<?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

Restore Command (app/Console/Commands/DatabaseRestore.php)

<?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

Backup Schedule (app/Console/Kernel.php)

<?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

Full System Backup (app/Console/Commands/FullSystemBackup.php)

<?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

Backup Config (config/backup.php)

<?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

Backup Logs Migration (database/migrations/create_backup_logs_table.php)

<?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.