Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
30 / 30
100.00% covered (success)
100.00%
5 / 5
CRAP
100.00% covered (success)
100.00%
1 / 1
BackupController
100.00% covered (success)
100.00%
30 / 30
100.00% covered (success)
100.00%
5 / 5
8
100.00% covered (success)
100.00%
1 / 1
 disk
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 folder
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 index
100.00% covered (success)
100.00%
18 / 18
100.00% covered (success)
100.00%
1 / 1
2
 run
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
2
 download
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
2
1<?php
2
3namespace App\Http\Controllers\admin;
4
5use App\Http\Controllers\Controller;
6use Illuminate\Http\RedirectResponse;
7use Illuminate\Support\Carbon;
8use Illuminate\Support\Facades\Artisan;
9use Illuminate\Support\Facades\Storage;
10use Illuminate\View\View;
11use 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 */
21class 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}