M5: retention policy, prune with repacking, append-only locations

- RetentionPolicy (last/daily/weekly/monthly) and PruneService: forget
  snapshots the policy drops, delete packs nothing uses and repack mostly
  unused ones by copying encrypted blobs as-is; new packs are indexed
  before old ones are removed
- a location lock keeps prune and backups apart; append-only locations
  are never pruned (cleanup via the location's lifecycle rules)
- weekly prune from the background job; occ backup:prune [--dry-run],
  backup:retention, backup:target:append-only
- tests: forget+prune frees space and the remaining snapshot restores
  identically, repack of mixed packs, dry-run preview, retention buckets
This commit is contained in:
2026-10-02 02:21:48 +02:00
parent e5eaf6df14
commit 7b3c9cabf6
16 changed files with 606 additions and 1 deletions
+21
View File
@@ -26,6 +26,7 @@ class BackupJob extends TimedJob {
public function __construct(
ITimeFactory $time,
private BackupService $backups,
private \OCA\NgBackup\Service\PruneService $prune,
private TargetService $targets,
private RunMapper $runs,
private IAppConfig $appConfig,
@@ -45,6 +46,26 @@ class BackupJob extends TimedJob {
$run = $this->backups->step($run, min($deadline, microtime(true) + $budget));
}
}
$this->weeklyPrune();
}
/** Once a week (after Sunday's backup), apply the retention policy to non-append-only locations. */
private function weeklyPrune(): void {
$lastWeek = $this->appConfig->getValueString(Application::APP_ID, 'last_prune_week', '');
if (date('N') !== '7' || $lastWeek === date('o-W') || $this->runs->findRunning(null, BackupService::KIND_FULL) !== []) {
return;
}
foreach ($this->targets->list() as $t) {
if ($t->getAppendOnly()) {
continue;
}
try {
$this->prune->apply($t);
} catch (\Throwable $e) {
$this->logger->warning('NG Backup: cleanup of ' . $t->getName() . ' failed: ' . $e->getMessage());
}
}
$this->appConfig->setValueString(Application::APP_ID, 'last_prune_week', date('o-W'));
}
/** Daily schedule: appconfig ng_backup/schedule_time "HH:MM" (server time); empty = no schedule. */