🙌

spatie/laravel-backup 使用例

2021/05/10に公開

https://github.com/spatie/laravel-backup

dependency
https://github.com/spatie/db-dumper

laravel-backup

構成

vendor/spatie/laravel-backup/src
├── BackupDestination
│   ├── Backup.php
│   ├── BackupCollection.php
│   ├── BackupDestination.php
│   └── BackupDestinationFactory.php
├── BackupServiceProvider.php
├── Commands
│   ├── BackupCommand.php
│   ├── BaseCommand.php
│   ├── CleanupCommand.php
│   ├── ListCommand.php
│   └── MonitorCommand.php
├── Events
│   ├── BackupHasFailed.php
│   ├── BackupManifestWasCreated.php
│   ├── BackupWasSuccessful.php
│   ├── BackupZipWasCreated.php
│   ├── CleanupHasFailed.php
│   ├── CleanupWasSuccessful.php
│   ├── HealthyBackupWasFound.php
│   └── UnhealthyBackupWasFound.php
├── Exceptions
│   ├── CannotCreateDbDumper.php
│   ├── InvalidBackupDestination.php
│   ├── InvalidBackupJob.php
│   ├── InvalidCommand.php
│   ├── InvalidConfiguration.php
│   ├── InvalidHealthCheck.php
│   └── NotificationCouldNotBeSent.php
├── Helpers
│   ├── ConsoleOutput.php
│   ├── File.php
│   ├── Format.php
│   ├── RightAlignedTableStyle.php
│   └── functions.php
├── Listeners
│   └── EncryptBackupArchive.php
├── Notifications
│   ├── BaseNotification.php
│   ├── EventHandler.php
│   ├── Notifiable.php
│   └── Notifications
│       ├── BackupHasFailed.php
│       ├── BackupWasSuccessful.php
│       ├── CleanupHasFailed.php
│       ├── CleanupWasSuccessful.php
│       ├── HealthyBackupWasFound.php
│       └── UnhealthyBackupWasFound.php
└── Tasks
    ├── Backup
    │   ├── BackupJob.php
    │   ├── BackupJobFactory.php
    │   ├── DbDumperFactory.php
    │   ├── FileSelection.php
    │   ├── Manifest.php
    │   └── Zip.php
    ├── Cleanup
    │   ├── CleanupJob.php
    │   ├── CleanupStrategy.php
    │   ├── Period.php
    │   └── Strategies
    └── Monitor
        ├── BackupDestinationStatus.php
        ├── BackupDestinationStatusFactory.php
        ├── HealthCheck.php
        ├── HealthCheckFailure.php
        └── HealthChecks

使用例

リスト、バックアップ、バックアップ削除

以下を表現

  • バックアップファイル情報のリスト
  • バックアップ
  • ハックアップファイル削除

https://github.com/bytefury/crater/blob/master/app/Http/Controllers/V1/Backup/BackupsController.php

<?php
// Implementation taken from nova-backup-tool - https://github.com/spatie/nova-backup-tool/

namespace Crater\Http\Controllers\V1\Backup;

use Illuminate\Http\Request;
use Illuminate\Support\Facades\Cache;
use Spatie\Backup\BackupDestination\Backup;
use Spatie\Backup\BackupDestination\BackupDestination;
use Spatie\Backup\Helpers\Format;
use Crater\Jobs\CreateBackupJob;
use Crater\Rules\Backup\BackupDisk;
use Crater\Rules\Backup\PathToZip;
use Illuminate\Http\JsonResponse;

class BackupsController extends ApiController
{
    /**
     * Display a listing of the resource.
     *
     * @return JsonResponse
     */
    public function index(Request $request)
    {
        $configuredBackupDisks = config('backup.backup.destination.disks');

        try {
	    // BackupDestination = バックアップファイルを表現するクラス。保存機能もこのクラスが担当している。
	    // ここではバックアップしたファイルの情報をコレクションで取得している
	    // ページネーションは考慮していないはず。
            $backupDestination = BackupDestination::create(config('filesystems.default'), config('backup.backup.name'));

            $backups = Cache::remember("backups-{$request->file_disk_id}", now()->addSeconds(4), function () use ($backupDestination) {
                return $backupDestination
                    ->backups()
                    ->map(function (Backup $backup) {
                        return [
                            'path' => $backup->path(),
                            'created_at' => $backup->date()->format('Y-m-d H:i:s'),
                            'size' => Format::humanReadableSize($backup->size()),
                        ];
                    })
                    ->toArray();
            });

            return response()->json([
                'backups' => $backups,
                'disks' => $configuredBackupDisks
            ]);
        } catch (\Exception $e) {
            return response()->json([
                'backups' => [],
                'error' => 'invalid_disk_credentials',
                'error_message' => $e->getMessage(),
                'disks' => $configuredBackupDisks
            ]);
        }
    }

    /**
     * Store a newly created resource in storage.
     *
     * @param  \Illuminate\Http\Request  $request
     * @return JsonResponse
     */
    public function store(Request $request)
    {
        // CreateBackupJobのコードを下に記載
        dispatch(new CreateBackupJob($request->all()))->onQueue(config('backup.queue.name'));

        return $this->respondSuccess();
    }

