diff --git a/docs/pages/.vitepress/config.js b/docs/pages/.vitepress/config.js index c7536c7..1944b88 100644 --- a/docs/pages/.vitepress/config.js +++ b/docs/pages/.vitepress/config.js @@ -12,7 +12,10 @@ export default defineConfig({ sidebar: [ { text: "Introduction", - items: [{ text: "Getting started", link: "/getting-started.md" }], + items: [ + { text: "Getting started", link: "/getting-started.md" }, + { text: "Commands", link: "/commands.md" }, + ], }, { text: "Configuration", @@ -26,6 +29,7 @@ export default defineConfig({ items: [ { text: "Pipeline", link: "/pipeline.md" }, { text: "Notifications", link: "/notifications.md" }, + { text: "Metadata", link: "/metadata.md" }, ], }, ], diff --git a/docs/pages/commands.md b/docs/pages/commands.md new file mode 100644 index 0000000..20f177b --- /dev/null +++ b/docs/pages/commands.md @@ -0,0 +1,9 @@ +# Commands + +Statamic backup comes with a few commands to help you manage backups from a cli. + +| Command | Description | +| ---------------------------------------- | ------------------------------------------------------------ | +| `php artisan statamic:backup` | Run the backup process | +| `php artisan statamic:backup:temp-clean` | Clean up leftover temporary backup files, like upload chunks | +| `php artisan statamic:backup:restore` | Restore your site to chosen backup | diff --git a/phpunit.xml b/phpunit.xml index e781157..f78b591 100644 --- a/phpunit.xml +++ b/phpunit.xml @@ -18,6 +18,7 @@ + diff --git a/src/Console/Commands/BackupCommand.php b/src/Console/Commands/BackupCommand.php index 81c2334..d829c66 100644 --- a/src/Console/Commands/BackupCommand.php +++ b/src/Console/Commands/BackupCommand.php @@ -7,6 +7,8 @@ use Illuminate\Console\Command; use Itiden\Backup\Facades\Backuper; +use function Laravel\Prompts\{info, spin}; + /** * Backup site */ @@ -14,14 +16,12 @@ class BackupCommand extends Command { protected $signature = 'statamic:backup'; - protected $description = 'Backup your stuff'; + protected $description = 'Run the backup pipeline'; public function handle() { - $this->components->info('Backing up content'); - - $backup_location = Backuper::backup(); + $backup = spin(fn () => Backuper::backup(), 'Backing up...'); - $this->components->info('Backup saved to ' . $backup_location->path); + info('Backup saved to ' . $backup->path); } } diff --git a/src/Console/Commands/ClearFilesCommand.php b/src/Console/Commands/ClearFilesCommand.php index 9f15bc6..141e7a4 100644 --- a/src/Console/Commands/ClearFilesCommand.php +++ b/src/Console/Commands/ClearFilesCommand.php @@ -7,25 +7,27 @@ use Illuminate\Console\Command; use Illuminate\Support\Facades\File; +use function Laravel\Prompts\info; + /** * Clear the backup temp directory */ class ClearFilesCommand extends Command { - protected $signature = 'statamic:backup:clear'; + protected $signature = 'statamic:backup:temp-clear'; - protected $description = 'Empty the temp directory'; + protected $description = 'Clear the backup temp directory'; public function handle() { - if (! File::exists(config('backup.temp_path'))) { - $this->components->info('Backup temp directory does not exist, no need to clear it.'); + if (!File::exists(config('backup.temp_path'))) { + info('Backup temp directory does not exist, no need to clear it.'); return; } File::cleanDirectory(config('backup.temp_path')); - $this->components->info('Backup temp directory cleared successfully'); + info('Backup temp directory cleared successfully'); } } diff --git a/src/Console/Commands/RestoreCommand.php b/src/Console/Commands/RestoreCommand.php index 0882f2b..d54520b 100644 --- a/src/Console/Commands/RestoreCommand.php +++ b/src/Console/Commands/RestoreCommand.php @@ -5,30 +5,47 @@ namespace Itiden\Backup\Console\Commands; use Illuminate\Console\Command; -use Illuminate\Contracts\Console\PromptsForMissingInput; +use Itiden\Backup\Contracts\Repositories\BackupRepository; use Itiden\Backup\DataTransferObjects\BackupDto; use Itiden\Backup\Facades\Restorer; +use function Laravel\Prompts\{confirm, spin, info, select}; + /** * Restore content from a directory / backup */ -class RestoreCommand extends Command implements PromptsForMissingInput +final class RestoreCommand extends Command { - protected $signature = 'statamic:backup:restore {path} {--force}'; + protected $signature = 'statamic:backup:restore {--path=} {--force}'; protected $description = 'Reset or restore content from a directory / backup'; - protected function promptForMissingArgumentsUsing() + public function handle(BackupRepository $repo) { - return [ - 'path' => 'Which filepath does your backup have?', - ]; - } + /* @var BackupDto $backup */ + $backup = match (true) { + (bool) $this->option('path') => BackupDto::fromAbsolutePath($this->option('path')), + default => BackupDto::fromFile(select( + label: 'Which backup do you want to restore to?', + scroll: 10, + options: $repo->all()->flatMap( + fn (BackupDto $backup) => [$backup->path => $backup->path] + ), + required: true + )), + }; - public function handle() - { - if ($this->option('force') || $this->confirm('Are you sure you want to restore your content?')) { - Restorer::restore(BackupDto::fromAbsolutePath($this->argument('path'))); + if ( + $this->option('force') + || confirm( + label: "Are you sure you want to restore your content?", + hint: "This will overwrite your current content with state from {$backup->created_at->format('Y-m-d H:i:s')}", + required: true + ) + ) { + spin(fn () => Restorer::restore($backup), 'Restoring backup'); + + info('Backup restored!'); } } } diff --git a/tests/Feature/ClearTempTest.php b/tests/Feature/ClearTempTest.php index c7dcbd3..3f50405 100644 --- a/tests/Feature/ClearTempTest.php +++ b/tests/Feature/ClearTempTest.php @@ -9,7 +9,7 @@ File::deleteDirectory($temp_path); - $this->artisan('statamic:backup:clear')->assertExitCode(0); + $this->artisan('statamic:backup:temp-clear')->assertExitCode(0); }); it("will clear temp path when running backup clear command", function () { @@ -21,7 +21,7 @@ expect(File::allFiles($temp_path))->toHaveCount(1); - $this->artisan('statamic:backup:clear'); + $this->artisan('statamic:backup:temp-clear'); expect(File::allFiles($temp_path))->toHaveCount(0); }); diff --git a/tests/Feature/RestoreBackupTest.php b/tests/Feature/RestoreBackupTest.php index 14b0390..35c061f 100644 --- a/tests/Feature/RestoreBackupTest.php +++ b/tests/Feature/RestoreBackupTest.php @@ -75,19 +75,19 @@ File::cleanDirectory(config('backup.content_path')); - $this->artisan('statamic:backup:restore', ['path' => Storage::path($backup->path)]) + $this->artisan('statamic:backup:restore', ['--path' => Storage::path($backup->path)]) ->expectsConfirmation('Are you sure you want to restore your content?', 'no'); expect(File::isEmptyDirectory(config('backup.content_path')))->toBeTrue(); - $this->artisan('statamic:backup:restore', ['path' => Storage::path($backup->path), '--force' => true]) + $this->artisan('statamic:backup:restore', ['--path' => Storage::path($backup->path), '--force' => true]) ->assertExitCode(0); }); it('can restore from path command', function () { $backup = Backuper::backup(); - $this->artisan('statamic:backup:restore', ['path' => Storage::path($backup->path), '--force' => true]) + $this->artisan('statamic:backup:restore', ['--path' => Storage::path($backup->path), '--force' => true]) ->assertExitCode(0); expect(File::isEmptyDirectory(config('backup.content_path')))->toBeFalse(); diff --git a/tests/Feature/RestoreCommandTest.php b/tests/Feature/RestoreCommandTest.php new file mode 100644 index 0000000..54441d7 --- /dev/null +++ b/tests/Feature/RestoreCommandTest.php @@ -0,0 +1,35 @@ +group('restore-command'); + +it('shows all available backups', function () { + app(BackupRepository::class)->empty(); + + Backuper::backup(); + + $backups = app(BackupRepository::class)->all(); + + artisan('statamic:backup:restore') + ->expectsQuestion( + question: 'Which backup do you want to restore to?', + answer: $backups->first()->path + ) + ->expectsConfirmation('Are you sure you want to restore your content?') + ->assertFailed(); +}); + +it('can restore from a specific path', function () { + app(BackupRepository::class)->empty(); + + $backup = Backuper::backup(); + + artisan('statamic:backup:restore', ['--path' => Storage::disk(config('backup.destination.disk'))->path($backup->path)]) + ->expectsConfirmation('Are you sure you want to restore your content?') + ->assertFailed(); +}); diff --git a/tests/Unit/PipeTest.php b/tests/Unit/PipeTest.php index 791ba98..dbc07f2 100644 --- a/tests/Unit/PipeTest.php +++ b/tests/Unit/PipeTest.php @@ -43,7 +43,7 @@ Assets::class, ]); -test('can skip a pipe', function () { +test('can skip a pipe with users', function () { /** @var Users::class $pipe */ $pipe = app()->make(Users::class); @@ -63,3 +63,28 @@ $zipper->close(); }); + +test('can skip a pipe with content', function () { + /** @var Users::class $pipe */ + $pipe = app()->make(Content::class); + + $callable = function ($z) { + return $z; + }; + + File::copyDirectory(config('backup.content_path'), config('backup.content_path') . '_backup'); + File::deleteDirectory(config('backup.content_path')); + + $zipper = Zipper::open(config('backup.temp_path') . '/backup.zip'); + + $pipe->backup(zip: $zipper, next: $callable); + + + expect($zipper->getMeta())->toHaveKey(Content::class); + expect($zipper->getMeta()[Content::class])->toHaveKey('skipped', 'Content directory didn\'t exist, is it configured correctly?'); + + $zipper->close(); + + File::copyDirectory(config('backup.content_path') . '_backup', config('backup.content_path')); + File::deleteDirectory(config('backup.content_path') . '_backup'); +});