Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 9a908e2e3c | |||
| d8367d7beb | |||
| 11141f27f4 |
@@ -5,7 +5,7 @@
|
||||
<display-name>Package - MokoSuiteBackup</display-name>
|
||||
<org>MokoConsulting</org>
|
||||
<description>Full-site backup and restore for Joomla — database, files, and configuration</description>
|
||||
<version>01.21.00-dev</version>
|
||||
<version>01.22.00-dev</version>
|
||||
<license spdx="GPL-3.0-or-later">GNU General Public License v3</license>
|
||||
</identity>
|
||||
<governance>
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
# FILE INFORMATION
|
||||
# DEFGROUP: Gitea.Workflow
|
||||
# INGROUP: mokoplatform.Automation
|
||||
# VERSION: 01.21.00
|
||||
# VERSION: 01.22.00
|
||||
# BRIEF: Auto-create feature branch when an issue is opened
|
||||
|
||||
name: "Universal: Issue Branch"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# MokoSuiteBackup
|
||||
|
||||
<!-- VERSION: 01.21.00 -->
|
||||
<!-- VERSION: 01.22.00 -->
|
||||
|
||||
Full-site backup and restore for Joomla — database, files, and configuration.
|
||||
|
||||
|
||||
+1
-1
@@ -7,7 +7,7 @@
|
||||
-->
|
||||
<extension type="plugin" group="webservices" method="upgrade">
|
||||
<name>Web Services - MokoSuiteBackup</name>
|
||||
<version>01.21.00</version>
|
||||
<version>01.22.00-rc</version>
|
||||
<creationDate>2026-06-02</creationDate>
|
||||
<author>Moko Consulting</author>
|
||||
<authorEmail>hello@mokoconsulting.tech</authorEmail>
|
||||
|
||||
@@ -176,6 +176,29 @@
|
||||
</field>
|
||||
</fieldset>
|
||||
|
||||
<fieldset name="retention" label="COM_MOKOJOOMBACKUP_FIELDSET_RETENTION">
|
||||
<field
|
||||
name="retention_days"
|
||||
type="number"
|
||||
label="COM_MOKOJOOMBACKUP_FIELD_RETENTION_DAYS"
|
||||
description="COM_MOKOJOOMBACKUP_FIELD_RETENTION_DAYS_DESC"
|
||||
default="0"
|
||||
min="0"
|
||||
max="365"
|
||||
hint="0"
|
||||
/>
|
||||
<field
|
||||
name="retention_count"
|
||||
type="number"
|
||||
label="COM_MOKOJOOMBACKUP_FIELD_RETENTION_COUNT"
|
||||
description="COM_MOKOJOOMBACKUP_FIELD_RETENTION_COUNT_DESC"
|
||||
default="0"
|
||||
min="0"
|
||||
max="999"
|
||||
hint="0"
|
||||
/>
|
||||
</fieldset>
|
||||
|
||||
<fieldset name="notifications" label="COM_MOKOJOOMBACKUP_FIELDSET_NOTIFICATIONS">
|
||||
<field
|
||||
name="notify_email"
|
||||
|
||||
@@ -197,6 +197,13 @@ COM_MOKOJOOMBACKUP_FIELD_NOTIFY_SUCCESS="Notify on Success"
|
||||
COM_MOKOJOOMBACKUP_FIELD_NOTIFY_SUCCESS_DESC="Send an email when a backup completes successfully."
|
||||
COM_MOKOJOOMBACKUP_FIELD_NOTIFY_FAILURE="Notify on Failure"
|
||||
COM_MOKOJOOMBACKUP_FIELD_NOTIFY_FAILURE_DESC="Send an email when a backup fails. Includes log excerpt for debugging."
|
||||
; Retention
|
||||
COM_MOKOJOOMBACKUP_FIELDSET_RETENTION="Retention"
|
||||
COM_MOKOJOOMBACKUP_FIELD_RETENTION_DAYS="Keep Backups (days)"
|
||||
COM_MOKOJOOMBACKUP_FIELD_RETENTION_DAYS_DESC="Delete completed backups from this profile older than this many days. Set to 0 to use the global default from component options."
|
||||
COM_MOKOJOOMBACKUP_FIELD_RETENTION_COUNT="Keep Backups (count)"
|
||||
COM_MOKOJOOMBACKUP_FIELD_RETENTION_COUNT_DESC="Maximum number of completed backups to keep for this profile. Oldest are removed first. Set to 0 to use the global default from component options."
|
||||
|
||||
COM_MOKOJOOMBACKUP_FIELD_NTFY_SPACER_DESC="<strong>Push Notifications (ntfy)</strong> — Send instant push notifications to your phone or desktop via <a href='https://ntfy.sh' target='_blank'>ntfy.sh</a> or a self-hosted ntfy server."
|
||||
COM_MOKOJOOMBACKUP_FIELD_NTFY_TOPIC="ntfy Topic"
|
||||
COM_MOKOJOOMBACKUP_FIELD_NTFY_TOPIC_DESC="The ntfy topic to publish notifications to. Leave blank to disable push notifications."
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
-->
|
||||
<extension type="component" method="upgrade">
|
||||
<name>MokoSuiteBackup</name>
|
||||
<version>01.21.00</version>
|
||||
<version>01.22.00-rc</version>
|
||||
<creationDate>2026-06-02</creationDate>
|
||||
<author>Moko Consulting</author>
|
||||
<authorEmail>hello@mokoconsulting.tech</authorEmail>
|
||||
|
||||
@@ -36,6 +36,8 @@ CREATE TABLE IF NOT EXISTS `#__mokosuitebackup_profiles` (
|
||||
`notify_user_groups` VARCHAR(255) NOT NULL DEFAULT '' COMMENT 'Comma-separated Joomla user group IDs',
|
||||
`notify_on_success` TINYINT(1) NOT NULL DEFAULT 0,
|
||||
`notify_on_failure` TINYINT(1) NOT NULL DEFAULT 1,
|
||||
`retention_days` INT(11) NOT NULL DEFAULT 0 COMMENT '0 = use global default',
|
||||
`retention_count` INT(11) NOT NULL DEFAULT 0 COMMENT '0 = use global default',
|
||||
`ntfy_topic` VARCHAR(255) NOT NULL DEFAULT '' COMMENT 'ntfy topic name',
|
||||
`ntfy_server` VARCHAR(512) NOT NULL DEFAULT 'https://ntfy.sh' COMMENT 'ntfy server URL',
|
||||
`ntfy_token` VARCHAR(255) NOT NULL DEFAULT '' COMMENT 'ntfy access token (optional)',
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
-- Add per-profile retention settings
|
||||
ALTER TABLE `#__mokosuitebackup_profiles`
|
||||
ADD COLUMN `retention_days` INT(11) NOT NULL DEFAULT 0 COMMENT '0 = use global default' AFTER `notify_on_failure`,
|
||||
ADD COLUMN `retention_count` INT(11) NOT NULL DEFAULT 0 COMMENT '0 = use global default' AFTER `retention_days`;
|
||||
@@ -7,7 +7,7 @@
|
||||
-->
|
||||
<extension type="plugin" group="actionlog" method="upgrade">
|
||||
<name>Action Log - MokoSuiteBackup</name>
|
||||
<version>01.21.00</version>
|
||||
<version>01.22.00-rc</version>
|
||||
<creationDate>2026-06-04</creationDate>
|
||||
<author>Moko Consulting</author>
|
||||
<authorEmail>hello@mokoconsulting.tech</authorEmail>
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
-->
|
||||
<extension type="plugin" group="console" method="upgrade">
|
||||
<name>Console - MokoSuiteBackup</name>
|
||||
<version>01.21.00</version>
|
||||
<version>01.22.00-rc</version>
|
||||
<creationDate>2026-06-04</creationDate>
|
||||
<author>Moko Consulting</author>
|
||||
<authorEmail>hello@mokoconsulting.tech</authorEmail>
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
-->
|
||||
<extension type="plugin" group="content" method="upgrade">
|
||||
<name>Content - MokoSuiteBackup</name>
|
||||
<version>01.21.00</version>
|
||||
<version>01.22.00-rc</version>
|
||||
<creationDate>2026-06-04</creationDate>
|
||||
<author>Moko Consulting</author>
|
||||
<authorEmail>hello@mokoconsulting.tech</authorEmail>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<extension type="plugin" group="quickicon" method="upgrade">
|
||||
<name>Quick Icon - MokoSuiteBackup</name>
|
||||
<version>01.21.00</version>
|
||||
<version>01.22.00-rc</version>
|
||||
<creationDate>2026-06-02</creationDate>
|
||||
<author>Moko Consulting</author>
|
||||
<authorEmail>hello@mokoconsulting.tech</authorEmail>
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
-->
|
||||
<extension type="plugin" group="system" method="upgrade">
|
||||
<name>System - MokoSuiteBackup</name>
|
||||
<version>01.21.00</version>
|
||||
<version>01.22.00-rc</version>
|
||||
<creationDate>2026-06-02</creationDate>
|
||||
<author>Moko Consulting</author>
|
||||
<authorEmail>hello@mokoconsulting.tech</authorEmail>
|
||||
|
||||
@@ -133,72 +133,109 @@ final class MokoSuiteBackup extends CMSPlugin implements SubscriberInterface
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove backup records and files older than max_age_days or exceeding max_backups.
|
||||
* Remove backup records and files per profile retention settings.
|
||||
* Each profile can override the global max_age_days and max_backups.
|
||||
* A profile value of 0 means "use the global default".
|
||||
*/
|
||||
private function cleanupOldBackups(): void
|
||||
{
|
||||
$db = Factory::getDbo();
|
||||
$maxAge = (int) $this->params->get('max_age_days', 30);
|
||||
$maxBackups = (int) $this->params->get('max_backups', 10);
|
||||
$db = Factory::getDbo();
|
||||
$globalMaxAge = (int) ComponentHelper::getParams('com_mokosuitebackup')->get('max_age_days', 30);
|
||||
$globalMaxCount = (int) ComponentHelper::getParams('com_mokosuitebackup')->get('max_backups', 10);
|
||||
|
||||
// Delete by age
|
||||
$cutoff = date('Y-m-d H:i:s', strtotime("-{$maxAge} days"));
|
||||
$query = $db->getQuery(true)
|
||||
->select('id, absolute_path')
|
||||
->from($db->quoteName('#__mokosuitebackup_records'))
|
||||
->where($db->quoteName('backupstart') . ' < ' . $db->quote($cutoff))
|
||||
->where($db->quoteName('status') . ' = ' . $db->quote('complete'));
|
||||
$db->setQuery($query);
|
||||
$expired = $db->loadObjectList();
|
||||
|
||||
foreach ($expired as $record) {
|
||||
if (!empty($record->absolute_path) && is_file($record->absolute_path)) {
|
||||
if (!@unlink($record->absolute_path)) {
|
||||
continue; // Don't delete DB record if file can't be removed
|
||||
}
|
||||
}
|
||||
|
||||
$db->setQuery(
|
||||
$db->getQuery(true)
|
||||
->delete($db->quoteName('#__mokosuitebackup_records'))
|
||||
->where($db->quoteName('id') . ' = ' . (int) $record->id)
|
||||
);
|
||||
$db->execute();
|
||||
}
|
||||
|
||||
// Enforce max backups count (keep newest)
|
||||
// Load all published profiles with their retention settings
|
||||
$query = $db->getQuery(true)
|
||||
->select('COUNT(*)')
|
||||
->from($db->quoteName('#__mokosuitebackup_records'))
|
||||
->where($db->quoteName('status') . ' = ' . $db->quote('complete'));
|
||||
->select([$db->quoteName('id'), $db->quoteName('retention_days'), $db->quoteName('retention_count')])
|
||||
->from($db->quoteName('#__mokosuitebackup_profiles'))
|
||||
->where($db->quoteName('published') . ' = 1');
|
||||
$db->setQuery($query);
|
||||
$totalCount = (int) $db->loadResult();
|
||||
$profiles = $db->loadObjectList();
|
||||
|
||||
if ($totalCount > $maxBackups) {
|
||||
$excess = $totalCount - $maxBackups;
|
||||
foreach ($profiles as $profile) {
|
||||
$maxAge = (int) $profile->retention_days > 0 ? (int) $profile->retention_days : $globalMaxAge;
|
||||
$maxCount = (int) $profile->retention_count > 0 ? (int) $profile->retention_count : $globalMaxCount;
|
||||
$pid = (int) $profile->id;
|
||||
|
||||
// Delete by age for this profile
|
||||
$cutoff = date('Y-m-d H:i:s', strtotime("-{$maxAge} days"));
|
||||
$query = $db->getQuery(true)
|
||||
->select('id, absolute_path')
|
||||
->from($db->quoteName('#__mokosuitebackup_records'))
|
||||
->where($db->quoteName('status') . ' = ' . $db->quote('complete'))
|
||||
->order($db->quoteName('backupstart') . ' ASC');
|
||||
$db->setQuery($query, 0, $excess);
|
||||
$oldest = $db->loadObjectList();
|
||||
->where($db->quoteName('profile_id') . ' = ' . $pid)
|
||||
->where($db->quoteName('backupstart') . ' < ' . $db->quote($cutoff))
|
||||
->where($db->quoteName('status') . ' = ' . $db->quote('complete'));
|
||||
$db->setQuery($query);
|
||||
$expired = $db->loadObjectList();
|
||||
|
||||
foreach ($oldest as $record) {
|
||||
if (!empty($record->absolute_path) && is_file($record->absolute_path)) {
|
||||
if (!@unlink($record->absolute_path)) {
|
||||
continue; // Do not delete DB record if file cannot be removed
|
||||
}
|
||||
}
|
||||
foreach ($expired as $record) {
|
||||
$this->deleteBackupRecord($db, $record);
|
||||
}
|
||||
|
||||
$db->setQuery(
|
||||
$db->getQuery(true)
|
||||
->delete($db->quoteName('#__mokosuitebackup_records'))
|
||||
->where($db->quoteName('id') . ' = ' . (int) $record->id)
|
||||
);
|
||||
$db->execute();
|
||||
// Enforce max count for this profile (keep newest)
|
||||
$query = $db->getQuery(true)
|
||||
->select('COUNT(*)')
|
||||
->from($db->quoteName('#__mokosuitebackup_records'))
|
||||
->where($db->quoteName('profile_id') . ' = ' . $pid)
|
||||
->where($db->quoteName('status') . ' = ' . $db->quote('complete'));
|
||||
$db->setQuery($query);
|
||||
$totalCount = (int) $db->loadResult();
|
||||
|
||||
if ($totalCount > $maxCount) {
|
||||
$excess = $totalCount - $maxCount;
|
||||
$query = $db->getQuery(true)
|
||||
->select('id, absolute_path')
|
||||
->from($db->quoteName('#__mokosuitebackup_records'))
|
||||
->where($db->quoteName('profile_id') . ' = ' . $pid)
|
||||
->where($db->quoteName('status') . ' = ' . $db->quote('complete'))
|
||||
->order($db->quoteName('backupstart') . ' ASC');
|
||||
$db->setQuery($query, 0, $excess);
|
||||
$oldest = $db->loadObjectList();
|
||||
|
||||
foreach ($oldest as $record) {
|
||||
$this->deleteBackupRecord($db, $record);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Also clean up orphaned records (profile deleted but records remain)
|
||||
$query = $db->getQuery(true)
|
||||
->select('r.id, r.absolute_path')
|
||||
->from($db->quoteName('#__mokosuitebackup_records', 'r'))
|
||||
->join('LEFT', $db->quoteName('#__mokosuitebackup_profiles', 'p') . ' ON p.id = r.profile_id')
|
||||
->where('p.id IS NULL')
|
||||
->where($db->quoteName('r.status') . ' = ' . $db->quote('complete'));
|
||||
$db->setQuery($query);
|
||||
$orphans = $db->loadObjectList();
|
||||
|
||||
foreach ($orphans as $record) {
|
||||
$this->deleteBackupRecord($db, $record);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a backup record and its archive file.
|
||||
*/
|
||||
private function deleteBackupRecord(object $db, object $record): void
|
||||
{
|
||||
if (!empty($record->absolute_path) && is_file($record->absolute_path)) {
|
||||
if (!@unlink($record->absolute_path)) {
|
||||
return; // Don't delete DB record if file can't be removed
|
||||
}
|
||||
|
||||
// Also remove the log file if it exists alongside the archive
|
||||
$logPath = preg_replace('/\.(zip|tar\.gz)$/i', '.log', $record->absolute_path);
|
||||
|
||||
if (is_file($logPath)) {
|
||||
@unlink($logPath);
|
||||
}
|
||||
}
|
||||
|
||||
$db->setQuery(
|
||||
$db->getQuery(true)
|
||||
->delete($db->quoteName('#__mokosuitebackup_records'))
|
||||
->where($db->quoteName('id') . ' = ' . (int) $record->id)
|
||||
);
|
||||
$db->execute();
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
-->
|
||||
<extension type="plugin" group="task" method="upgrade">
|
||||
<name>Task - MokoSuiteBackup</name>
|
||||
<version>01.21.00</version>
|
||||
<version>01.22.00-rc</version>
|
||||
<creationDate>2026-06-02</creationDate>
|
||||
<author>Moko Consulting</author>
|
||||
<authorEmail>hello@mokoconsulting.tech</authorEmail>
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
-->
|
||||
<extension type="plugin" group="webservices" method="upgrade">
|
||||
<name>Web Services - MokoSuiteBackup</name>
|
||||
<version>01.21.00</version>
|
||||
<version>01.22.00-rc</version>
|
||||
<creationDate>2026-06-02</creationDate>
|
||||
<author>Moko Consulting</author>
|
||||
<authorEmail>hello@mokoconsulting.tech</authorEmail>
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
<extension type="package" method="upgrade">
|
||||
<name>Package - MokoSuiteBackup</name>
|
||||
<packagename>mokosuitebackup</packagename>
|
||||
<version>01.21.00</version>
|
||||
<version>01.22.00-rc</version>
|
||||
<creationDate>2026-06-02</creationDate>
|
||||
<author>Moko Consulting</author>
|
||||
<authorEmail>hello@mokoconsulting.tech</authorEmail>
|
||||
|
||||
Reference in New Issue
Block a user