    /**
     * Remove the specified resource from storage.
     *
     * @param  \Illuminate\Http\Request  $request
     * @return JsonResponse
     */
    public function destroy($disk, Request $request)
    {
        $validated = $request->validate([
            'path' => ['required', new PathToZip()],
        ]);

        $backupDestination = BackupDestination::create(config('filesystems.default'), config('backup.backup.name'));

        $backupDestination
            ->backups()
            ->first(function (Backup $backup) use ($validated) {
                return $backup->path() === $validated['path'];
            })
            ->delete();

        return $this->respondSuccess();
    }
}

バックアップ

https://github.com/bytefury/crater/blob/master/app/Jobs/CreateBackupJob.php

<?php

namespace Crater\Jobs;

use Crater\Models\FileDisk;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use Spatie\Backup\Tasks\Backup\BackupJobFactory;

class CreateBackupJob implements ShouldQueue
{
    use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;

    protected $data;

    /**
     * Create a new job instance.
     *
     * @return void
     */
    public function __construct($data = '')
    {
       $this->data = $data;
    }

    /**
     * Execute the job.
     *
     * @return void
     */
    public function handle()
    {
        $fileDisk = FileDisk::find($this->data['file_disk_id']);
        $fileDisk->setConfig();

        $prefix = env('DYNAMIC_DISK_PREFIX', 'temp_');

        config(['backup.backup.destination.disks' => [$prefix . $fileDisk->driver]]);

        $backupJob = BackupJobFactory::createFromArray(config('backup'));

        if ($this->data['option'] === 'only-db') {
            $backupJob->dontBackupFilesystem();
        }

        if ($this->data['option'] === 'only-files') {
            $backupJob->dontBackupDatabases();
        }

        if (! empty($this->data['option'])) {
            $prefix = str_replace('_', '-', $this->data['option']).'-';

            $backupJob->setFilename($prefix.date('Y-m-d-H-i-s').'.zip');
        }

        // コード下に記載
        $backupJob->run();
    }
}

https://github.com/spatie/laravel-backup/blob/master/src/Tasks/Backup/BackupJob.php

// https://github.com/spatie/laravel-backup/blob/61296bfd8ada78893f926ab6df47d56e0ea3b419/src/Tasks/Backup/BackupJob.php#L128
    public function run()
    {
        $temporaryDirectoryPath = config('backup.backup.temporary_directory') ?? storage_path('app/backup-temp');

        $this->temporaryDirectory = (new TemporaryDirectory($temporaryDirectoryPath))
            ->name('temp')
            ->force()
            ->create()
            ->empty();

        try {
            if (! count($this->backupDestinations)) {
                throw InvalidBackupJob::noDestinationsSpecified();
            }

            $manifest = $this->createBackupManifest();

            if (! $manifest->count()) {
                throw InvalidBackupJob::noFilesToBeBackedUp();
            }

            $zipFile = $this->createZipContainingEveryFileInManifest($manifest);

            $this->copyToBackupDestinations($zipFile);
        } catch (Exception $exception) {
            consoleOutput()->error("Backup failed because {$exception->getMessage()}.".PHP_EOL.$exception->getTraceAsString());

            $this->sendNotification(new BackupHasFailed($exception));

            $this->temporaryDirectory->delete();

            throw $exception;
        }

        $this->temporaryDirectory->delete();
    }

バックアップファイルダウンロード

https://github.com/bytefury/crater/blob/master/app/Http/Controllers/V1/Backup/DownloadBackupController.php

<?php
// Implementation taken from nova-backup-tool - https://github.com/spatie/nova-backup-tool/

namespace Crater\Http\Controllers\V1\Backup;

use Crater\Http\Controllers\Controller;
use Illuminate\Http\Request;
use Spatie\Backup\BackupDestination\Backup;
use Spatie\Backup\BackupDestination\BackupDestination;
use Crater\Rules\Backup\BackupDisk;
use Crater\Rules\Backup\PathToZip;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpFoundation\StreamedResponse;

class DownloadBackupController extends ApiController
{
    public function __invoke(Request $request)
    {
        $validated = $request->validate([
            'path' => ['required', new PathToZip()],
        ]);

        $backupDestination = BackupDestination::create(config('filesystems.default'), config('backup.backup.name'));

        $backup = $backupDestination->backups()->first(function (Backup $backup) use ($validated) {
            return $backup->path() === $validated['path'];
        });

        if (! $backup) {
            return response('Backup not found', Response::HTTP_UNPROCESSABLE_ENTITY);
        }

        return $this->respondWithBackupStream($backup);
    }

    public function respondWithBackupStream(Backup $backup): StreamedResponse
    {
        $fileName = pathinfo($backup->path(), PATHINFO_BASENAME);

        $downloadHeaders = [
            'Cache-Control' => 'must-revalidate, post-check=0, pre-check=0',
            'Content-Type' => 'application/zip',
            'Content-Length' => $backup->size(),
            'Content-Disposition' => 'attachment; filename="'.$fileName.'"',
            'Pragma' => 'public',
        ];

        return response()->stream(function () use ($backup) {
            $stream = $backup->stream();

            fpassthru($stream);

            if (is_resource($stream)) {
                fclose($stream);
            }
        }, 200, $downloadHeaders);
    }
}

Discussion