Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 9a908e2e3c | |||
| d8367d7beb | |||
| 11141f27f4 |
@@ -5,7 +5,7 @@
|
|||||||
<display-name>Package - MokoSuiteBackup</display-name>
|
<display-name>Package - MokoSuiteBackup</display-name>
|
||||||
<org>MokoConsulting</org>
|
<org>MokoConsulting</org>
|
||||||
<description>Full-site backup and restore for Joomla — database, files, and configuration</description>
|
<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>
|
<license spdx="GPL-3.0-or-later">GNU General Public License v3</license>
|
||||||
</identity>
|
</identity>
|
||||||
<governance>
|
<governance>
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
# FILE INFORMATION
|
# FILE INFORMATION
|
||||||
# DEFGROUP: Gitea.Workflow
|
# DEFGROUP: Gitea.Workflow
|
||||||
# INGROUP: mokoplatform.Automation
|
# INGROUP: mokoplatform.Automation
|
||||||
# VERSION: 01.21.00
|
# VERSION: 01.22.00
|
||||||
# BRIEF: Auto-create feature branch when an issue is opened
|
# BRIEF: Auto-create feature branch when an issue is opened
|
||||||
|
|
||||||
name: "Universal: Issue Branch"
|
name: "Universal: Issue Branch"
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# MokoSuiteBackup
|
# MokoSuiteBackup
|
||||||
|
|
||||||
<!-- VERSION: 01.21.00 -->
|
<!-- VERSION: 01.22.00 -->
|
||||||
|
|
||||||
Full-site backup and restore for Joomla — database, files, and configuration.
|
Full-site backup and restore for Joomla — database, files, and configuration.
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -7,7 +7,7 @@
|
|||||||
-->
|
-->
|
||||||
<extension type="plugin" group="webservices" method="upgrade">
|
<extension type="plugin" group="webservices" method="upgrade">
|
||||||
<name>Web Services - MokoSuiteBackup</name>
|
<name>Web Services - MokoSuiteBackup</name>
|
||||||
<version>01.21.00</version>
|
<version>01.22.00-rc</version>
|
||||||
<creationDate>2026-06-02</creationDate>
|
<creationDate>2026-06-02</creationDate>
|
||||||
<author>Moko Consulting</author>
|
<author>Moko Consulting</author>
|
||||||
<authorEmail>hello@mokoconsulting.tech</authorEmail>
|
<authorEmail>hello@mokoconsulting.tech</authorEmail>
|
||||||
|
|||||||
@@ -176,6 +176,29 @@
|
|||||||
</field>
|
</field>
|
||||||
</fieldset>
|
</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">
|
<fieldset name="notifications" label="COM_MOKOJOOMBACKUP_FIELDSET_NOTIFICATIONS">
|
||||||
<field
|
<field
|
||||||
name="notify_email"
|
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_SUCCESS_DESC="Send an email when a backup completes successfully."
|
||||||
COM_MOKOJOOMBACKUP_FIELD_NOTIFY_FAILURE="Notify on Failure"
|
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."
|
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_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="ntfy Topic"
|
||||||
COM_MOKOJOOMBACKUP_FIELD_NTFY_TOPIC_DESC="The ntfy topic to publish notifications to. Leave blank to disable push notifications."
|
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">
|
<extension type="component" method="upgrade">
|
||||||
<name>MokoSuiteBackup</name>
|
<name>MokoSuiteBackup</name>
|
||||||
<version>01.21.00</version>
|
<version>01.22.00-rc</version>
|
||||||
<creationDate>2026-06-02</creationDate>
|
<creationDate>2026-06-02</creationDate>
|
||||||
<author>Moko Consulting</author>
|
<author>Moko Consulting</author>
|
||||||
<authorEmail>hello@mokoconsulting.tech</authorEmail>
|
<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_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_success` TINYINT(1) NOT NULL DEFAULT 0,
|
||||||
`notify_on_failure` TINYINT(1) NOT NULL DEFAULT 1,
|
`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_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_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)',
|
`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">
|
<extension type="plugin" group="actionlog" method="upgrade">
|
||||||
<name>Action Log - MokoSuiteBackup</name>
|
<name>Action Log - MokoSuiteBackup</name>
|
||||||
<version>01.21.00</version>
|
<version>01.22.00-rc</version>
|
||||||
<creationDate>2026-06-04</creationDate>
|
<creationDate>2026-06-04</creationDate>
|
||||||
<author>Moko Consulting</author>
|
<author>Moko Consulting</author>
|
||||||
<authorEmail>hello@mokoconsulting.tech</authorEmail>
|
<authorEmail>hello@mokoconsulting.tech</authorEmail>
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
-->
|
-->
|
||||||
<extension type="plugin" group="console" method="upgrade">
|
<extension type="plugin" group="console" method="upgrade">
|
||||||
<name>Console - MokoSuiteBackup</name>
|
<name>Console - MokoSuiteBackup</name>
|
||||||
<version>01.21.00</version>
|
<version>01.22.00-rc</version>
|
||||||
<creationDate>2026-06-04</creationDate>
|
<creationDate>2026-06-04</creationDate>
|
||||||
<author>Moko Consulting</author>
|
<author>Moko Consulting</author>
|
||||||
<authorEmail>hello@mokoconsulting.tech</authorEmail>
|
<authorEmail>hello@mokoconsulting.tech</authorEmail>
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
-->
|
-->
|
||||||
<extension type="plugin" group="content" method="upgrade">
|
<extension type="plugin" group="content" method="upgrade">
|
||||||
<name>Content - MokoSuiteBackup</name>
|
<name>Content - MokoSuiteBackup</name>
|
||||||
<version>01.21.00</version>
|
<version>01.22.00-rc</version>
|
||||||
<creationDate>2026-06-04</creationDate>
|
<creationDate>2026-06-04</creationDate>
|
||||||
<author>Moko Consulting</author>
|
<author>Moko Consulting</author>
|
||||||
<authorEmail>hello@mokoconsulting.tech</authorEmail>
|
<authorEmail>hello@mokoconsulting.tech</authorEmail>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<extension type="plugin" group="quickicon" method="upgrade">
|
<extension type="plugin" group="quickicon" method="upgrade">
|
||||||
<name>Quick Icon - MokoSuiteBackup</name>
|
<name>Quick Icon - MokoSuiteBackup</name>
|
||||||
<version>01.21.00</version>
|
<version>01.22.00-rc</version>
|
||||||
<creationDate>2026-06-02</creationDate>
|
<creationDate>2026-06-02</creationDate>
|
||||||
<author>Moko Consulting</author>
|
<author>Moko Consulting</author>
|
||||||
<authorEmail>hello@mokoconsulting.tech</authorEmail>
|
<authorEmail>hello@mokoconsulting.tech</authorEmail>
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
-->
|
-->
|
||||||
<extension type="plugin" group="system" method="upgrade">
|
<extension type="plugin" group="system" method="upgrade">
|
||||||
<name>System - MokoSuiteBackup</name>
|
<name>System - MokoSuiteBackup</name>
|
||||||
<version>01.21.00</version>
|
<version>01.22.00-rc</version>
|
||||||
<creationDate>2026-06-02</creationDate>
|
<creationDate>2026-06-02</creationDate>
|
||||||
<author>Moko Consulting</author>
|
<author>Moko Consulting</author>
|
||||||
<authorEmail>hello@mokoconsulting.tech</authorEmail>
|
<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
|
private function cleanupOldBackups(): void
|
||||||
{
|
{
|
||||||
$db = Factory::getDbo();
|
$db = Factory::getDbo();
|
||||||
$maxAge = (int) $this->params->get('max_age_days', 30);
|
$globalMaxAge = (int) ComponentHelper::getParams('com_mokosuitebackup')->get('max_age_days', 30);
|
||||||
$maxBackups = (int) $this->params->get('max_backups', 10);
|
$globalMaxCount = (int) ComponentHelper::getParams('com_mokosuitebackup')->get('max_backups', 10);
|
||||||
|
|
||||||
// Delete by age
|
// Load all published profiles with their retention settings
|
||||||
$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)
|
|
||||||
$query = $db->getQuery(true)
|
$query = $db->getQuery(true)
|
||||||
->select('COUNT(*)')
|
->select([$db->quoteName('id'), $db->quoteName('retention_days'), $db->quoteName('retention_count')])
|
||||||
->from($db->quoteName('#__mokosuitebackup_records'))
|
->from($db->quoteName('#__mokosuitebackup_profiles'))
|
||||||
->where($db->quoteName('status') . ' = ' . $db->quote('complete'));
|
->where($db->quoteName('published') . ' = 1');
|
||||||
$db->setQuery($query);
|
$db->setQuery($query);
|
||||||
$totalCount = (int) $db->loadResult();
|
$profiles = $db->loadObjectList();
|
||||||
|
|
||||||
if ($totalCount > $maxBackups) {
|
foreach ($profiles as $profile) {
|
||||||
$excess = $totalCount - $maxBackups;
|
$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)
|
$query = $db->getQuery(true)
|
||||||
->select('id, absolute_path')
|
->select('id, absolute_path')
|
||||||
->from($db->quoteName('#__mokosuitebackup_records'))
|
->from($db->quoteName('#__mokosuitebackup_records'))
|
||||||
->where($db->quoteName('status') . ' = ' . $db->quote('complete'))
|
->where($db->quoteName('profile_id') . ' = ' . $pid)
|
||||||
->order($db->quoteName('backupstart') . ' ASC');
|
->where($db->quoteName('backupstart') . ' < ' . $db->quote($cutoff))
|
||||||
$db->setQuery($query, 0, $excess);
|
->where($db->quoteName('status') . ' = ' . $db->quote('complete'));
|
||||||
$oldest = $db->loadObjectList();
|
$db->setQuery($query);
|
||||||
|
$expired = $db->loadObjectList();
|
||||||
|
|
||||||
foreach ($oldest as $record) {
|
foreach ($expired as $record) {
|
||||||
if (!empty($record->absolute_path) && is_file($record->absolute_path)) {
|
$this->deleteBackupRecord($db, $record);
|
||||||
if (!@unlink($record->absolute_path)) {
|
}
|
||||||
continue; // Do not delete DB record if file cannot be removed
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
$db->setQuery(
|
// Enforce max count for this profile (keep newest)
|
||||||
$db->getQuery(true)
|
$query = $db->getQuery(true)
|
||||||
->delete($db->quoteName('#__mokosuitebackup_records'))
|
->select('COUNT(*)')
|
||||||
->where($db->quoteName('id') . ' = ' . (int) $record->id)
|
->from($db->quoteName('#__mokosuitebackup_records'))
|
||||||
);
|
->where($db->quoteName('profile_id') . ' = ' . $pid)
|
||||||
$db->execute();
|
->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">
|
<extension type="plugin" group="task" method="upgrade">
|
||||||
<name>Task - MokoSuiteBackup</name>
|
<name>Task - MokoSuiteBackup</name>
|
||||||
<version>01.21.00</version>
|
<version>01.22.00-rc</version>
|
||||||
<creationDate>2026-06-02</creationDate>
|
<creationDate>2026-06-02</creationDate>
|
||||||
<author>Moko Consulting</author>
|
<author>Moko Consulting</author>
|
||||||
<authorEmail>hello@mokoconsulting.tech</authorEmail>
|
<authorEmail>hello@mokoconsulting.tech</authorEmail>
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
-->
|
-->
|
||||||
<extension type="plugin" group="webservices" method="upgrade">
|
<extension type="plugin" group="webservices" method="upgrade">
|
||||||
<name>Web Services - MokoSuiteBackup</name>
|
<name>Web Services - MokoSuiteBackup</name>
|
||||||
<version>01.21.00</version>
|
<version>01.22.00-rc</version>
|
||||||
<creationDate>2026-06-02</creationDate>
|
<creationDate>2026-06-02</creationDate>
|
||||||
<author>Moko Consulting</author>
|
<author>Moko Consulting</author>
|
||||||
<authorEmail>hello@mokoconsulting.tech</authorEmail>
|
<authorEmail>hello@mokoconsulting.tech</authorEmail>
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
<extension type="package" method="upgrade">
|
<extension type="package" method="upgrade">
|
||||||
<name>Package - MokoSuiteBackup</name>
|
<name>Package - MokoSuiteBackup</name>
|
||||||
<packagename>mokosuitebackup</packagename>
|
<packagename>mokosuitebackup</packagename>
|
||||||
<version>01.21.00</version>
|
<version>01.22.00-rc</version>
|
||||||
<creationDate>2026-06-02</creationDate>
|
<creationDate>2026-06-02</creationDate>
|
||||||
<author>Moko Consulting</author>
|
<author>Moko Consulting</author>
|
||||||
<authorEmail>hello@mokoconsulting.tech</authorEmail>
|
<authorEmail>hello@mokoconsulting.tech</authorEmail>
|
||||||
|
|||||||
Reference in New Issue
Block a user