Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ Why use our backup addon?
- Choose exactly what you want to backup by configuring the backup [pipeline](docs/pipeline.md).
- Easy to extend and customize, [just create a new pipes](docs/pipeline.md#creating-a-new-backup-pipe)!
- Uses laravels [storage system](https://laravel.com/docs/11.x/filesystem) and thus supports external storage out of the box.
- Tested, the addon have over 85% test coverage.
- Tested, the addon have over 95% test coverage.

## Installation

Expand Down
34 changes: 5 additions & 29 deletions client/src/components/Actions.vue
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,6 @@
:status="file.status"
:percent="file.progress * 100"
:file="file"
@restore="restore(file)"
/>
</ul>
</div>
Expand All @@ -38,7 +37,11 @@ export default {
upload: UploadButton,
"upload-status": UploadStatus,
},

mounted(){
this.$root.$on("uploaded", (file) => {
this.files = this.files.filter((item) => item.file.uniqueIdentifier !== file.uniqueIdentifier)
});
},
data() {
return {
files: [],
Expand Down Expand Up @@ -80,33 +83,6 @@ export default {
this.loading = false;
});
},
restore(file) {
this.loading = true;
this.confirming = false;
file.status = "restoring";

this.$store.dispatch('backup-provider/setStatus','restore_in_progress');

this.$axios
.post(cp_url("api/backups/restore-from-path"), {
path: file.path,
})
.then(({ data }) => {
this.$toast.info(__(data.message));
})
.catch((error) => {
let message = __("statamic-backup::backup.restore.failed");

if (error.response.data.message) {
message = error.response.data.message;
}
this.$toast.error(__(message));
})
.finally(() => {
this.loading = false;
file.status = "restored";
});
},
},
};
</script>
39 changes: 20 additions & 19 deletions client/src/components/Listing.vue
Original file line number Diff line number Diff line change
Expand Up @@ -38,22 +38,22 @@
v-if="canDownload.isPermitted"
:disabled="!canDownload.isPossible"
:text="__('statamic-backup::backup.download.label')"
:redirect="download_url(backup.timestamp)"
:redirect="download_url(backup.id)"
/>
<span v-if="canRestore.isPermitted && canRestore.isPossible">
<hr class="divider" />
<dropdown-item
:disabled="!canRestore.isPossible"
:text="__('statamic-backup::backup.restore.label')"
@click="initiateRestore(backup.timestamp, backup.name)"
@click="initiateRestore(backup.id, backup.name)"
/>
</span>
<span v-if="canDestroy.isPermitted && canDestroy.isPossible">
<hr class="divider" />
<dropdown-item
:text="__('statamic-backup::backup.destroy.label')"
dangerous="true"
@click="initiateDestroy(backup.timestamp, backup.name)"
@click="initiateDestroy(backup.id, backup.name)"
/>
</span>
</dropdown-list>
Expand Down Expand Up @@ -92,7 +92,8 @@ export default {
mixins: [Listing],

mounted() {
this.$on("onDestroyed", this.request);
this.$on("onDestroyed", this.request); // refetch backup list after destroy is completed
this.$root.$on("uploaded", this.request); // refetch backup list after upload is completed
},
watch: {
status(newStatus, oldStatus) {
Expand All @@ -117,7 +118,7 @@ export default {
columns: this.initialColumns,
confirmingRestore: false,
confirmingDestroy: false,
activeTimestamp: null,
activeId: null,
activeName: null,
};
},
Expand All @@ -139,22 +140,22 @@ export default {
},
},
methods: {
download_url(timestamp) {
return cp_url("api/backups/download/" + timestamp);
download_url(id) {
return cp_url("api/backups/download/" + id);
},
restore_url(timestamp) {
return cp_url("api/backups/restore/" + timestamp);
restore_url(id) {
return cp_url("api/backups/restore/" + id);
},
destroy_url(timestamp) {
return cp_url("api/backups/" + timestamp);
destroy_url(id) {
return cp_url("api/backups/" + id);
},
initiateDestroy(timestamp, name) {
this.activeTimestamp = timestamp;
initiateDestroy(id, name) {
this.activeId = id;
this.activeName = name;
this.confirmingDestroy = true;
},
initiateRestore(timestamp, name) {
this.activeTimestamp = timestamp;
initiateRestore(id, name) {
this.activeId = id;
this.activeName = name;
this.confirmingRestore = true;
},
Expand All @@ -165,7 +166,7 @@ export default {

this.$store.dispatch('backup-provider/setStatus', 'restore_in_progress');
this.$axios
.post(this.restore_url(this.activeTimestamp))
.post(this.restore_url(this.activeId))
.then(({ data }) => {
this.$toast.info(__(data.message));
this.$emit("onRestored");
Expand All @@ -180,15 +181,15 @@ export default {
})
.finally(() => {
this.activeName = null;
this.activeTimestamp = null;
this.activeId = null;
});
},
destroy() {
if (!this.canDestroy.isPossible) return console.warn("Cannot destroy backups.");

this.confirmingDestroy = false;
this.$axios
.delete(this.destroy_url(this.activeTimestamp))
.delete(this.destroy_url(this.activeId))
.then(({ data }) => {
this.$toast.success(__(data.message));
this.$emit("onDestroyed");
Expand All @@ -203,7 +204,7 @@ export default {
})
.finally(() => {
this.activeName = null;
this.activeTimestamp = null;
this.activeId = null;
});
},
},
Expand Down
6 changes: 2 additions & 4 deletions client/src/components/Upload.vue
Original file line number Diff line number Diff line change
Expand Up @@ -87,11 +87,9 @@ export default {
});

this.resumable.on("fileSuccess", (file, event) => {
const data = JSON.parse(event);
this.findFile(file).status = "success";
this.findFile(file).path = data.file;
const data = JSON.parse(event);;
this.$toast.success(data.message);
this.$emit("uploaded", data.file);
this.$root.$emit("uploaded", file);
});

this.resumable.on("fileError", (file, event) => {
Expand Down
23 changes: 0 additions & 23 deletions client/src/components/UploadStatus.vue
Original file line number Diff line number Diff line change
Expand Up @@ -52,26 +52,6 @@
>
<svg-icon name="micro/circle-with-cross" class="h-4 w-4" />
</button>

<button
v-if="status === 'success'"
@click.prevent="restore"
class="btn-primary"
>
<svg-icon name="folder-home" class="h-4 w-4 mr-2 text-current" />
<span>{{ __("statamic-backup::backup.restore.label") }}</span>
</button>

<loading-graphic v-if="status === 'restoring'" :inline="true" text="" />

<span v-if="status === 'restored'" class="text-green-500 filename">
{{ __("statamic-backup::backup.restore.success") }}
<svg-icon
v-if="status === 'restored'"
name="light/check"
class="h-4 w-4 ml-2"
/>
</span>
</div>
</div>
</template>
Expand All @@ -92,9 +72,6 @@ export default {
pause() {
this.$emit("pause", this.file);
},
restore() {
this.$emit("restore", this.file);
},
},
};
</script>
7 changes: 7 additions & 0 deletions config/backup.php
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,13 @@
// 'time' => '03:00',
],

/**
* The backup name resolver
*
* the resolver handles generating and parsing backup names
*/
'name_resolver' => \Itiden\Backup\GenericBackupNameResolver::class,

/**
* The backup repository
*
Expand Down
4 changes: 4 additions & 0 deletions docs/pages/.vitepress/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,10 @@ export default defineConfig({
{ text: "Metadata", link: "/metadata.md" },
],
},
{
text: "Advanced",
items: [{ text: "Naming backups", link: "/naming-backups.md" }],
},
],

search: {
Expand Down
63 changes: 63 additions & 0 deletions docs/pages/naming-backups.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
# Naming backups

If you want to customize how backups are named and "discovered", you can!

The default naming scheme will be:

```
{app.name}-{timestamp}-{id}.zip
```

## Customizing

You can customize the naming by providing your own `BackupNameResolver` implementation.

This class is responsible for generating filenames and parsing files into identifiable information and required metadata in the form of `ResolvedBackupData`.
So when making your own implementation, you need to make sure that your generate and parseFilename methods work togheter or it will not work.

Here is an example of a custom `BackupNameResolver` implementation:

```php
use Carbon\CarbonImmutable;
use Itiden\Backup\Contracts\BackupNameResolver;
use Itiden\Backup\DataTransferObjects\ResolvedBackupData;

final readonly class MyAppSpecificBackupNameResolver implements BackupNameResolver
{
private const string Separator = '---';

// return a custom filename, the ".zip" extension will be added automatically if it is missing
public function generateFilename(CarbonImmutable $createdAt, string $id): string
{
$parts = [
"some-testest-that-implies-something",
$createdAt->format('Y-m-d'),
$id,
];

return implode(self::Separator, $parts);
}

public function parseFilename(string $path): ?ResolvedBackupData
{
$filename = pathinfo($path, PATHINFO_FILENAME);

$parts = explode(self::Separator, $filename);

// if the filename cannot be parsed, return null
if (count($parts) !== 3) {
return null;
}

[$name, $date, $identifier] = $parts;

$createdAt = CarbonImmutable::createFromFormat('Y-m-d', $date);

return new ResolvedBackupData(
createdAt: $createdAt,
id: $identifier,
name: $name
);
}
}
```
11 changes: 3 additions & 8 deletions routes/cp.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
use Itiden\Backup\Http\Controllers\Api\BackupController;
use Itiden\Backup\Http\Controllers\Api\DestroyBackupController;
use Itiden\Backup\Http\Controllers\Api\RestoreController;
use Itiden\Backup\Http\Controllers\Api\RestoreFromPathController;
use Itiden\Backup\Http\Controllers\Api\StateController;
use Itiden\Backup\Http\Controllers\Api\StoreBackupController;
use Itiden\Backup\Http\Controllers\DownloadBackupController;
Expand Down Expand Up @@ -43,19 +42,15 @@
->middleware('can:create backups')
->name('store');

Route::delete('/{timestamp}', DestroyBackupController::class)
Route::delete('/{id}', DestroyBackupController::class)
->middleware('can:delete backups')
->name('destroy');

Route::get('/download/{timestamp}', DownloadBackupController::class)
Route::get('/download/{id}', DownloadBackupController::class)
->middleware('can:download backups')
->name('download');

Route::post('/restore/{timestamp}', RestoreController::class)
Route::post('/restore/{id}', RestoreController::class)
->middleware('can:restore backups')
->name('restore');

Route::post('/restore-from-path', RestoreFromPathController::class)
->middleware('can:restore backups')
->name('restore-from-path');
});
4 changes: 2 additions & 2 deletions src/Backuper.php
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,7 @@ private function resolveMetaFromZip(Zipper $zip): Collection
/**
* Remove oldest backups when max backups is exceeded if it's present.
*/
private function enforceMaxBackups(): void
public function enforceMaxBackups(): void
{
$maxBackups = config('backup.max_backups', false);
if (!$maxBackups) {
Expand All @@ -133,7 +133,7 @@ private function enforceMaxBackups(): void
if ($backups->count() > $maxBackups) {
$backups
->slice($maxBackups)
->each(fn(BackupDto $backup): ?BackupDto => $this->repository->remove($backup->timestamp));
->each(fn(BackupDto $backup): ?BackupDto => $this->repository->remove($backup->id));
}
}
}
10 changes: 8 additions & 2 deletions src/Console/Commands/RestoreCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,15 @@ final class RestoreCommand extends Command

public function handle(BackupRepository $repo): void
{
/* @var BackupDto $backup */
/** @var BackupDto $backup */
$backup = match (true) {
(bool) $this->option('path') => BackupDto::fromAbsolutePath($this->option('path')),
(bool) $this->option('path') => new BackupDto(
'not-that-important',
basename($this->option('path')),
now()->toImmutable(),
(string) filesize($this->option('path')),
$this->option('path'),
),
default
=> BackupDto::fromFile(select(
label: 'Which backup do you want to restore to?',
Expand Down
Loading