diff --git a/README.md b/README.md
index 2d5a769..f740964 100644
--- a/README.md
+++ b/README.md
@@ -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
diff --git a/client/src/components/Actions.vue b/client/src/components/Actions.vue
index a89623c..fef3e38 100644
--- a/client/src/components/Actions.vue
+++ b/client/src/components/Actions.vue
@@ -23,7 +23,6 @@
:status="file.status"
:percent="file.progress * 100"
:file="file"
- @restore="restore(file)"
/>
@@ -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: [],
@@ -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";
- });
- },
},
};
diff --git a/client/src/components/Listing.vue b/client/src/components/Listing.vue
index 84b0644..6eff728 100644
--- a/client/src/components/Listing.vue
+++ b/client/src/components/Listing.vue
@@ -38,14 +38,14 @@
v-if="canDownload.isPermitted"
:disabled="!canDownload.isPossible"
:text="__('statamic-backup::backup.download.label')"
- :redirect="download_url(backup.timestamp)"
+ :redirect="download_url(backup.id)"
/>
@@ -53,7 +53,7 @@
@@ -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) {
@@ -117,7 +118,7 @@ export default {
columns: this.initialColumns,
confirmingRestore: false,
confirmingDestroy: false,
- activeTimestamp: null,
+ activeId: null,
activeName: null,
};
},
@@ -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;
},
@@ -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");
@@ -180,7 +181,7 @@ export default {
})
.finally(() => {
this.activeName = null;
- this.activeTimestamp = null;
+ this.activeId = null;
});
},
destroy() {
@@ -188,7 +189,7 @@ export default {
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");
@@ -203,7 +204,7 @@ export default {
})
.finally(() => {
this.activeName = null;
- this.activeTimestamp = null;
+ this.activeId = null;
});
},
},
diff --git a/client/src/components/Upload.vue b/client/src/components/Upload.vue
index fa85065..2d839f8 100644
--- a/client/src/components/Upload.vue
+++ b/client/src/components/Upload.vue
@@ -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) => {
diff --git a/client/src/components/UploadStatus.vue b/client/src/components/UploadStatus.vue
index 9db568a..f8ef2b2 100644
--- a/client/src/components/UploadStatus.vue
+++ b/client/src/components/UploadStatus.vue
@@ -52,26 +52,6 @@
>
-
-
-
-
-
-
- {{ __("statamic-backup::backup.restore.success") }}
-
-
@@ -92,9 +72,6 @@ export default {
pause() {
this.$emit("pause", this.file);
},
- restore() {
- this.$emit("restore", this.file);
- },
},
};
diff --git a/config/backup.php b/config/backup.php
index 9a688a8..f89cc33 100644
--- a/config/backup.php
+++ b/config/backup.php
@@ -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
*
diff --git a/docs/pages/.vitepress/config.js b/docs/pages/.vitepress/config.js
index 1944b88..d5d3e56 100644
--- a/docs/pages/.vitepress/config.js
+++ b/docs/pages/.vitepress/config.js
@@ -32,6 +32,10 @@ export default defineConfig({
{ text: "Metadata", link: "/metadata.md" },
],
},
+ {
+ text: "Advanced",
+ items: [{ text: "Naming backups", link: "/naming-backups.md" }],
+ },
],
search: {
diff --git a/docs/pages/naming-backups.md b/docs/pages/naming-backups.md
new file mode 100644
index 0000000..0382821
--- /dev/null
+++ b/docs/pages/naming-backups.md
@@ -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
+ );
+ }
+}
+```
diff --git a/routes/cp.php b/routes/cp.php
index 12f786f..2f3bf53 100644
--- a/routes/cp.php
+++ b/routes/cp.php
@@ -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;
@@ -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');
});
diff --git a/src/Backuper.php b/src/Backuper.php
index 31b786f..7cc2ed5 100644
--- a/src/Backuper.php
+++ b/src/Backuper.php
@@ -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) {
@@ -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));
}
}
}
diff --git a/src/Console/Commands/RestoreCommand.php b/src/Console/Commands/RestoreCommand.php
index 25ea252..10834e3 100644
--- a/src/Console/Commands/RestoreCommand.php
+++ b/src/Console/Commands/RestoreCommand.php
@@ -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?',
diff --git a/src/Contracts/BackupNameResolver.php b/src/Contracts/BackupNameResolver.php
new file mode 100644
index 0000000..6f73d8e
--- /dev/null
+++ b/src/Contracts/BackupNameResolver.php
@@ -0,0 +1,22 @@
+afterLast('-')
- ->before('.zip')
- ->toString();
- $bytes = Storage::disk(config('backup.destination.disk'))->size($path);
+ $values = app(BackupNameResolver::class)->parseFilename($path);
- return new self(
- name: File::name($path),
- created_at: Carbon::createFromTimestamp($timestamp),
- size: StatamicStr::fileSizeForHumans($bytes, 2),
- path: $path,
- timestamp: $timestamp,
- );
- }
+ if (!$values) {
+ return null;
+ }
- /**
- * Create a new BackupDto from a absolute path
- */
- public static function fromAbsolutePath(string $path): self
- {
- $timestamp = str(basename($path))
- ->afterLast('-')
- ->before('.zip')
- ->toString();
- $bytes = File::size($path);
+ $bytes = Storage::disk(config('backup.destination.disk'))->size($path);
- return new self(
- name: File::name($path),
- created_at: Carbon::createFromTimestamp($timestamp),
+ return new static(
+ id: $values->id,
+ name: $values->name,
+ created_at: $values->createdAt,
size: StatamicStr::fileSizeForHumans($bytes, 2),
path: $path,
- timestamp: $timestamp,
);
}
}
diff --git a/src/DataTransferObjects/ResolvedBackupData.php b/src/DataTransferObjects/ResolvedBackupData.php
new file mode 100644
index 0000000..ab88f9f
--- /dev/null
+++ b/src/DataTransferObjects/ResolvedBackupData.php
@@ -0,0 +1,17 @@
+slug(),
+ $createdAt->timestamp,
+ $id,
+ ]);
+ }
+
+ public function parseFilename(string $path): ?ResolvedBackupData
+ {
+ /** @var string */
+ $filename = pathinfo($path, PATHINFO_FILENAME);
+
+ $parts = explode(static::Separator, $filename);
+
+ if (count($parts) !== 3)
+ return null;
+
+ [$name, $createdAt, $id] = $parts;
+
+ return new ResolvedBackupData(
+ createdAt: CarbonImmutable::createFromTimestamp($createdAt),
+ id: $id,
+ name: $name,
+ );
+ }
+}
diff --git a/src/Http/Controllers/Api/DestroyBackupController.php b/src/Http/Controllers/Api/DestroyBackupController.php
index e89046c..e12a503 100644
--- a/src/Http/Controllers/Api/DestroyBackupController.php
+++ b/src/Http/Controllers/Api/DestroyBackupController.php
@@ -10,9 +10,9 @@
final readonly class DestroyBackupController
{
- public function __invoke(string $timestamp, BackupRepository $repo): JsonResponse|RedirectResponse
+ public function __invoke(string $id, BackupRepository $repo): JsonResponse|RedirectResponse
{
- $backup = $repo->remove($timestamp);
+ $backup = $repo->remove($id);
return response()->json(['message' => __('statamic-backup::backup.destroy.success', [
'name' => $backup->name,
diff --git a/src/Http/Controllers/Api/RestoreController.php b/src/Http/Controllers/Api/RestoreController.php
index 2d45e1f..ce11bdc 100644
--- a/src/Http/Controllers/Api/RestoreController.php
+++ b/src/Http/Controllers/Api/RestoreController.php
@@ -8,15 +8,15 @@
use Illuminate\Contracts\Cache\Repository;
use Illuminate\Http\JsonResponse;
use Itiden\Backup\Exceptions\ActionAlreadyInProgress;
-use Itiden\Backup\Jobs\RestoreFromTimestampJob;
+use Itiden\Backup\Jobs\RestoreJob;
use Itiden\Backup\StateManager;
use Statamic\Contracts\Auth\User;
final readonly class RestoreController
{
- public function __invoke(string $timestamp, StateManager $stateManager, #[Authenticated] User $user): JsonResponse
+ public function __invoke(string $id, StateManager $stateManager, #[Authenticated] User $user): JsonResponse
{
- $stateManager->dispatch(new RestoreFromTimestampJob($timestamp, $user));
+ $stateManager->dispatch(new RestoreJob($id, $user));
return response()->json(['message' => __('statamic-backup::backup.restore.started')]);
}
diff --git a/src/Http/Controllers/Api/RestoreFromPathController.php b/src/Http/Controllers/Api/RestoreFromPathController.php
deleted file mode 100644
index ee13b26..0000000
--- a/src/Http/Controllers/Api/RestoreFromPathController.php
+++ /dev/null
@@ -1,22 +0,0 @@
-dispatch(new RestoreFromPathJob(path: $request->validated('path')));
-
- return response()->json(['message' => __('statamic-backup::backup.restore.started')]);
- }
-}
diff --git a/src/Http/Controllers/DownloadBackupController.php b/src/Http/Controllers/DownloadBackupController.php
index 833625c..7f24a70 100644
--- a/src/Http/Controllers/DownloadBackupController.php
+++ b/src/Http/Controllers/DownloadBackupController.php
@@ -13,9 +13,9 @@
/**
* Handle the incoming request.
*/
- public function __invoke(string $timestamp, BackupRepository $repo): StreamedResponse
+ public function __invoke(string $id, BackupRepository $repo): StreamedResponse
{
- $backup = $repo->find($timestamp);
+ $backup = $repo->find($id);
$backup
->getMetadata()
diff --git a/src/Http/Controllers/UploadController.php b/src/Http/Controllers/UploadController.php
index 4dd1ba9..4af4b53 100644
--- a/src/Http/Controllers/UploadController.php
+++ b/src/Http/Controllers/UploadController.php
@@ -6,6 +6,8 @@
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
+use Itiden\Backup\Backuper;
+use Itiden\Backup\Contracts\Repositories\BackupRepository;
use Itiden\Backup\DataTransferObjects\ChunkyTestDto;
use Itiden\Backup\DataTransferObjects\ChunkyUploadDto;
use Itiden\Backup\Http\Requests\ChunkyUploadRequest;
@@ -13,9 +15,15 @@
final readonly class UploadController
{
- public function __invoke(ChunkyUploadRequest $request): JsonResponse
+ public function __invoke(ChunkyUploadRequest $request, BackupRepository $repo, Backuper $backuper): JsonResponse
{
- return Chunky::put(ChunkyUploadDto::fromRequest($request));
+ return Chunky::put(ChunkyUploadDto::fromRequest($request), onCompleted: function (string $completeFile) use (
+ $repo,
+ $backuper,
+ ): void {
+ $repo->add($completeFile);
+ $backuper->enforceMaxBackups();
+ });
}
public function test(Request $request): JsonResponse
diff --git a/src/Http/Requests/RestoreFromPathRequest.php b/src/Http/Requests/RestoreFromPathRequest.php
deleted file mode 100644
index 7d1400e..0000000
--- a/src/Http/Requests/RestoreFromPathRequest.php
+++ /dev/null
@@ -1,17 +0,0 @@
- 'required|string',
- ];
- }
-}
diff --git a/src/Http/Resources/BackupResource.php b/src/Http/Resources/BackupResource.php
index 46a8293..cd159f1 100644
--- a/src/Http/Resources/BackupResource.php
+++ b/src/Http/Resources/BackupResource.php
@@ -17,7 +17,7 @@ public function toArray($request): array
'name' => $this->name,
'created_at' => $this->created_at->format('Y-m-d H:i:s'),
'path' => $this->path,
- 'timestamp' => $this->timestamp,
+ 'id' => $this->id,
'size' => $this->size,
'metadata' => new MetadataResource(resource: $this->getMetadata()),
];
diff --git a/src/Jobs/RestoreFromPathJob.php b/src/Jobs/RestoreFromPathJob.php
deleted file mode 100644
index 9f90d16..0000000
--- a/src/Jobs/RestoreFromPathJob.php
+++ /dev/null
@@ -1,43 +0,0 @@
-restore(BackupDto::fromAbsolutePath($this->path));
-
- $cache->forget(StateManager::JOB_QUEUED_KEY);
-
- File::delete($this->path);
- }
-
- public function failed(): void
- {
- app(Repository::class)->forget(StateManager::JOB_QUEUED_KEY);
- }
-}
diff --git a/src/Jobs/RestoreFromTimestampJob.php b/src/Jobs/RestoreJob.php
similarity index 77%
rename from src/Jobs/RestoreFromTimestampJob.php
rename to src/Jobs/RestoreJob.php
index 587b836..72885bc 100644
--- a/src/Jobs/RestoreFromTimestampJob.php
+++ b/src/Jobs/RestoreJob.php
@@ -9,7 +9,7 @@
use Itiden\Backup\Restorer;
use Statamic\Contracts\Auth\User;
-final class RestoreFromTimestampJob implements ShouldQueue
+final class RestoreJob implements ShouldQueue
{
use Queueable;
@@ -17,7 +17,7 @@ final class RestoreFromTimestampJob implements ShouldQueue
* Create a new job instance.
*/
public function __construct(
- private string $timestamp,
+ private string $id,
private User $user,
) {
}
@@ -28,6 +28,6 @@ public function __construct(
public function handle(Restorer $backuper): void
{
auth()->login($this->user); // ugly but it works;
- $backuper->restoreFromTimestamp($this->timestamp);
+ $backuper->restoreFromId($this->id);
}
}
diff --git a/src/Models/Metadata.php b/src/Models/Metadata.php
index ccaf161..582b81f 100644
--- a/src/Models/Metadata.php
+++ b/src/Models/Metadata.php
@@ -39,7 +39,7 @@ public function __construct(
'root' => config('backup.metadata_path') . '/.meta',
]);
- $yaml = YAML::parse($this->filesystem->get($this->backup->timestamp) ?? '');
+ $yaml = YAML::parse($this->filesystem->get($this->backup->id) ?? '');
$this->createdBy = $yaml['created_by'] ?? null;
$this->downloads = array_map(UserActionDto::fromArray(...), $yaml['downloads'] ?? []);
@@ -107,13 +107,13 @@ public function addSkippedPipe(string $pipe, string $reason): void
public function delete(): void
{
- $this->filesystem->delete($this->backup->timestamp);
+ $this->filesystem->delete($this->backup->id);
}
private function save(): void
{
$this->filesystem->put(
- $this->backup->timestamp,
+ $this->backup->id,
YAML::dump([
'created_by' => $this->createdBy,
'downloads' => array_map(fn(UserActionDto $action): array => $action->toArray(), $this->downloads),
diff --git a/src/Repositories/FileBackupRepository.php b/src/Repositories/FileBackupRepository.php
index 1ed0c27..dcb1280 100644
--- a/src/Repositories/FileBackupRepository.php
+++ b/src/Repositories/FileBackupRepository.php
@@ -4,13 +4,14 @@
namespace Itiden\Backup\Repositories;
-use Carbon\Carbon;
+use Carbon\CarbonImmutable;
use Illuminate\Contracts\Filesystem\Filesystem;
use Illuminate\Filesystem\FilesystemAdapter;
use Illuminate\Http\File as StreamableFile;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Str;
+use Itiden\Backup\Contracts\BackupNameResolver;
use Itiden\Backup\Contracts\Repositories\BackupRepository;
use Itiden\Backup\DataTransferObjects\BackupDto;
use Itiden\Backup\Events\BackupDeleted;
@@ -22,53 +23,46 @@ final class FileBackupRepository implements BackupRepository
/** @var FilesystemAdapter */
private Filesystem $filesystem;
- public function __construct()
- {
+ public function __construct(
+ private BackupNameResolver $nameResolver,
+ ) {
$this->path = config('backup.destination.path');
$this->filesystem = Storage::disk(config('backup.destination.disk'));
}
- private function makeFilename(string $timestamp): string
- {
- return Str::slug(config('app.name')) . '-' . $timestamp . '.zip';
- }
-
public function all(): Collection
{
- return collect($this->filesystem->files($this->path))
+ return collect($this->filesystem->allFiles($this->path))
->map(BackupDto::fromFile(...))
- ->sortByDesc('timestamp');
+ ->whereInstanceOf(BackupDto::class)
+ ->sortByDesc(fn(BackupDto $backup) => $backup->created_at);
}
public function add(string $path): BackupDto
{
$this->filesystem->makeDirectory(path: $this->path);
- $timestamp = (string) Carbon::now()->unix();
+ $id = (string) Str::ulid();
$this->filesystem->putFileAs(
path: $this->path,
file: new StreamableFile($path),
- name: $this->makeFilename($timestamp),
+ name: (string) str($this->nameResolver->generateFilename(CarbonImmutable::now(), $id))->finish('.zip'),
);
- return $this->find($timestamp);
+ return $this->find($id);
}
- public function find(string $timestamp): ?BackupDto
+ public function find(string $id): ?BackupDto
{
- $path = "{$this->path}/{$this->makeFilename($timestamp)}";
-
- if (!$this->filesystem->exists($path)) {
- return null;
- }
-
- return BackupDto::fromFile($path);
+ return $this
+ ->all()
+ ->first(fn(BackupDto $backup): bool => $backup->id === $id);
}
- public function remove(string $timestamp): ?BackupDto
+ public function remove(string $id): ?BackupDto
{
- $backup = $this->find($timestamp);
+ $backup = $this->find($id);
if (!$backup) {
return null;
@@ -85,7 +79,7 @@ public function empty(): bool
{
$this
->all()
- ->each(fn(BackupDto $backup): ?BackupDto => $this->remove($backup->timestamp));
+ ->each(fn(BackupDto $backup): ?BackupDto => $this->remove($backup->id));
return Storage::disk(config('backup.destination.disk'))->deleteDirectory(config('backup.destination.path'));
}
}
diff --git a/src/Restorer.php b/src/Restorer.php
index de29bd7..0547ed3 100644
--- a/src/Restorer.php
+++ b/src/Restorer.php
@@ -31,12 +31,12 @@ public function __construct(
*
* @throws Exception
*/
- public function restoreFromTimestamp(string $timestamp): void
+ public function restoreFromId(string $id): void
{
- $backup = $this->repository->find($timestamp);
+ $backup = $this->repository->find($id);
if (!$backup) {
- throw new RuntimeException("Backup with timestamp {$timestamp} not found.");
+ throw new RuntimeException("Backup with id {$id} not found.");
}
$this->restore($backup);
diff --git a/src/ServiceProvider.php b/src/ServiceProvider.php
index 5fb0caf..bbbb123 100644
--- a/src/ServiceProvider.php
+++ b/src/ServiceProvider.php
@@ -8,6 +8,7 @@
use Itiden\Backup\Console\Commands\BackupCommand;
use Itiden\Backup\Console\Commands\ClearFilesCommand;
use Itiden\Backup\Console\Commands\RestoreCommand;
+use Itiden\Backup\Contracts\BackupNameResolver;
use Itiden\Backup\Contracts\Repositories\BackupRepository;
use Itiden\Backup\Events\BackupDeleted;
use Statamic\Auth\Permissions as PermissionContract;
@@ -69,6 +70,7 @@ public function register(): void
$this->mergeConfigFrom(__DIR__ . '/../config/backup.php', 'backup');
$this->app->bind(BackupRepository::class, config('backup.repository'));
+ $this->app->bind(BackupNameResolver::class, config('backup.name_resolver'));
}
private function configureCommands(): void
diff --git a/src/Support/Chunky.php b/src/Support/Chunky.php
index 937a1fc..dc0fabb 100644
--- a/src/Support/Chunky.php
+++ b/src/Support/Chunky.php
@@ -4,6 +4,7 @@
namespace Itiden\Backup\Support;
+use Closure;
use Illuminate\Contracts\Filesystem\Filesystem;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Response;
@@ -12,6 +13,8 @@
use Itiden\Backup\DataTransferObjects\ChunkyUploadDto;
use SplFileInfo;
+use function Illuminate\Filesystem\join_paths;
+
final class Chunky
{
private Filesystem $disk;
@@ -33,9 +36,10 @@ public function path(?string $path = ''): string
}
/**
- * put a from≤ chunks.
+ * Store a chunk of a file. If all chunks are uploaded, merge them into a single file.
+ * @param ?Closure $onCompleted Callback to run when the file is fully uploaded.
*/
- public function put(ChunkyUploadDto $dto): JsonResponse
+ public function put(ChunkyUploadDto $dto, ?Closure $onCompleted = null): JsonResponse
{
if (!$this->disk->putFileAs($dto->path, $dto->file, $dto->filename . '.part' . $dto->currentChunk)) {
return response()->json(['message' => 'Error saving chunk'], Response::HTTP_INTERNAL_SERVER_ERROR);
@@ -54,7 +58,12 @@ public function put(ChunkyUploadDto $dto): JsonResponse
}
$completeFile = $this->mergeChunksIntoFile($dto->path, $dto->filename, $dto->totalChunks);
+
if ($completeFile) {
+ if ($onCompleted) {
+ $onCompleted($completeFile);
+ }
+
return response()->json(
['message' => 'File successfully uploaded', 'file' => $completeFile],
Response::HTTP_CREATED,
@@ -83,13 +92,15 @@ public function mergeChunksIntoFile(string $path, string $filename, int $totalCh
}
fclose($file);
+ $targetPath = join_paths('backups', $filename);
+
// move the file to the backups folder
- $this->disk->move($path . '/' . $filename, 'backups/' . $filename);
+ $this->disk->move(join_paths($path, $filename), $targetPath);
// delete the chunks
$this->disk->deleteDirectory($path);
- return $this->path('backups/' . $filename);
+ return $this->path($targetPath);
}
/**
diff --git a/src/Support/Facades/Chunky.php b/src/Support/Facades/Chunky.php
index 6af5c07..0fc42d9 100644
--- a/src/Support/Facades/Chunky.php
+++ b/src/Support/Facades/Chunky.php
@@ -10,9 +10,9 @@
use Itiden\Backup\Support\Chunky as ChunkySupport;
/**
- * @method static \Illuminate\Http\JsonResponse put(ChunkyUploadDto $dto)
+ * @method static \Illuminate\Http\JsonResponse put(ChunkyUploadDto $dto, ?Closure $onCompleted = null)
* @method static \Illuminate\Http\JsonResponse exists(ChunkyTestDto $dto)
- * @method static string path()
+ * @method static string path(string $path = '')
*
* @see \Itiden\Backup\Support\Chunky
*/
diff --git a/tests/Feature/BackupMetadataTest.php b/tests/Feature/BackupMetadataTest.php
index 58c1382..780ec2e 100644
--- a/tests/Feature/BackupMetadataTest.php
+++ b/tests/Feature/BackupMetadataTest.php
@@ -131,7 +131,7 @@
$metadata = $backup->getMetadata();
$metadata->addSkippedPipe(pipe: UserPipe::class, reason: 'Some reason');
- $file = File::get(config('backup.metadata_path') . '/.meta/' . $backup->timestamp);
+ $file = File::get(config('backup.metadata_path') . '/.meta/' . $backup->id);
$yaml = app(Yaml::class)->parse($file);
expect($file)->not->toBeEmpty();
diff --git a/tests/Feature/DeleteBackupTest.php b/tests/Feature/DeleteBackupTest.php
index b7a5c07..4108591 100644
--- a/tests/Feature/DeleteBackupTest.php
+++ b/tests/Feature/DeleteBackupTest.php
@@ -13,7 +13,7 @@
it('cant be deleted by a guest', function (): void {
$backup = Backuper::backup();
- $res = deleteJson(cp_route('api.itiden.backup.destroy', $backup->timestamp));
+ $res = deleteJson(cp_route('api.itiden.backup.destroy', $backup->id));
expect($res->status())->toBe(401);
expect(app(BackupRepository::class)->all())->toHaveCount(1);
@@ -24,7 +24,7 @@
actingAs(user());
- $res = deleteJson(cp_route('api.itiden.backup.destroy', $backup->timestamp));
+ $res = deleteJson(cp_route('api.itiden.backup.destroy', $backup->id));
expect($res->status())->toBe(403);
expect(app(BackupRepository::class)->all())->toHaveCount(1);
@@ -41,7 +41,7 @@
actingAs($user);
- $response = deleteJson(cp_route('api.itiden.backup.destroy', $backup->timestamp));
+ $response = deleteJson(cp_route('api.itiden.backup.destroy', $backup->id));
expect($response->status())->toBe(200);
expect($response->json('message'))->toBe('Deleted ' . $backup->name);
diff --git a/tests/Feature/DownloadBackupTest.php b/tests/Feature/DownloadBackupTest.php
index 3a22dd6..dc02d3a 100644
--- a/tests/Feature/DownloadBackupTest.php
+++ b/tests/Feature/DownloadBackupTest.php
@@ -14,7 +14,7 @@
it('cant be downloaded by a guest', function (): void {
$backup = Backuper::backup();
- $responseJson = getJson(cp_route('api.itiden.backup.download', $backup->timestamp));
+ $responseJson = getJson(cp_route('api.itiden.backup.download', $backup->id));
expect($responseJson->status())->toBe(401);
});
@@ -24,7 +24,7 @@
actingAs(user());
- $responseJson = getJson(cp_route('api.itiden.backup.download', $backup->timestamp));
+ $responseJson = getJson(cp_route('api.itiden.backup.download', $backup->id));
expect($responseJson->status())->toBe(403);
});
@@ -43,7 +43,7 @@
actingAs($user);
- $response = get(cp_route('api.itiden.backup.download', $backup->timestamp));
+ $response = get(cp_route('api.itiden.backup.download', $backup->id));
expect($response)->assertDownload();
})->with(['s3', 'local']);
@@ -59,7 +59,7 @@
actingAs($user);
- get(cp_route('api.itiden.backup.download', $backup->timestamp));
+ get(cp_route('api.itiden.backup.download', $backup->id));
$metadata = $backup->getMetadata();
diff --git a/tests/Feature/PreventsSimultaneousActionsTest.php b/tests/Feature/PreventsSimultaneousActionsTest.php
index b6a6e7e..1e9133a 100644
--- a/tests/Feature/PreventsSimultaneousActionsTest.php
+++ b/tests/Feature/PreventsSimultaneousActionsTest.php
@@ -5,7 +5,7 @@
use Illuminate\Support\Facades\Bus;
use Itiden\Backup\Jobs\BackupJob;
use Itiden\Backup\Jobs\RestoreFromPathJob;
-use Itiden\Backup\Jobs\RestoreFromTimestampJob;
+use Itiden\Backup\Jobs\RestoreJob;
use Statamic\Contracts\Auth\User;
use function Itiden\Backup\Tests\user;
@@ -39,34 +39,14 @@
actingAs($user);
postJson(cp_route('api.itiden.backup.restore', [
- 'timestamp' => now()->timestamp,
+ 'id' => now()->timestamp,
]));
postJson(cp_route('api.itiden.backup.restore', [
- 'timestamp' => now()->timestamp,
+ 'id' => now()->timestamp,
]))->assertServerError();
- Bus::assertDispatchedTimes(RestoreFromTimestampJob::class, 1);
- });
-
- it('prevents simultaneous restore from path jobs', function (): void {
- Bus::fake();
-
- $user = tap(user())
- ->assignRole('super admin')
- ->save();
-
- actingAs($user);
-
- postJson(cp_route('api.itiden.backup.restore-from-path'), [
- 'path' => 'test',
- ]);
-
- postJson(cp_route('api.itiden.backup.restore-from-path'), [
- 'path' => 'test',
- ])->assertServerError();
-
- Bus::assertDispatchedTimes(RestoreFromPathJob::class, 1);
+ Bus::assertDispatchedTimes(RestoreJob::class, 1);
});
it('prevents other actions when something is queued', function (): void {
@@ -79,21 +59,16 @@
actingAs($user);
postJson(cp_route('api.itiden.backup.restore', [
- 'timestamp' => now()->timestamp,
+ 'id' => now()->timestamp,
]));
postJson(cp_route('api.itiden.backup.store'))->assertServerError();
- postJson(cp_route('api.itiden.backup.restore-from-path'), [
- 'path' => 'test',
- ])->assertServerError();
-
postJson(cp_route('api.itiden.backup.restore', [
- 'timestamp' => now()->timestamp,
+ 'id' => now()->timestamp,
]))->assertServerError();
Bus::assertNotDispatched(BackupJob::class);
- Bus::assertNotDispatched(RestoreFromPathJob::class);
- Bus::assertDispatchedTimes(RestoreFromTimestampJob::class, 1);
+ Bus::assertDispatchedTimes(RestoreJob::class, 1);
});
});
diff --git a/tests/Feature/RestoreBackupTest.php b/tests/Feature/RestoreBackupTest.php
index d8bcdfc..2399794 100644
--- a/tests/Feature/RestoreBackupTest.php
+++ b/tests/Feature/RestoreBackupTest.php
@@ -15,16 +15,16 @@
use function Pest\Laravel\postJson;
describe('api:restore', function (): void {
- it('cant restore by timestamp by a guest', function (): void {
- $response = postJson(cp_route('api.itiden.backup.restore', 'timestamp'));
+ it('cant restore by id by a guest', function (): void {
+ $response = postJson(cp_route('api.itiden.backup.restore', 'id'));
expect($response->status())->toBe(Response::HTTP_UNAUTHORIZED);
});
- it('cant restore by timestamp by a user without permissons a backup', function (): void {
+ it('cant restore by id by a user without permissons a backup', function (): void {
actingAs(user());
- $response = postJson(cp_route('api.itiden.backup.restore', 'timestamp'));
+ $response = postJson(cp_route('api.itiden.backup.restore', 'id'));
expect($response->status())->toBe(Response::HTTP_FORBIDDEN);
});
@@ -38,12 +38,12 @@
actingAs($user);
- $response = postJson(cp_route('api.itiden.backup.restore', 'timestamp'));
+ $response = postJson(cp_route('api.itiden.backup.restore', 'id'));
expect($response->status())->toBe(Response::HTTP_INTERNAL_SERVER_ERROR);
});
- it('can restore by timestamp', function (): void {
+ it('can restore by id', function (): void {
$backup = Backuper::backup();
$user = user();
@@ -54,7 +54,7 @@
actingAs($user);
- $response = postJson(cp_route('api.itiden.backup.restore', $backup->timestamp));
+ $response = postJson(cp_route('api.itiden.backup.restore', $backup->id));
expect($response->status())->toBe(Response::HTTP_OK);
});
@@ -71,10 +71,10 @@
actingAs($user);
- $response = postJson(cp_route('api.itiden.backup.restore', $backup->timestamp));
+ $response = postJson(cp_route('api.itiden.backup.restore', $backup->id));
Event::assertDispatched(BackupRestored::class, function (BackupRestored $event) use ($backup): bool {
- return $event->backup->timestamp === $backup->timestamp;
+ return $event->backup->id === $backup->id;
});
expect($response->status())->toBe(Response::HTTP_OK);
});
@@ -116,7 +116,7 @@
actingAs($user);
- $response = postJson(cp_route('api.itiden.backup.restore', $backup->timestamp));
+ $response = postJson(cp_route('api.itiden.backup.restore', $backup->id));
expect($response->status())->toBe(Response::HTTP_OK);
expect($backup
diff --git a/tests/Feature/RestoreFromPathTest.php b/tests/Feature/RestoreFromPathTest.php
deleted file mode 100644
index 5b46632..0000000
--- a/tests/Feature/RestoreFromPathTest.php
+++ /dev/null
@@ -1,89 +0,0 @@
-assignRole('super admin')
- ->save();
-
- actingAs($user);
-
- $path = Storage::disk(config('backup.destination.disk'))->path($backup->path);
-
- $response = postJson(cp_route('api.itiden.backup.restore-from-path'), [
- 'path' => $path,
- ]);
-
- expect($response->status())->toBe(Response::HTTP_OK);
- });
-
- it('can restore from path and delete after', function (): void {
- $backup = Backuper::backup();
-
- $user = user();
-
- $user
- ->assignRole('super admin')
- ->save();
-
- actingAs($user);
-
- $path = Storage::disk(config('backup.destination.disk'))->path($backup->path);
-
- $response = postJson(cp_route('api.itiden.backup.restore-from-path'), [
- 'path' => $path,
- ]);
-
- expect($response->status())->toBe(Response::HTTP_OK);
- expect(File::exists($path))->toBeFalse();
- });
-
- it('will not restore empty archives and dispatches failed event', function (): void {
- Event::fake();
- $user = user();
-
- $emptyArchive = storage_path(config('backup.temp_path') . '/empty.zip');
-
- // The zip file cant be empty, but when extracting it can if the password is wrong.
- Zipper::write($emptyArchive)
- ->addFromString('empty.txt', 'empty')
- ->encrypt('not-the-password-we-decrypt-with')
- ->close();
-
- $user
- ->assignRole('super admin')
- ->save();
-
- actingAs($user);
-
- $response = postJson(cp_route('api.itiden.backup.restore-from-path'), [
- 'path' => $emptyArchive,
- ]);
-
- Event::assertDispatched(RestoreFailed::class);
-
- expect($response->status())->toBe(Response::HTTP_INTERNAL_SERVER_ERROR);
- });
-})
- ->group('restore-from-path')
- ->afterEach(function (): void {
- File::cleanDirectory(config('backup.temp_path'));
- });
diff --git a/tests/Feature/UploadControllerTest.php b/tests/Feature/UploadControllerTest.php
new file mode 100644
index 0000000..c132361
--- /dev/null
+++ b/tests/Feature/UploadControllerTest.php
@@ -0,0 +1,116 @@
+deleteDirectory(config('backup.destination.path'));
+
+ $backup = Backuper::backup();
+
+ $chunks = chunk_file(
+ file: Storage::disk(config('backup.destination.disk'))->path(join_paths($backup->path)),
+ path: config('backup.temp_path') . '/test-chunks/',
+ buffer: 512,
+ );
+
+ $totalSize = Storage::disk(config('backup.destination.disk'))->size(join_paths($backup->path));
+
+ $bodies = $chunks->map(fn(string $chunk, int $index): array => [
+ 'resumableIdentifier' => 'test-chunk-identifier',
+ 'resumableFilename' => $backup->name,
+ 'resumableTotalChunks' => $chunks->count(),
+ 'resumableChunkNumber' => $index + 1,
+ 'resumableTotalSize' => $totalSize,
+ 'file' => UploadedFile::fake()->createWithContent(basename($chunk), file_get_contents($chunk)),
+ ]);
+
+ $user = user();
+ $user->makeSuper();
+ $user->save();
+
+ actingAs($user);
+
+ $bodies
+ ->take($chunks->count() - 1)
+ ->each(function (array $values): void {
+ $res = postJson(cp_route('itiden.backup.chunky.upload'), $values);
+ $res->assertStatus(201);
+ $res->assertJsonStructure(['message']);
+ });
+
+ expect(app(BackupRepository::class)->all())->toHaveCount(1);
+
+ $res = postJson(cp_route('itiden.backup.chunky.upload'), $bodies->last());
+
+ $res->assertSuccessful();
+ expect(app(BackupRepository::class)->all())->toHaveCount(2);
+
+ File::cleanDirectory(Chunky::path());
+ File::cleanDirectory(config('backup.temp_path'));
+ app(BackupRepository::class)->empty();
+ });
+
+ it('can test if a chunk exists', function (): void {
+ File::cleanDirectory(config('backup.temp_path'));
+ Storage::disk(config('backup.destination.disk'))->deleteDirectory(config('backup.destination.path'));
+
+ $backup = Backuper::backup();
+
+ $chunks = chunk_file(
+ file: Storage::disk(config('backup.destination.disk'))->path(join_paths($backup->path)),
+ path: config('backup.temp_path') . '/test-chunks/',
+ buffer: 512,
+ );
+
+ $totalSize = Storage::disk(config('backup.destination.disk'))->size(join_paths($backup->path));
+
+ $bodies = $chunks->map(fn(string $chunk, int $index): array => [
+ 'resumableIdentifier' => 'test-chunk-identifier',
+ 'resumableFilename' => $backup->name,
+ 'resumableTotalChunks' => $chunks->count(),
+ 'resumableChunkNumber' => $index + 1,
+ 'resumableTotalSize' => $totalSize,
+ 'file' => UploadedFile::fake()->createWithContent(basename($chunk), file_get_contents($chunk)),
+ ]);
+
+ $user = user();
+ $user->makeSuper();
+ $user->save();
+
+ actingAs($user);
+
+ $chunksToTest = $bodies->take($chunks->count() - 1);
+
+ $chunksToTest->each(function (array $values): void {
+ $res = postJson(cp_route('itiden.backup.chunky.upload'), $values);
+ $res->assertStatus(201);
+ });
+
+ $chunksToTest->each(function (array $values): void {
+ $res = getJson(cp_route('itiden.backup.chunky.test', $values));
+ $res->assertStatus(200);
+ });
+
+ File::cleanDirectory(Chunky::path());
+ File::cleanDirectory(config('backup.temp_path'));
+ app(BackupRepository::class)->empty();
+ });
+});
diff --git a/tests/Feature/ViewBackupsTest.php b/tests/Feature/ViewBackupsTest.php
index 5448aa6..8461250 100644
--- a/tests/Feature/ViewBackupsTest.php
+++ b/tests/Feature/ViewBackupsTest.php
@@ -84,7 +84,7 @@
'size',
'path',
'created_at',
- 'timestamp',
+ 'id',
'metadata' => [
'created_by',
'downloads',
diff --git a/tests/Unit/BackupDtoTest.php b/tests/Unit/BackupDtoTest.php
index 8156f81..2623978 100644
--- a/tests/Unit/BackupDtoTest.php
+++ b/tests/Unit/BackupDtoTest.php
@@ -15,7 +15,7 @@
$backup = Backuper::backup();
- expect($backup->timestamp)->toBeString();
- expect($backup->timestamp)->toBe((string) $fakeTime->timestamp);
+ expect($backup->id)->toBeString();
+ expect($backup->created_at->timestamp)->toBe($fakeTime->timestamp);
});
})->group('backupdto');
diff --git a/tests/Unit/BackupRepositoryTest.php b/tests/Unit/BackupRepositoryTest.php
index b1f9ad1..571af5e 100644
--- a/tests/Unit/BackupRepositoryTest.php
+++ b/tests/Unit/BackupRepositoryTest.php
@@ -20,16 +20,16 @@
expect($backups->first())->toBeInstanceOf(BackupDto::class);
});
- it('can get backup by timestamp', function (): void {
+ it('can get backup by id', function (): void {
$backup = Backuper::backup();
- $backupByTimestamp = app(BackupRepository::class)->find($backup->timestamp);
+ $foundBackup = app(BackupRepository::class)->find($backup->id);
- expect($backupByTimestamp)->toBeInstanceOf(BackupDto::class);
- expect($backupByTimestamp->timestamp)->toBe($backup->timestamp);
- expect($backupByTimestamp)->toEqual($backup);
+ expect($foundBackup)->toBeInstanceOf(BackupDto::class);
+ expect($foundBackup->id)->toBe($backup->id);
+ expect($foundBackup)->toEqual($backup);
});
- it('returns null when timestamp doesnt exist', function (): void {
+ it('returns null when id doesnt exist', function (): void {
$backup = app(BackupRepository::class)->find('1234567890');
expect($backup)->toBeNull();
});
@@ -48,7 +48,7 @@
Event::fake();
$backup = Backuper::backup();
- app(BackupRepository::class)->remove($backup->timestamp);
+ app(BackupRepository::class)->remove($backup->id);
Event::assertDispatched(BackupDeleted::class);
});
@@ -61,14 +61,23 @@
expect(Storage::disk('local')->files(storage_path('statamic-backup/.metadata')))->toBeEmpty();
});
- it('can delete backup by timestamp', function (): void {
+ it('can delete backup by id', function (): void {
$backup = Backuper::backup();
- $backup = app(BackupRepository::class)->remove($backup->timestamp);
+ $backup = app(BackupRepository::class)->remove($backup->id);
expect($backup)->toBeInstanceOf(BackupDto::class);
expect(Storage::disk(config('backup.destination.disk'))->exists(
config('backup.destination.path') . "/{$backup->name}.zip",
))->toBeFalse();
});
+
+ it('returns null and doesnt dispatch event when backup doesnt exist', function (): void {
+ Event::fake();
+
+ $backup = app(BackupRepository::class)->remove('1234567890');
+
+ expect($backup)->toBeNull();
+ Event::assertNotDispatched(BackupDeleted::class);
+ });
})->group('backuprepository');
diff --git a/tests/Unit/BackuperTest.php b/tests/Unit/BackuperTest.php
index c71cf18..ddbf2af 100644
--- a/tests/Unit/BackuperTest.php
+++ b/tests/Unit/BackuperTest.php
@@ -5,6 +5,7 @@
use Carbon\Carbon;
use Illuminate\Support\Facades\File;
use Illuminate\Support\Facades\Storage;
+use Itiden\Backup\Contracts\BackupNameResolver;
use Itiden\Backup\Contracts\Repositories\BackupRepository;
use Itiden\Backup\DataTransferObjects\BackupDto;
use Itiden\Backup\Facades\Backuper;
@@ -18,14 +19,22 @@
describe('backuper', function (): void {
it('can backup', function (): void {
- $this->withoutExceptionHandling();
+ Carbon::setTestNow(Carbon::now());
+
$backup = Backuper::backup();
+ $filename = app(BackupNameResolver::class)->generateFilename(Carbon::now()->toImmutable(), $backup->id);
+
expect($backup)->toBeInstanceOf(BackupDto::class);
- expect(Storage::disk(config('backup.destination.disk'))->exists(
- config('backup.destination.path') . "/{$backup->name}.zip",
- ))->toBeTrue();
+ expect(Storage::disk(config('backup.destination.disk'))->exists(str(
+ config('backup.destination.path') . "/{$filename}",
+ )->finish('.zip')))->toBeTrue();
+
+ expect(pathinfo(
+ Storage::disk(config('backup.destination.disk'))->path($backup->path),
+ PATHINFO_EXTENSION,
+ ))->toBe('zip');
});
it('backups correct files', function (): void {
diff --git a/tests/Unit/ChunkyTest.php b/tests/Unit/ChunkyTest.php
index 39365df..8d6f556 100644
--- a/tests/Unit/ChunkyTest.php
+++ b/tests/Unit/ChunkyTest.php
@@ -8,6 +8,7 @@
use Itiden\Backup\DataTransferObjects\ChunkyUploadDto;
use Itiden\Backup\Support\Facades\Chunky;
+use function Illuminate\Filesystem\join_paths;
use function Itiden\Backup\Tests\chunk_file;
describe('chunky', function (): void {
@@ -44,18 +45,27 @@
),
);
- $responses = $dtos->map(Chunky::put(...));
+ $uploadedFile = null;
+
+ $responses = $dtos->map(function (ChunkyUploadDto $r) use (&$uploadedFile): JsonResponse {
+ return Chunky::put($r, onCompleted: function (string $file) use (&$uploadedFile): void {
+ $uploadedFile = $file;
+ });
+ });
expect($responses->every(fn(JsonResponse $res): bool => $res->getStatusCode() === 201))->toBeTrue();
expect($responses
->last()
->getData(true))->toHaveKey('file');
- expect(Chunky::path() . '/backups/homepage.md')->toBeFile();
- expect(File::get(Chunky::path() . '/backups/homepage.md'))->toBe(File::get(
+ expect(Chunky::path('backups/homepage.md'))->toBeFile();
+
+ expect(File::get(Chunky::path('/backups/homepage.md')))->toBe(File::get(
__DIR__ . '/../__fixtures__/content/collections/pages/homepage.md',
));
+ expect($uploadedFile)->toEqual(Chunky::path('/backups/homepage.md'));
+
File::deleteDirectory(Chunky::path());
File::deleteDirectory(config('backup.temp_path') . '/chunks');
});
diff --git a/tests/Unit/RestorerTest.php b/tests/Unit/RestorerTest.php
index 41c3b58..bc6d6f9 100644
--- a/tests/Unit/RestorerTest.php
+++ b/tests/Unit/RestorerTest.php
@@ -15,14 +15,14 @@
use function Itiden\Backup\Tests\user;
describe('restorer', function (): void {
- it('can restore from timestamp', function (): void {
+ it('can restore from id', function (): void {
$backup = Backuper::backup();
File::cleanDirectory(fixtures_path('content/collections'));
expect(File::isEmptyDirectory(fixtures_path('content/collections')))->toBeTrue();
- Restorer::restoreFromTimestamp($backup->timestamp);
+ Restorer::restoreFromId($backup->id);
expect(File::isEmptyDirectory(fixtures_path('content/collections')))->toBeFalse();
});
@@ -57,10 +57,10 @@
Restorer::restore(
new BackupDto(
name: 'test',
- created_at: now(),
+ created_at: now()->toImmutable(),
size: '0',
path: 'test/path',
- timestamp: (string) now()->timestamp,
+ id: (string) random_bytes(10),
),
);
})->throws(RestoreFailed::class);