Skip to content

Commit d9a43a8

Browse files
feat(backup)!: add support for customizing backup names and use ids to identify backups (#60)
Refactors backup naming and retrieval to allow users to implement custom `BackupNameResolver`s. BREAKING CHANGE: backups using the old system wont be discovered anymore. uploading files is also reworked so they will now appear in the list and get ran through the namegenerator.
1 parent f6ecae5 commit d9a43a8

44 files changed

Lines changed: 470 additions & 416 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ Why use our backup addon?
2525
- Choose exactly what you want to backup by configuring the backup [pipeline](docs/pipeline.md).
2626
- Easy to extend and customize, [just create a new pipes](docs/pipeline.md#creating-a-new-backup-pipe)!
2727
- Uses laravels [storage system](https://laravel.com/docs/11.x/filesystem) and thus supports external storage out of the box.
28-
- Tested, the addon have over 85% test coverage.
28+
- Tested, the addon have over 95% test coverage.
2929

3030
## Installation
3131

client/src/components/Actions.vue

Lines changed: 5 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,6 @@
2323
:status="file.status"
2424
:percent="file.progress * 100"
2525
:file="file"
26-
@restore="restore(file)"
2726
/>
2827
</ul>
2928
</div>
@@ -38,7 +37,11 @@ export default {
3837
upload: UploadButton,
3938
"upload-status": UploadStatus,
4039
},
41-
40+
mounted(){
41+
this.$root.$on("uploaded", (file) => {
42+
this.files = this.files.filter((item) => item.file.uniqueIdentifier !== file.uniqueIdentifier)
43+
});
44+
},
4245
data() {
4346
return {
4447
files: [],
@@ -80,33 +83,6 @@ export default {
8083
this.loading = false;
8184
});
8285
},
83-
restore(file) {
84-
this.loading = true;
85-
this.confirming = false;
86-
file.status = "restoring";
87-
88-
this.$store.dispatch('backup-provider/setStatus','restore_in_progress');
89-
90-
this.$axios
91-
.post(cp_url("api/backups/restore-from-path"), {
92-
path: file.path,
93-
})
94-
.then(({ data }) => {
95-
this.$toast.info(__(data.message));
96-
})
97-
.catch((error) => {
98-
let message = __("statamic-backup::backup.restore.failed");
99-
100-
if (error.response.data.message) {
101-
message = error.response.data.message;
102-
}
103-
this.$toast.error(__(message));
104-
})
105-
.finally(() => {
106-
this.loading = false;
107-
file.status = "restored";
108-
});
109-
},
11086
},
11187
};
11288
</script>

client/src/components/Listing.vue

Lines changed: 20 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -38,22 +38,22 @@
3838
v-if="canDownload.isPermitted"
3939
:disabled="!canDownload.isPossible"
4040
:text="__('statamic-backup::backup.download.label')"
41-
:redirect="download_url(backup.timestamp)"
41+
:redirect="download_url(backup.id)"
4242
/>
4343
<span v-if="canRestore.isPermitted && canRestore.isPossible">
4444
<hr class="divider" />
4545
<dropdown-item
4646
:disabled="!canRestore.isPossible"
4747
:text="__('statamic-backup::backup.restore.label')"
48-
@click="initiateRestore(backup.timestamp, backup.name)"
48+
@click="initiateRestore(backup.id, backup.name)"
4949
/>
5050
</span>
5151
<span v-if="canDestroy.isPermitted && canDestroy.isPossible">
5252
<hr class="divider" />
5353
<dropdown-item
5454
:text="__('statamic-backup::backup.destroy.label')"
5555
dangerous="true"
56-
@click="initiateDestroy(backup.timestamp, backup.name)"
56+
@click="initiateDestroy(backup.id, backup.name)"
5757
/>
5858
</span>
5959
</dropdown-list>
@@ -92,7 +92,8 @@ export default {
9292
mixins: [Listing],
9393
9494
mounted() {
95-
this.$on("onDestroyed", this.request);
95+
this.$on("onDestroyed", this.request); // refetch backup list after destroy is completed
96+
this.$root.$on("uploaded", this.request); // refetch backup list after upload is completed
9697
},
9798
watch: {
9899
status(newStatus, oldStatus) {
@@ -117,7 +118,7 @@ export default {
117118
columns: this.initialColumns,
118119
confirmingRestore: false,
119120
confirmingDestroy: false,
120-
activeTimestamp: null,
121+
activeId: null,
121122
activeName: null,
122123
};
123124
},
@@ -139,22 +140,22 @@ export default {
139140
},
140141
},
141142
methods: {
142-
download_url(timestamp) {
143-
return cp_url("api/backups/download/" + timestamp);
143+
download_url(id) {
144+
return cp_url("api/backups/download/" + id);
144145
},
145-
restore_url(timestamp) {
146-
return cp_url("api/backups/restore/" + timestamp);
146+
restore_url(id) {
147+
return cp_url("api/backups/restore/" + id);
147148
},
148-
destroy_url(timestamp) {
149-
return cp_url("api/backups/" + timestamp);
149+
destroy_url(id) {
150+
return cp_url("api/backups/" + id);
150151
},
151-
initiateDestroy(timestamp, name) {
152-
this.activeTimestamp = timestamp;
152+
initiateDestroy(id, name) {
153+
this.activeId = id;
153154
this.activeName = name;
154155
this.confirmingDestroy = true;
155156
},
156-
initiateRestore(timestamp, name) {
157-
this.activeTimestamp = timestamp;
157+
initiateRestore(id, name) {
158+
this.activeId = id;
158159
this.activeName = name;
159160
this.confirmingRestore = true;
160161
},
@@ -165,7 +166,7 @@ export default {
165166
166167
this.$store.dispatch('backup-provider/setStatus', 'restore_in_progress');
167168
this.$axios
168-
.post(this.restore_url(this.activeTimestamp))
169+
.post(this.restore_url(this.activeId))
169170
.then(({ data }) => {
170171
this.$toast.info(__(data.message));
171172
this.$emit("onRestored");
@@ -180,15 +181,15 @@ export default {
180181
})
181182
.finally(() => {
182183
this.activeName = null;
183-
this.activeTimestamp = null;
184+
this.activeId = null;
184185
});
185186
},
186187
destroy() {
187188
if (!this.canDestroy.isPossible) return console.warn("Cannot destroy backups.");
188189
189190
this.confirmingDestroy = false;
190191
this.$axios
191-
.delete(this.destroy_url(this.activeTimestamp))
192+
.delete(this.destroy_url(this.activeId))
192193
.then(({ data }) => {
193194
this.$toast.success(__(data.message));
194195
this.$emit("onDestroyed");
@@ -203,7 +204,7 @@ export default {
203204
})
204205
.finally(() => {
205206
this.activeName = null;
206-
this.activeTimestamp = null;
207+
this.activeId = null;
207208
});
208209
},
209210
},

client/src/components/Upload.vue

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -87,11 +87,9 @@ export default {
8787
});
8888
8989
this.resumable.on("fileSuccess", (file, event) => {
90-
const data = JSON.parse(event);
91-
this.findFile(file).status = "success";
92-
this.findFile(file).path = data.file;
90+
const data = JSON.parse(event);;
9391
this.$toast.success(data.message);
94-
this.$emit("uploaded", data.file);
92+
this.$root.$emit("uploaded", file);
9593
});
9694
9795
this.resumable.on("fileError", (file, event) => {

client/src/components/UploadStatus.vue

Lines changed: 0 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -52,26 +52,6 @@
5252
>
5353
<svg-icon name="micro/circle-with-cross" class="h-4 w-4" />
5454
</button>
55-
56-
<button
57-
v-if="status === 'success'"
58-
@click.prevent="restore"
59-
class="btn-primary"
60-
>
61-
<svg-icon name="folder-home" class="h-4 w-4 mr-2 text-current" />
62-
<span>{{ __("statamic-backup::backup.restore.label") }}</span>
63-
</button>
64-
65-
<loading-graphic v-if="status === 'restoring'" :inline="true" text="" />
66-
67-
<span v-if="status === 'restored'" class="text-green-500 filename">
68-
{{ __("statamic-backup::backup.restore.success") }}
69-
<svg-icon
70-
v-if="status === 'restored'"
71-
name="light/check"
72-
class="h-4 w-4 ml-2"
73-
/>
74-
</span>
7555
</div>
7656
</div>
7757
</template>
@@ -92,9 +72,6 @@ export default {
9272
pause() {
9373
this.$emit("pause", this.file);
9474
},
95-
restore() {
96-
this.$emit("restore", this.file);
97-
},
9875
},
9976
};
10077
</script>

config/backup.php

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,13 @@
4848
// 'time' => '03:00',
4949
],
5050

51+
/**
52+
* The backup name resolver
53+
*
54+
* the resolver handles generating and parsing backup names
55+
*/
56+
'name_resolver' => \Itiden\Backup\GenericBackupNameResolver::class,
57+
5158
/**
5259
* The backup repository
5360
*

docs/pages/.vitepress/config.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,10 @@ export default defineConfig({
3232
{ text: "Metadata", link: "/metadata.md" },
3333
],
3434
},
35+
{
36+
text: "Advanced",
37+
items: [{ text: "Naming backups", link: "/naming-backups.md" }],
38+
},
3539
],
3640

3741
search: {

docs/pages/naming-backups.md

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
# Naming backups
2+
3+
If you want to customize how backups are named and "discovered", you can!
4+
5+
The default naming scheme will be:
6+
7+
```
8+
{app.name}-{timestamp}-{id}.zip
9+
```
10+
11+
## Customizing
12+
13+
You can customize the naming by providing your own `BackupNameResolver` implementation.
14+
15+
This class is responsible for generating filenames and parsing files into identifiable information and required metadata in the form of `ResolvedBackupData`.
16+
So when making your own implementation, you need to make sure that your generate and parseFilename methods work togheter or it will not work.
17+
18+
Here is an example of a custom `BackupNameResolver` implementation:
19+
20+
```php
21+
use Carbon\CarbonImmutable;
22+
use Itiden\Backup\Contracts\BackupNameResolver;
23+
use Itiden\Backup\DataTransferObjects\ResolvedBackupData;
24+
25+
final readonly class MyAppSpecificBackupNameResolver implements BackupNameResolver
26+
{
27+
private const string Separator = '---';
28+
29+
// return a custom filename, the ".zip" extension will be added automatically if it is missing
30+
public function generateFilename(CarbonImmutable $createdAt, string $id): string
31+
{
32+
$parts = [
33+
"some-testest-that-implies-something",
34+
$createdAt->format('Y-m-d'),
35+
$id,
36+
];
37+
38+
return implode(self::Separator, $parts);
39+
}
40+
41+
public function parseFilename(string $path): ?ResolvedBackupData
42+
{
43+
$filename = pathinfo($path, PATHINFO_FILENAME);
44+
45+
$parts = explode(self::Separator, $filename);
46+
47+
// if the filename cannot be parsed, return null
48+
if (count($parts) !== 3) {
49+
return null;
50+
}
51+
52+
[$name, $date, $identifier] = $parts;
53+
54+
$createdAt = CarbonImmutable::createFromFormat('Y-m-d', $date);
55+
56+
return new ResolvedBackupData(
57+
createdAt: $createdAt,
58+
id: $identifier,
59+
name: $name
60+
);
61+
}
62+
}
63+
```

routes/cp.php

Lines changed: 3 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@
44
use Itiden\Backup\Http\Controllers\Api\BackupController;
55
use Itiden\Backup\Http\Controllers\Api\DestroyBackupController;
66
use Itiden\Backup\Http\Controllers\Api\RestoreController;
7-
use Itiden\Backup\Http\Controllers\Api\RestoreFromPathController;
87
use Itiden\Backup\Http\Controllers\Api\StateController;
98
use Itiden\Backup\Http\Controllers\Api\StoreBackupController;
109
use Itiden\Backup\Http\Controllers\DownloadBackupController;
@@ -43,19 +42,15 @@
4342
->middleware('can:create backups')
4443
->name('store');
4544

46-
Route::delete('/{timestamp}', DestroyBackupController::class)
45+
Route::delete('/{id}', DestroyBackupController::class)
4746
->middleware('can:delete backups')
4847
->name('destroy');
4948

50-
Route::get('/download/{timestamp}', DownloadBackupController::class)
49+
Route::get('/download/{id}', DownloadBackupController::class)
5150
->middleware('can:download backups')
5251
->name('download');
5352

54-
Route::post('/restore/{timestamp}', RestoreController::class)
53+
Route::post('/restore/{id}', RestoreController::class)
5554
->middleware('can:restore backups')
5655
->name('restore');
57-
58-
Route::post('/restore-from-path', RestoreFromPathController::class)
59-
->middleware('can:restore backups')
60-
->name('restore-from-path');
6156
});

src/Backuper.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -121,7 +121,7 @@ private function resolveMetaFromZip(Zipper $zip): Collection
121121
/**
122122
* Remove oldest backups when max backups is exceeded if it's present.
123123
*/
124-
private function enforceMaxBackups(): void
124+
public function enforceMaxBackups(): void
125125
{
126126
$maxBackups = config('backup.max_backups', false);
127127
if (!$maxBackups) {
@@ -133,7 +133,7 @@ private function enforceMaxBackups(): void
133133
if ($backups->count() > $maxBackups) {
134134
$backups
135135
->slice($maxBackups)
136-
->each(fn(BackupDto $backup): ?BackupDto => $this->repository->remove($backup->timestamp));
136+
->each(fn(BackupDto $backup): ?BackupDto => $this->repository->remove($backup->id));
137137
}
138138
}
139139
}

0 commit comments

Comments
 (0)