Merge branch 'feat/backup' into nightly

This commit is contained in:
FreddleSpl0it committed 2026-11-09 15:50:43 +01:00
commit aededa7ade
9 files changed
+39 -5

No files matched your search

+1
View File
@@ -126,6 +126,7 @@ $template_data = [
'mbox_templates' => $mbox_templates,
'backup_timezone' => getenv('TZ') ?: 'UTC',
'backup_hostname' => getenv('MAILCOW_HOSTNAME') ?: '',
'backup_demo_mode' => $DEMO_MODE,
'lang_admin' => json_encode($lang['admin']),
'lang_datatables' => json_encode($lang['datatables'])
];
@@ -39,7 +39,8 @@ use Redis;
* process_get_return(BackupModule::fromGlobals()->api->getSettings());
* echo json_encode(BackupModule::create($pdo, Context::system(), $config, $redis)->api->export($domain, $page)); // backup.php
*
* $config: timezone (TZ of mailcow, for the schedule), alias_disable_external_domains ($ALIAS_DISABLE_EXTERNAL_DOMAINS).
* $config: timezone (TZ of mailcow, for the schedule), alias_disable_external_domains ($ALIAS_DISABLE_EXTERNAL_DOMAINS),
* demo_mode ($DEMO_MODE: the API changes nothing).
* $redis: for DKIM keys, rate limits and the domain map (export and restore).
*/
class BackupModule
@@ -73,15 +74,17 @@ class BackupModule
$restore = new RestoreService($restoreRepo, $jobRepo, $groups, $sogo, $dovecot, $legacy, (bool)($config['alias_disable_external_domains'] ?? false));
$domainRestore = new DomainRestoreService($restoreRepo, $jobRepo, $groups, $legacy);
return new self($settings, $jobs, $export, $restore, $domainRestore,
new BackupApiController($settings, $jobs, $export, $restore, $domainRestore, new ApiResponse(Settings::SECRETS)));
new BackupApiController($settings, $jobs, $export, $restore, $domainRestore, new ApiResponse(Settings::SECRETS),
(bool)($config['demo_mode'] ?? false)));
}
/** Legacy entry point for procedural callers: reads $pdo, $redis, $_SESSION, TZ and $ALIAS_DISABLE_EXTERNAL_DOMAINS */
/** Legacy entry point for procedural callers: reads $pdo, $redis, $_SESSION, TZ, $ALIAS_DISABLE_EXTERNAL_DOMAINS and $DEMO_MODE */
public static function fromGlobals(?Context $context = null): self
{
return self::create($GLOBALS['pdo'], $context ?? Context::fromSession($_SESSION ?? []), [
'timezone' => getenv('TZ') ?: 'UTC',
'alias_disable_external_domains' => ($GLOBALS['ALIAS_DISABLE_EXTERNAL_DOMAINS'] ?? false) === true,
], ($GLOBALS['redis'] ?? null) instanceof Redis ? $GLOBALS['redis'] : null);
'demo_mode' => ($GLOBALS['DEMO_MODE'] ?? false) === true,
],($GLOBALS['redis'] ?? null) instanceof Redis ? $GLOBALS['redis'] : null);
}
}
@@ -23,7 +23,8 @@ use PDOException;
class BackupApiController
{
public function __construct(private SettingsService $settings, private JobService $jobs, private ExportService $export,
private RestoreService $restore, private DomainRestoreService $domainRestore, private ApiResponse $response)
private RestoreService $restore, private DomainRestoreService $domainRestore, private ApiResponse $response,
private bool $demoMode = false)
{
}
@@ -147,11 +148,15 @@ class BackupApiController
/**
* Runs one write call: $work gets the log entry and returns the result for process_*_return().
* A UserError becomes a danger message, a database error stops the call with one `mysql_error` message.
* In demo mode no write call runs.
*/
private function handle(string $action, string $object, mixed $data, callable $work): bool
{
$log = ['backup', $action, $object, $data];
try {
if ($this->demoMode) {
throw new UserError('demo_mode_enabled');
}
$result = $work($log);
} catch (UserError $e) {
$result = $this->response->error($e, $log);
+1
View File
@@ -13,6 +13,7 @@ $template_data = [
'lang_datatables' => json_encode($lang['datatables']),
'backup_configured' => \Mailcow\Backup\BackupModule::fromGlobals()->settings->load()->configured(),
'backup_timezone' => getenv('TZ') ?: 'UTC',
'backup_demo_mode' => $DEMO_MODE,
];
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/footer.inc.php';
+1
View File
@@ -607,6 +607,7 @@
"created_from_backup": "Aus dem Backup angelegt",
"data_added": "Hinzugefügt",
"deleted_domain_children": "Die Domain wurde gelöscht: Nur die ganze Domain lässt sich wiederherstellen.",
"demo_mode": "Demo Mode ist aktiviert: Backups, Wiederherstellungen und Änderungen an den Einstellungen sind gesperrt",
"details": "Details",
"domains": "Domains",
"duration": "Dauer",
+1
View File
@@ -607,6 +607,7 @@
"created_from_backup": "Created from the backup",
"data_added": "Added",
"deleted_domain_children": "The domain was deleted: only the whole domain can be restored.",
"demo_mode": "Demo Mode is enabled: backups, restores and changes to the settings are disabled",
"details": "Details",
"domains": "Domains",
"duration": "Duration",
@@ -13,6 +13,7 @@
<div class="row mb-4">
<div class="col-sm-9 offset-sm-3">
<p class="text-muted">{{ lang.backup.intro }} <a href="/admin/restore">{{ lang.backup.intro_restore_link }}</a></p>
{% if backup_demo_mode %}<div class="alert alert-warning">{{ lang.backup.demo_mode }}</div>{% endif %}
<div id="backup_status">
<div class="backup-status-summary mb-2" aria-live="polite"><span class="text-muted">{{ lang.footer.loading }}</span></div>
+1
View File
@@ -10,6 +10,7 @@
<span>{{ lang.backup.restore_card }}</span>
</div>
<div class="card-body">
{% if backup_demo_mode %}<div class="alert alert-warning">{{ lang.backup.demo_mode }}</div>{% endif %}
<div class="backup-restore-hints"></div>
{# left-aligned with the search of the toolbar below #}
<div class="d-flex flex-wrap align-items-center gap-2 mb-3 px-2">