Retention does not delete pruned backups from remote destinations #229

Closed
opened 2026-07-06 01:32:42 +00:00 by jmiller · 3 comments
Owner

Problem

Backup retention (both RetentionManager::prune() after a backup and the plugin's hourly doCleanup()) only deletes the local record + archive + .log. The archive copies uploaded to remote destinations (SFTP / FTP / S3 / Google Drive) are never removed, so remotes grow unbounded regardless of the profile's retention_days / retention_count.

Root causes

  1. No uploader exposed a delete operation — RemoteUploaderInterface only had upload() / testConnection().
  2. #__mokosuitebackup_records does not track which destinations a backup was uploaded to.

Fix (in progress)

  • Add delete(string $remoteName): array to RemoteUploaderInterface and implement it in all four uploaders (idempotent — already-absent file counts as success).
  • RetentionManager now, for each pruned record, deletes the archive (archivename) from the profile's currently-enabled remotes (best-effort; failures logged, never block local pruning).
  • The standalone restore.php is intentionally NOT deleted — it is a single fixed-name file every backup overwrites, so newer backups still depend on it.

Since records don't store their destinations, deletion targets the profile's current enabled remotes. Documented as a known limitation (a destination removed after a backup won't be cleaned).

https://claude.ai/code/session_01WbGBN9VyRK61zczYWcCQ2i

## Problem Backup retention (both `RetentionManager::prune()` after a backup and the plugin's hourly `doCleanup()`) only deletes the **local** record + archive + `.log`. The archive copies uploaded to remote destinations (SFTP / FTP / S3 / Google Drive) are **never removed**, so remotes grow unbounded regardless of the profile's `retention_days` / `retention_count`. ## Root causes 1. No uploader exposed a delete operation — `RemoteUploaderInterface` only had `upload()` / `testConnection()`. 2. `#__mokosuitebackup_records` does not track which destinations a backup was uploaded to. ## Fix (in progress) - Add `delete(string $remoteName): array` to `RemoteUploaderInterface` and implement it in all four uploaders (idempotent — already-absent file counts as success). - `RetentionManager` now, for each pruned record, deletes the archive (`archivename`) from the profile's **currently-enabled** remotes (best-effort; failures logged, never block local pruning). - **The standalone `restore.php` is intentionally NOT deleted** — it is a single fixed-name file every backup overwrites, so newer backups still depend on it. Since records don't store their destinations, deletion targets the profile's current enabled remotes. Documented as a known limitation (a destination removed after a backup won't be cleaned). https://claude.ai/code/session_01WbGBN9VyRK61zczYWcCQ2i
Author
Owner

Branch created: feature/229-retention-does-not-delete-pruned-backups

git fetch origin
git checkout feature/229-retention-does-not-delete-pruned-backups
Branch created: [`feature/229-retention-does-not-delete-pruned-backups`](https://git.mokoconsulting.tech/MokoConsulting/MokoSuiteBackup/src/branch/feature/229-retention-does-not-delete-pruned-backups) ```bash git fetch origin git checkout feature/229-retention-does-not-delete-pruned-backups ```
Author
Owner

Fixed in #231, merged to dev. Each uploader (SFTP/FTP/S3/Google Drive) now has an idempotent delete(), and RetentionManager removes each pruned archive from the profile's enabled remotes (best-effort; restore.php deliberately preserved). Will verify remote pruning on suite.dev after the next dev build, then close.

Fixed in #231, merged to `dev`. Each uploader (SFTP/FTP/S3/Google Drive) now has an idempotent `delete()`, and `RetentionManager` removes each pruned archive from the profile's enabled remotes (best-effort; `restore.php` deliberately preserved). Will verify remote pruning on suite.dev after the next `dev` build, then close.
Author
Owner

Verified on suite.dev (02.58.09-dev).

  • RetentionManager::deleteFromRemotes() is deployed and wired into prune(); a post-backup prune ran (profile 1 held at its retention_count=10 after the 11th backup).
  • The exact delete() path retention uses was exercised directly against the live SFTP remote: upload throwaway → delete() removes it → second delete() is an idempotent no-op success (all OK). So an expiring archive is genuinely removed from the destination, and a partially-cleaned remote never blocks pruning.
  • restore.php was correctly left in place on the remote (shared, overwritten each backup — not pruned).

Remote retention delete confirmed working. Closing.

✅ **Verified on suite.dev** (`02.58.09-dev`). - `RetentionManager::deleteFromRemotes()` is deployed and wired into `prune()`; a post-backup prune ran (profile 1 held at its `retention_count=10` after the 11th backup). - The exact `delete()` path retention uses was exercised directly against the live SFTP remote: **upload throwaway → `delete()` removes it → second `delete()` is an idempotent no-op success** (all OK). So an expiring archive is genuinely removed from the destination, and a partially-cleaned remote never blocks pruning. - `restore.php` was correctly **left in place** on the remote (shared, overwritten each backup — not pruned). Remote retention delete confirmed working. Closing.
Sign in to join this conversation.