purusottampanta/laravel-gdrive-backup

Automatic incremental + weekly full backups of Eloquent models to a personal Google Drive account via OAuth2, driven entirely by config.

Maintainers

Package info

github.com/purusottampanta/laravel-gdrive-backup

pkg:composer/purusottampanta/laravel-gdrive-backup

Transparency log

Statistics

Installs: 16

Dependents: 0

Suggesters: 0

Stars: 0

Open Issues: 0

v1.0.2 2026-08-09 14:17 UTC

This package is auto-updated.

Last update: 2026-08-09 14:21:58 UTC


README

Automatic, config-driven backups of your Eloquent models to a personal Google Drive account (OAuth2, not a service account):

  • Every created / updated / deleted on a configured model queues an incremental JSON event, uploaded to backups/incremental/{date}/.
  • A weekly full snapshot exports every configured table as JSON Lines to backups/full/{date}/, then prunes old snapshots and superseded incremental cycles.
  • A restore command replays the latest full snapshot plus every incremental event since, idempotently.
  • One config flag turns the whole thing on or off — disabled means observers are never attached and nothing is scheduled, not just a runtime no-op.

Requirements

  • PHP 8.2+
  • Laravel 10
  • QUEUE_CONNECTION=database (or any queue driver — jobs are queued, never run sync)

Installation

composer require purusottampanta/laravel-gdrive-backup

Laravel's package auto-discovery registers Puru\GdriveBackup\Providers\BackupServiceProvider automatically — no manual provider registration needed.

Publish the config file:

php artisan vendor:publish --tag=gdrive-backup-config

This creates config/backup.php in your app. Migrations for the database queue (jobs, job_batches, failed_jobs tables) and the OAuth routes are auto-loaded by the package — you don't need to publish them unless you want to customize them, in which case:

php artisan vendor:publish --tag=gdrive-backup-migrations
php artisan vendor:publish --tag=gdrive-backup-routes
php artisan migrate

Configuration

Everything lives in config/backup.php and environment variables.

# .env

BACKUP_ENABLED=true

QUEUE_CONNECTION=database

GOOGLE_DRIVE_CLIENT_ID=
GOOGLE_DRIVE_CLIENT_SECRET=
GOOGLE_DRIVE_REDIRECT_URI=https://your-app.example.com/backup/google/callback
GOOGLE_DRIVE_HTTP_TIMEOUT=30

BACKUP_ROOT_FOLDER=backups
BACKUP_CHUNK_SIZE=500
BACKUP_KEEP_WEEKLY_SNAPSHOTS=4
BACKUP_WEEKLY_CRON="0 2 * * 0"
BACKUP_RETRY_CRON="*/15 * * * *"

The single on/off switch

// config/backup.php
'enabled' => (bool) env('BACKUP_ENABLED', true),

Set BACKUP_ENABLED=false in any environment (e.g. local, staging, a throwaway QA box) and:

  • Model observers are never attachedBackupServiceProvider skips Model::observe() entirely at boot, so create/update/delete on your models has zero backup-related overhead, not just a skipped upload.
  • The weekly snapshot and failed-upload-retry schedules are never registered with the Laravel scheduler.
  • backup:snapshot, backup:restore, and backup:retry-failed refuse to run and print a warning, unless you pass --force.
  • Any job already on the queue from before you flipped the switch off will log and no-op instead of calling the Google API.

Flip it back to true and everything re-attaches on the next request/deploy — no code changes required.

Registering your models

// config/backup.php
'models' => [
    \App\Models\Consignment::class => 'consignments',
    \App\Models\Box::class => 'boxes',
    \App\Models\Item::class => 'items',
    \App\Models\Location::class => 'locations',
    \App\Models\LabelRequest::class => 'label_requests',
    \App\Models\ThirdPartyDetail::class => 'third_party_details',
    \App\Models\Bill::class => 'bills',
    \App\Models\BillDetail::class => 'bill_details',
],

Add or remove entries here per project — nothing else needs to change. Optionally implement the marker interface for type clarity:

final class Consignment extends Model implements \Puru\GdriveBackup\Contracts\Backupable
{
    public function backupTableName(): string
    {
        return 'consignments';
    }
}

