Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
| Total | |
100.00% |
30 / 30 |
|
100.00% |
5 / 5 |
CRAP | |
100.00% |
1 / 1 |
| BackupController | |
100.00% |
30 / 30 |
|
100.00% |
5 / 5 |
8 | |
100.00% |
1 / 1 |
| disk | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
| folder | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
| index | |
100.00% |
18 / 18 |
|
100.00% |
1 / 1 |
2 | |||
| run | |
100.00% |
5 / 5 |
|
100.00% |
1 / 1 |
2 | |||
| download | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
2 | |||
| 1 | <?php |
| 2 | |
| 3 | namespace App\Http\Controllers\admin; |
| 4 | |
| 5 | use App\Http\Controllers\Controller; |
| 6 | use Illuminate\Http\RedirectResponse; |
| 7 | use Illuminate\Support\Carbon; |
| 8 | use Illuminate\Support\Facades\Artisan; |
| 9 | use Illuminate\Support\Facades\Storage; |
| 10 | use Illuminate\View\View; |
| 11 | use Symfony\Component\HttpFoundation\StreamedResponse; |
| 12 | |
| 13 | /** |
| 14 | * Settings → Backups. Lists the archives spatie/laravel-backup has written |
| 15 | * to the `local` disk, lets an admin download one or trigger a fresh |
| 16 | * database backup now. Restoring is deliberately NOT offered here — it is |
| 17 | * destructive and stays a CLI-only operation (same standing rule as |
| 18 | * migrate / cache:clear: never HTTP-triggerable). The page shows the |
| 19 | * restore runbook as text instead. |
| 20 | */ |
| 21 | class BackupController extends Controller |
| 22 | { |
| 23 | private function disk() |
| 24 | { |
| 25 | $disks = (array) config('backup.backup.destination.disks', ['local']); |
| 26 | |
| 27 | return Storage::disk($disks[0] ?? 'local'); |
| 28 | } |
| 29 | |
| 30 | private function folder(): string |
| 31 | { |
| 32 | return config('backup.backup.name', config('app.name', 'laravel-backup')); |
| 33 | } |
| 34 | |
| 35 | public function index(): View |
| 36 | { |
| 37 | $disk = $this->disk(); |
| 38 | $folder = $this->folder(); |
| 39 | |
| 40 | $backups = collect($disk->exists($folder) ? $disk->files($folder) : []) |
| 41 | ->filter(fn ($path) => str_ends_with($path, '.zip')) |
| 42 | ->map(fn ($path) => [ |
| 43 | 'name' => basename($path), |
| 44 | 'size' => $disk->size($path), |
| 45 | 'date' => Carbon::createFromTimestamp($disk->lastModified($path)), |
| 46 | ]) |
| 47 | ->sortByDesc('date') |
| 48 | ->values(); |
| 49 | |
| 50 | return view('admin.backups.index', [ |
| 51 | 'backups' => $backups, |
| 52 | 'totalBytes' => $backups->sum('size'), |
| 53 | 'autoDaily' => (bool) settings('backup.auto_daily', true), |
| 54 | 'dailyTime' => settings('backup.daily_time', '02:30'), |
| 55 | 'keepDays' => (int) settings('backup.keep_days', 30), |
| 56 | ]); |
| 57 | } |
| 58 | |
| 59 | public function run(): RedirectResponse |
| 60 | { |
| 61 | try { |
| 62 | // --only-db keeps the manual button fast and light enough to run |
| 63 | // inline in a request; the full DB+files archive runs weekly from |
| 64 | // the scheduler (App\Console\Kernel). |
| 65 | Artisan::call('backup:run', ['--only-db' => true]); |
| 66 | } catch (\Throwable $e) { |
| 67 | report($e); |
| 68 | |
| 69 | return back()->with('error', 'Backup failed: ' . $e->getMessage()); |
| 70 | } |
| 71 | |
| 72 | return back()->with('success', 'Database backup created.'); |
| 73 | } |
| 74 | |
| 75 | public function download(string $name): StreamedResponse |
| 76 | { |
| 77 | $disk = $this->disk(); |
| 78 | $path = $this->folder() . '/' . basename($name); |
| 79 | |
| 80 | abort_unless(str_ends_with($path, '.zip') && $disk->exists($path), 404); |
| 81 | |
| 82 | return $disk->download($path); |
| 83 | } |
| 84 | } |