Skip to content

Commit 0672a99

Browse files
authored
add file backups (#855)
* Add file backups * restore file backups * fix * fix ui * fix ui * fix backup restore for files * support isolated users * add dev command to composer
1 parent 6885757 commit 0672a99

34 files changed

+1090
-420
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
/storage/database.sqlite
1111
/storage/database-test.sqlite
1212
/storage/database.sqlite-journal
13+
/storage/database-test.sqlite-journal
1314
.env
1415
.env.backup
1516
.env.production
Lines changed: 26 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
<?php
22

3-
namespace App\Actions\Database;
3+
namespace App\Actions\Backup;
44

55
use App\Enums\BackupFileStatus;
66
use App\Enums\BackupStatus;
7+
use App\Enums\BackupType;
78
use App\Enums\DatabaseStatus;
89
use App\Models\Backup;
910
use App\Models\Server;
@@ -24,10 +25,13 @@ public function create(Server $server, array $input): Backup
2425
{
2526
$this->validate($server, $input);
2627

28+
$backupType = BackupType::from($input['type'] ?? BackupType::DATABASE->value);
29+
2730
$backup = new Backup([
28-
'type' => 'database',
31+
'type' => $backupType,
2932
'server_id' => $server->id,
30-
'database_id' => $input['database'] ?? null,
33+
'database_id' => $backupType === BackupType::DATABASE ? $input['database'] : null,
34+
'path' => $backupType === BackupType::FILE ? $input['path'] : null,
3135
'storage_id' => $input['storage'],
3236
'interval' => $input['interval'] == 'custom' ? $input['custom_interval'] : $input['interval'],
3337
'keep_backups' => $input['keep'],
@@ -73,7 +77,13 @@ public function stop(Backup $backup): void
7377

7478
private function validate(Server $server, array $input): void
7579
{
80+
$backupType = BackupType::from($input['type'] ?? BackupType::DATABASE->value);
81+
7682
$rules = [
83+
'type' => [
84+
'required',
85+
Rule::in([BackupType::DATABASE->value, BackupType::FILE->value]),
86+
],
7787
'storage' => [
7888
'required',
7989
Rule::exists('storage_providers', 'id'),
@@ -87,13 +97,23 @@ private function validate(Server $server, array $input): void
8797
'required',
8898
Rule::in(array_keys(config('core.cronjob_intervals'))),
8999
],
90-
'database' => [
100+
];
101+
102+
if ($backupType === BackupType::DATABASE) {
103+
$rules['database'] = [
91104
'required',
92105
Rule::exists('databases', 'id')
93106
->where('server_id', $server->id)
94107
->where('status', DatabaseStatus::READY),
95-
],
96-
];
108+
];
109+
} else {
110+
$rules['path'] = [
111+
'required',
112+
'string',
113+
'min:1',
114+
];
115+
}
116+
97117
if (isset($input['interval']) && $input['interval'] == 'custom') {
98118
$rules['custom_interval'] = [
99119
'required',
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
<?php
22

3-
namespace App\Actions\Database;
3+
namespace App\Actions\Backup;
44

55
use App\Enums\BackupFileStatus;
66
use App\Models\BackupFile;
Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
<?php
2+
3+
namespace App\Actions\Backup;
4+
5+
use App\Enums\BackupFileStatus;
6+
use App\Enums\BackupType;
7+
use App\Models\BackupFile;
8+
use App\Models\Database;
9+
use App\Models\Server;
10+
use App\Models\Service;
11+
use Illuminate\Support\Facades\Validator;
12+
use Illuminate\Validation\Rule;
13+
14+
class RestoreBackup
15+
{
16+
/**
17+
* @param array<string, mixed> $input
18+
*/
19+
public function restore(BackupFile $backupFile, array $input): void
20+
{
21+
$this->validate($backupFile->backup->server, $input, $backupFile->backup->type);
22+
23+
$backup = $backupFile->backup;
24+
$backupFile->status = BackupFileStatus::RESTORING;
25+
26+
if ($backup->type === BackupType::DATABASE) {
27+
$this->restoreDatabase($backupFile, $input);
28+
}
29+
30+
if ($backup->type === BackupType::FILE) {
31+
$this->restoreFile($backupFile, $input);
32+
}
33+
}
34+
35+
private function restoreDatabase(BackupFile $backupFile, array $input): void
36+
{
37+
/** @var Database $database */
38+
$database = Database::query()->findOrFail($input['database']);
39+
$backupFile->restored_to = $database->name;
40+
$backupFile->save();
41+
42+
dispatch(function () use ($backupFile, $database): void {
43+
/** @var Service $service */
44+
$service = $database->server->database();
45+
/** @var \App\Services\Database\Database $databaseHandler */
46+
$databaseHandler = $service->handler();
47+
$databaseHandler->restoreBackup($backupFile, $database->name);
48+
$backupFile->status = BackupFileStatus::RESTORED;
49+
$backupFile->restored_at = now();
50+
$backupFile->save();
51+
})->catch(function () use ($backupFile): void {
52+
$backupFile->status = BackupFileStatus::RESTORE_FAILED;
53+
$backupFile->save();
54+
})->onQueue('ssh');
55+
}
56+
57+
private function restoreFile(BackupFile $backupFile, array $input): void
58+
{
59+
// File backup restoration
60+
$restorePath = $input['path'];
61+
$owner = $input['owner'] ?? 'vito:vito';
62+
$permissions = $input['permissions'] ?? '755';
63+
64+
$backupFile->restored_to = $restorePath;
65+
$backupFile->save();
66+
67+
dispatch(function () use ($backupFile, $restorePath, $owner, $permissions): void {
68+
$server = $backupFile->backup->server;
69+
$tempBackupPath = $backupFile->tempPath();
70+
71+
// Download backup from storage provider
72+
$backupFile->backup->storage->provider()->ssh($server)->download(
73+
$backupFile->path(),
74+
$tempBackupPath
75+
);
76+
77+
// Extract the archive using OS service with custom owner and permissions
78+
$server->os()->extractArchive($tempBackupPath, $restorePath, $owner, $permissions);
79+
80+
// Clean up temporary file
81+
$server->os()->deleteFile($tempBackupPath);
82+
83+
$backupFile->status = BackupFileStatus::RESTORED;
84+
$backupFile->restored_at = now();
85+
$backupFile->save();
86+
})->catch(function () use ($backupFile): void {
87+
$backupFile->status = BackupFileStatus::RESTORE_FAILED;
88+
$backupFile->save();
89+
$backupFile->backup->server->os()->deleteFile($backupFile->tempPath());
90+
})->onQueue('ssh');
91+
}
92+
93+
private function validate(Server $server, array $input, BackupType $backupType): void
94+
{
95+
$rules = [];
96+
97+
if ($backupType === BackupType::DATABASE) {
98+
$rules['database'] = [
99+
'required',
100+
Rule::exists('databases', 'id')->where('server_id', $server->id),
101+
];
102+
} else {
103+
$rules['path'] = [
104+
'required',
105+
'string',
106+
'min:1',
107+
];
108+
$rules['owner'] = [
109+
'required',
110+
'string',
111+
'regex:/^[a-zA-Z0-9_-]+(:[a-zA-Z0-9_-]+)?$/',
112+
];
113+
$rules['permissions'] = [
114+
'required',
115+
'string',
116+
'regex:/^[0-7]{3,4}$/',
117+
];
118+
}
119+
120+
Validator::make($input, $rules)->validate();
121+
}
122+
}

app/Actions/Backup/RunBackup.php

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
<?php
2+
3+
namespace App\Actions\Backup;
4+
5+
use App\Enums\BackupFileStatus;
6+
use App\Enums\BackupStatus;
7+
use App\Enums\BackupType;
8+
use App\Models\Backup;
9+
use App\Models\BackupFile;
10+
use App\Models\Service;
11+
use App\Services\Database\Database;
12+
use Illuminate\Support\Str;
13+
14+
class RunBackup
15+
{
16+
public function run(Backup $backup): BackupFile
17+
{
18+
// Determine the backup name based on type
19+
$backupName = $backup->type === BackupType::FILE
20+
? basename($backup->path)
21+
: $backup->database?->name;
22+
23+
$file = new BackupFile([
24+
'backup_id' => $backup->id,
25+
'name' => Str::of($backupName)->slug().'-'.now()->format('YmdHis'),
26+
'status' => BackupFileStatus::CREATING,
27+
]);
28+
$file->save();
29+
30+
dispatch(function () use ($file, $backup): void {
31+
if ($backup->type === BackupType::DATABASE) {
32+
/** @var Service $service */
33+
$service = $backup->server->database();
34+
/** @var Database $databaseHandler */
35+
$databaseHandler = $service->handler();
36+
$databaseHandler->runBackup($file);
37+
}
38+
39+
if ($backup->type === BackupType::FILE) {
40+
$this->compressAndUploadFile($file, $backup);
41+
}
42+
43+
$file->status = BackupFileStatus::CREATED;
44+
$file->save();
45+
46+
if ($backup->status !== BackupStatus::RUNNING) {
47+
$backup->status = BackupStatus::RUNNING;
48+
$backup->save();
49+
}
50+
})->catch(function () use ($file, $backup): void {
51+
$backup->status = BackupStatus::FAILED;
52+
$backup->save();
53+
$file->status = BackupFileStatus::FAILED;
54+
$file->save();
55+
})->onQueue('ssh');
56+
57+
return $file;
58+
}
59+
60+
public function compressAndUploadFile(BackupFile $file, Backup $backup): void
61+
{
62+
$server = $backup->server;
63+
$sourcePath = $backup->path;
64+
$tempZipPath = $file->tempPath();
65+
66+
// Remove any existing zip file first
67+
$server->os()->deleteFile($tempZipPath);
68+
69+
// Compress the file/directory using OS service
70+
$server->os()->compress($sourcePath, $tempZipPath);
71+
72+
// Upload to storage provider
73+
$upload = $backup->storage->provider()->ssh($server)->upload(
74+
$tempZipPath,
75+
$file->path()
76+
);
77+
78+
// Clean up temporary file
79+
$server->os()->deleteFile($tempZipPath);
80+
81+
// Set file size from upload response
82+
$file->size = $upload['size'];
83+
$file->save();
84+
}
85+
}

app/Actions/Database/DeleteDatabase.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
namespace App\Actions\Database;
44

5+
use App\Actions\Backup\ManageBackup;
56
use App\Models\Backup;
67
use App\Models\Database;
78
use App\Models\Server;

app/Actions/Database/RestoreBackup.php

Lines changed: 0 additions & 52 deletions
This file was deleted.

0 commit comments

Comments
 (0)