Google OAuth setup

  1. In Google Cloud Console, create an OAuth 2.0 Client ID (type "Web application") with scope https://www.googleapis.com/auth/drive.file and the redirect URI from GOOGLE_DRIVE_REDIRECT_URI above.
  2. Restrict routes/backup.php's auth middleware (or add your own gate) so only a trusted operator can authorize/re-authorize access — completing the flow grants Drive access under their personal account.
  3. Visit /backup/google/redirect while logged in as that operator and complete Google's consent screen.
  4. The resulting token is encrypted (Laravel Crypt, so it's tied to your APP_KEY) and stored at storage/app/google/oauth.enc — never in the database. It refreshes itself automatically thereafter.

Running it

# Queue worker (any host that allows a long-running process, or a
# cron-triggered worker on shared hosting):
php artisan queue:work --queue=backups

# Single cron entry drives the scheduler (weekly snapshot + retry sweep):
* * * * * php /path/to/artisan schedule:run >> /dev/null 2>&1

Manual commands:

php artisan backup:snapshot [--sync] [--force]
php artisan backup:restore [--date=YYYY-MM-DD] [--force]
php artisan backup:retry-failed [--force]

Testing

The package ships with a full PHPUnit + Orchestra Testbench suite that never touches the real Google API — all Drive calls go through an in-memory fake (tests/Support/FakeGoogleDriveClient.php), and OAuth HTTP calls are covered separately using Laravel's Http::fake().

composer install
composer test
# or directly:
vendor/bin/phpunit
# single file / filter:
vendor/bin/phpunit tests/Unit/Services/RestoreServiceTest.php
vendor/bin/phpunit --filter test_restore_is_idempotent

What's covered:

  • DTOsIncrementalEventDTO (filename generation, JSON round-trip), OAuthTokenDTO (expiry/leeway logic, immutable refresh).
  • BackupFolderResolver — folder hierarchy creation and caching (no duplicate folders on repeated calls).
  • IncrementalBackupService — uploads land in the correct dated folder with exact JSON contents.
  • FullBackupService — chunked JSONL export, including the zero-records case.
  • RetentionService — keeps exactly N full snapshots, prunes superseded incremental day-folders, cascades deletes to children.
  • RestoreService — full snapshot restore, chronological incremental replay (independent of upload order), idempotency, "no snapshot available" failure, "latest snapshot" resolution.
  • GoogleOAuthService — consent URL construction, code exchange, encrypted-at-rest token persistence (asserts the raw file never contains the plaintext token), cached-vs-refreshed access token logic, missing-refresh-token failure.
  • The enable/disable switch — a dedicated test class (tests/Feature/DisabledSwitchTest.php) boots the app with backup.enabled = false already set, then asserts the observer is never attached (no job dispatched on model changes) and commands refuse to run without --force — verifying the boot-time behavior, not just a runtime short-circuit.
  • BackupableObserver — dispatches the right job with the right payload on create/update/delete, and is silent for unregistered models.
  • JobsUploadIncrementalBackupJob skips work while disabled, uploads correctly while enabled, and persists a replayable local file in failed(); RetryFailedUploadsJob re-dispatches and clears persisted failures, leaving malformed files alone for manual review.
  • Artisan commandsbackup:snapshot, backup:restore (including the confirmation prompt, --force, --date validation, and a full restore run against the fake Drive), and backup:retry-failed.
  • Service provider — config merging and the Drive client singleton binding.

Run this before every tag/release, and definitely before your first composer require your-vendor/gdrive-backup in a real project.

Publishing this package yourself

To ship this under your own name on GitHub + Packagist:

  1. Rename the Puru\GdriveBackup namespace throughout src/ (and in composer.json's autoload.psr-4 / extra.laravel.providers) to your own vendor namespace, e.g. YourOrg\GdriveBackup.
  2. Update composer.json's name field to your-vendor/gdrive-backup and the authors block.
  3. git init, commit, push to a new GitHub repo named to match (e.g. your-vendor/gdrive-backup).
  4. Tag a release: git tag v1.0.0 && git push --tags.
  5. On packagist.org, "Submit" the GitHub repo URL. Enable the GitHub Service Hook (Packagist prompts for this) so new tags auto-publish.
  6. In any project: composer require your-vendor/gdrive-backup.

For private/internal use without Packagist, add a VCS repository entry to the consuming project's composer.json instead:

{
    "repositories": [
        { "type": "vcs", "url": "https://github.com/your-vendor/gdrive-backup" }
    ]
}

License

MIT