Skip to content

[add-resource] New Plugin: Google Drive Backup (gdrive-backup) #4349

Description

@sandymac

Please add this plugin to GPM.

What it does: uploads Grav's own backups to Google Drive and keeps a rotating set there, modelled on the Home Assistant Google Drive Backup add-on. After a scheduled backup it syncs straight away in the CLI; a backup made with the admin's Backup now is left to the scheduler job gdrive-backup-sync (default 15 * * * *), so the web request never waits on an upload. Each sync works out retention over the local and Drive copies together, by the timestamp in the filename: the newest keep plus the newest backup in each of the last N days, ISO weeks, months and years. Only local backups that survive are uploaded, so a backup rotated off Drive is never re-uploaded. Google sign-in comes from Google Drive Auth: an OAuth account with drive.file and a folder the plugin creates itself ("Grav backups ()"), or any account with full drive and a folder link or id you paste (service accounts need a Shared Drive folder). A trashed or missing folder is replaced, with a warning, never restored or deleted.

The Admin2 settings page shows the last sync from user/data/gdrive-backup/status.json, which backup profiles are scheduled (with a warning while they include the site's Google sign-in), a gdrive-folder custom field with a live hint (parsed id, scope warning before saving), and a one-line check of what the next sync will do with the saved folder (5-second limit, cached 5 minutes). It registers no API routes. No Composer dependencies.

Security notes (SECURITY.md has the full list): only an admin with api.gdrive.manage (or api.super) can change the account and folder; the check is server-side in onAdminSave, so a plain api.config.write holder can't redirect the backup zips, which hold user/accounts and config, to a folder they control. Only files the plugin tagged (appProperties grav_backup=1) are ever candidates; starred files are never trashed; old copies go to Drive's trash, never a permanent delete; every upload's md5Checksum is checked against md5_file() and a failed upload holds back retention for the run; a non-blocking flock stops overlapping runs. The folder setting is parsed to a Drive id, URL-encoded in paths and quoted in queries; links on the settings page are built only from ids filtered to [A-Za-z0-9_-], the custom field escapes everything it renders and opens only drive.google.com links; nothing stored or cached comes from the Host header (the site name follows system.custom_base_url); the sync catches every Throwable so a plugin error can't break the backup that just ran or the scheduler; the folder check is not on the /data/resolve allowlist, so page editors can't reach it.

Testing: tests/smoke.php (the library's real Drive client over a fake transport, no network: retention buckets across day, week, month and year boundaries, filename parsing, upload-only-survivors, the starred/untagged/trash-only invariants, md5 mismatch and re-upload, folder resolution and the folder check branch for branch, the folder setting parser, the account/folder guard, the scheduled-profiles notice, the lock, version drift), PHPStan level 6 against Grav 2.0.23 / api 1.0.41 and Grav 2.2.4 / api 1.0.44, PHP 8.3 and 8.4, node --check on the custom field, yamllint, all in CI. Deployed on a production site running Grav 2.2.4, api 1.0.44, admin2 2.1.27, PHP 8.3: the settings page renders, and a sync uploaded a fresh backup with a verified checksum and applied retention.

Note: gdrive-images, the other plugin built on Google Drive Auth, is still private and will be submitted separately.

Activity

  1. rhukster commented on Oct 7, 2026

    @rhukster
    Member

    Added to GPM. Thanks.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions