getId(); } public function policy(): RetentionPolicy { return RetentionPolicy::fromArray(json_decode($this->appConfig->getValueString(Application::APP_ID, 'retention', '{}'), true) ?: []); } public function setPolicy(RetentionPolicy $policy): void { if ($policy->last < 1) { throw new \InvalidArgumentException('Keep at least the last snapshot'); } $this->appConfig->setValueString(Application::APP_ID, 'retention', json_encode($policy->toArray(), JSON_THROW_ON_ERROR)); } /** * @return array{kept: array>, forgotten: list, prune: array} */ public function apply(Target $target, bool $dryRun = false): array { if ($target->getAppendOnly()) { throw new \RuntimeException('Location ' . $target->getName() . ' is append-only: NG Backup does not delete anything there'); } if ($this->runs->findRunning($target->getId(), BackupService::KIND_FULL) !== []) { throw new \RuntimeException('A backup to ' . $target->getName() . ' is running; try again later'); } try { $this->locking->acquireLock(self::lockKey($target), ILockingProvider::LOCK_EXCLUSIVE); } catch (LockedException) { throw new \RuntimeException('Another backup or cleanup of ' . $target->getName() . ' is running'); } try { $repo = $this->targets->repository($target); $times = []; foreach ($repo->listSnapshots() as $id) { $meta = $repo->snapshotMeta($id); $times[$id] = (int)strtotime((string)($meta['time'] ?? '@0')); } $keep = $this->policy()->keep($times); $forget = array_values(array_diff(array_keys($times), array_keys($keep))); if (!$dryRun) { foreach ($forget as $id) { $repo->forget($id); try { $this->snapshots->delete($this->snapshots->findOne($target->getId(), $id)); } catch (\Throwable) { } } } // Also removes leftovers of interrupted runs. A dry run counts the snapshots it would forget as gone. $prune = $repo->prune($dryRun, 0.5, $dryRun ? $forget : []); return ['kept' => $keep, 'forgotten' => $forget, 'prune' => $prune]; } finally { $this->locking->releaseLock(self::lockKey($target), ILockingProvider::LOCK_EXCLUSIVE); } } }