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.
Please add this plugin to GPM.
gdrive-backupversion:inblueprints.yaml)grav: ['2.0']; dependenciesgrav >=2.0.23,gdrive-auth >=1.0.0(already in GPM, [add-resource] New Plugin: Google Drive Auth (gdrive-auth) #4348),api >=1.0.41,admin2 >=2.1.24; PHP 8.3+gdrive-backup-1.0.0.zip(git archive of the tag, dev files stripped), SHA-2566aea636ba78e75f16ed0e45782920e77beb3eff2178c3f4a079f286279a1e0c6What 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(default15 * * * *), 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 newestkeepplus 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 withdrive.fileand a folder the plugin creates itself ("Grav backups ()"), or any account with fulldriveand 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), agdrive-foldercustom 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(orapi.super) can change the account and folder; the check is server-side inonAdminSave, so a plainapi.config.writeholder can't redirect the backup zips, which holduser/accountsand 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'smd5Checksumis checked againstmd5_file()and a failed upload holds back retention for the run; a non-blockingflockstops 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 onlydrive.google.comlinks; nothing stored or cached comes from theHostheader (the site name followssystem.custom_base_url); the sync catches everyThrowableso a plugin error can't break the backup that just ran or the scheduler; the folder check is not on the/data/resolveallowlist, so page editors can't reach it.Testing:
tests/smoke.php(the library's realDriveclient 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 --checkon 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.