From 974b971340b9ab04b59e31c94c8d30b8442b42ff Mon Sep 17 00:00:00 2001 From: Jonathan Miller Date: Sun, 21 Jun 2026 19:08:27 -0500 Subject: [PATCH] feat: snapshot retention and automatic cleanup (#63) Add retention settings for content snapshots (max count, max age days) in component options. System plugin runs cleanupOldSnapshots() alongside existing backup cleanup, deleting JSON files and DB records. Closes #63 --- .../packages/com_mokosuitebackup/config.xml | 21 +++++ .../language/en-GB/com_mokosuitebackup.ini | 7 ++ .../src/Extension/MokoSuiteBackup.php | 88 +++++++++++++++++++ 3 files changed, 116 insertions(+) diff --git a/source/packages/com_mokosuitebackup/config.xml b/source/packages/com_mokosuitebackup/config.xml index 60428d2..6fdd1ad 100644 --- a/source/packages/com_mokosuitebackup/config.xml +++ b/source/packages/com_mokosuitebackup/config.xml @@ -118,6 +118,27 @@ /> +
+ + +
+
set('mokosuitebackup.last_cleanup', time()); $this->cleanupOldBackups(); + $this->cleanupOldSnapshots(); } /** @@ -152,6 +153,93 @@ final class MokoSuiteBackup extends CMSPlugin implements SubscriberInterface } } + /** + * Remove old content snapshots per component retention settings. + * + * Respects snapshot_retention_days (max age) and snapshot_retention_count + * (max number to keep). A value of 0 means unlimited for that setting. + */ + private function cleanupOldSnapshots(): void + { + try { + $this->doSnapshotCleanup(); + } catch (\Throwable $e) { + error_log('MokoSuiteBackup: cleanupOldSnapshots() failed: ' . $e->getMessage()); + } + } + + private function doSnapshotCleanup(): void + { + $db = Factory::getDbo(); + $params = ComponentHelper::getParams('com_mokosuitebackup'); + $retentionDays = (int) $params->get('snapshot_retention_days', 30); + $retentionCount = (int) $params->get('snapshot_retention_count', 20); + + // Delete snapshots older than retention_days + if ($retentionDays > 0) { + $cutoff = date('Y-m-d H:i:s', strtotime("-{$retentionDays} days")); + $query = $db->getQuery(true) + ->select([$db->quoteName('id'), $db->quoteName('data_file')]) + ->from($db->quoteName('#__mokosuitebackup_snapshots')) + ->where($db->quoteName('created') . ' < ' . $db->quote($cutoff)) + ->order($db->quoteName('created') . ' DESC'); + $db->setQuery($query); + $expired = $db->loadObjectList(); + + foreach ($expired as $snapshot) { + $this->deleteSnapshotRecord($db, $snapshot); + } + } + + // Enforce max count (keep newest) + if ($retentionCount > 0) { + $query = $db->getQuery(true) + ->select('COUNT(*)') + ->from($db->quoteName('#__mokosuitebackup_snapshots')); + $db->setQuery($query); + $totalCount = (int) $db->loadResult(); + + if ($totalCount > $retentionCount) { + $excess = $totalCount - $retentionCount; + $query = $db->getQuery(true) + ->select([$db->quoteName('id'), $db->quoteName('data_file')]) + ->from($db->quoteName('#__mokosuitebackup_snapshots')) + ->order($db->quoteName('created') . ' ASC'); + $db->setQuery($query, 0, $excess); + $oldest = $db->loadObjectList(); + + foreach ($oldest as $snapshot) { + $this->deleteSnapshotRecord($db, $snapshot); + } + } + } + } + + /** + * Delete a snapshot record and its JSON data file. + */ + private function deleteSnapshotRecord(object $db, object $snapshot): void + { + if (!empty($snapshot->data_file) && is_file($snapshot->data_file)) { + if (!@unlink($snapshot->data_file)) { + error_log('MokoSuiteBackup: Could not delete snapshot file (id=' . $snapshot->id . '): ' . $snapshot->data_file); + + return; + } + } + + try { + $db->setQuery( + $db->getQuery(true) + ->delete($db->quoteName('#__mokosuitebackup_snapshots')) + ->where($db->quoteName('id') . ' = ' . (int) $snapshot->id) + ); + $db->execute(); + } catch (\Exception $e) { + error_log('MokoSuiteBackup: Could not delete snapshot record ' . $snapshot->id . ': ' . $e->getMessage()); + } + } + private function doCleanup(): void { $db = Factory::getDbo();