Merge branch 'feat/backup' into nightly

This commit is contained in:
FreddleSpl0it committed 2026-11-09 15:44:47 +01:00
commit d475d1c593
66 files changed
+15359 -97

No files matched your search

+6
View File
@@ -28,6 +28,7 @@
<li><button class="dropdown-item" data-bs-target="#tab-config-customize" aria-selected="false" aria-controls="tab-config-customize" role="tab" data-bs-toggle="tab">{{ lang.admin.customize }}</button></li>
</ul>
</li>
<li role="presentation" class="nav-item"><button class="nav-link" data-bs-target="#tab-config-backup" aria-selected="false" aria-controls="tab-config-backup" role="tab" data-bs-toggle="tab">{{ lang.backup.tab }}</button></li>
<li role="presentation" class="nav-item"><button class="nav-link" data-bs-target="#tab-routing" aria-selected="false" aria-controls="tab-routing" role="tab" data-bs-toggle="tab">{{ lang.admin.routing }}</button></li>
<li role="presentation" class="nav-item"><button class="nav-link" data-bs-target="#tab-archive" aria-selected="false" aria-controls="tab-archive" role="tab" data-bs-toggle="tab">{{ lang.admin.archive }}</button></li>
<li role="presentation" class="nav-item"><button class="nav-link" data-bs-target="#tab-sys-mails" aria-selected="false" aria-controls="tab-sys-mails" role="tab" data-bs-toggle="tab">{{ lang.admin.sys_mails }}</button></li>
@@ -58,6 +59,7 @@
{% include 'admin/tab-config-rsettings.twig' %}
{% include 'admin/tab-config-customize.twig' %}
{% include 'admin/tab-config-password-settings.twig' %}
{% include 'admin/tab-config-backup.twig' %}
{% include 'admin/tab-sys-mails.twig' %}
{% include 'admin/tab-globalfilter-regex.twig' %}
</div>
@@ -66,9 +68,13 @@
</div>
{% include 'modals/admin.twig' %}
{% include 'modals/backup-job.twig' %}
<script type='text/javascript'>
var lang = {{ lang_admin|raw }};
lang.backup = {{ lang_backup|raw }};
var backup_timezone = '{{ backup_timezone|e('js') }}';
var backup_hostname = '{{ backup_hostname|e('js') }}';
var lang_datatables = {{ lang_datatables|raw }};
var admin_username = '{{ mailcow_cc_username }}';
var csrf_token = '{{ csrf_token }}';
@@ -0,0 +1,378 @@
{# Backup with restic: status, actions, settings, jobs and snapshots, filled by js/site/backup.js #}
{% set legend_style = 'font-family:var(--bs-body-font-family)' %}
<div class="tab-pane fade" id="tab-config-backup" role="tabpanel" aria-labelledby="tab-config-backup">
<div class="card mb-4">
<div class="card-header d-flex align-items-center fs-5">
<button class="btn d-md-none flex-grow-1 text-start" data-bs-target="#collapse-tab-config-backup" data-bs-toggle="collapse" aria-controls="collapse-tab-config-backup">
{{ lang.backup.tab }}
</button>
<span class="d-none d-md-block">{{ lang.backup.tab }}</span>
<button type="button" class="btn btn-xs btn-secondary ms-auto backup-refresh" title="{{ lang.backup.refresh }}" aria-label="{{ lang.backup.refresh }}"><i class="bi bi-arrow-clockwise"></i></button>
</div>
<div id="collapse-tab-config-backup" class="card-body collapse" data-bs-parent="#admin-content">
<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>
<div id="backup_status">
<div class="backup-status-summary mb-2" aria-live="polite"><span class="text-muted">{{ lang.footer.loading }}</span></div>
<div class="d-flex flex-wrap gap-2 mb-3">
<button type="button" class="btn btn-sm btn-xs-lg btn-success backup-job-request" data-type="backup" title="{{ lang.backup.backup_now_info }}"><i class="bi bi-cloud-arrow-up"></i> {{ lang.backup.backup_now }}</button>
<button type="button" class="btn btn-sm btn-xs-lg btn-secondary backup-job-request" data-type="init" title="{{ lang.backup.test_connection_info }}"><i class="bi bi-plug"></i> {{ lang.backup.test_connection }}</button>
<div class="btn-group">
<button type="button" class="btn btn-sm btn-xs-lg btn-secondary dropdown-toggle backup-maintenance" data-bs-toggle="dropdown" aria-expanded="false"><i class="bi bi-tools"></i> {{ lang.backup.maintenance }}</button>
<ul class="dropdown-menu">
<li><a class="dropdown-item backup-job-request" href="#" data-type="check" title="{{ lang.backup.check_info }}"><i class="bi bi-shield-check"></i> {{ lang.backup.check_now }}</a></li>
<li><a class="dropdown-item backup-job-request" href="#" data-type="prune" title="{{ lang.backup.prune_info }}"><i class="bi bi-scissors"></i> {{ lang.backup.prune_now }}</a></li>
</ul>
</div>
</div>
<div id="backup_status_alerts"></div>
</div>
</div>
</div>
<form class="form-horizontal" id="backup_settings" role="form" autocapitalize="none" autocorrect="off" autocomplete="off">
<legend class="fs-5 fw-semibold mb-0" style="{{ legend_style }}">{{ lang.backup.settings }}</legend>
<hr />
{# RESTIC_BACKUP_MODE of mailcow.conf as the backup container reports it, filled by js/site/backup.js #}
<div class="row mb-2">
<div class="col-sm-3 d-flex align-items-start justify-content-sm-end pt-sm-2">
<i style="font-size: 16px; cursor: pointer;" class="bi bi-patch-question-fill mx-2 ms-0" data-bs-toggle="tooltip" data-bs-placement="bottom" title="{{ lang.backup.scope_info }}"></i>
<span class="control-label">{{ lang.backup.scope }}</span>
</div>
<div class="col-sm-9 pt-sm-2 backup-scope">
<div><span class="fw-semibold backup-scope-title"></span> <small class="text-muted font-monospace ms-1 backup-scope-var"></small></div>
<div class="small mt-1 d-none backup-scope-in"><i class="bi bi-check-lg text-success me-1"></i><span></span></div>
<div class="small d-none backup-scope-out"><i class="bi bi-x-lg text-danger me-1"></i><span></span></div>
</div>
</div>
<div class="row mb-2">
<div class="col-sm-3 d-flex align-items-start justify-content-sm-end pt-sm-2">
<label class="control-label" for="backup_target">{{ lang.backup.target }}</label>
</div>
<div class="col-sm-9">
<select class="form-control" name="target" id="backup_target">
<option value="s3">{{ lang.backup.target_s3 }}</option>
<option value="sftp">{{ lang.backup.target_sftp }}</option>
<option value="rest">{{ lang.backup.target_rest }}</option>
<option value="local">{{ lang.backup.target_local }}</option>
</select>
</div>
</div>
<div class="backup-target" data-target="s3">
<div class="row mb-2">
<div class="col-sm-3 d-flex align-items-start justify-content-sm-end pt-sm-2">
<i style="font-size: 16px; cursor: pointer;" class="bi bi-patch-question-fill mx-2 ms-0" data-bs-toggle="tooltip" data-bs-placement="bottom" title="{{ lang.backup.s3_endpoint_info }}"></i>
<label class="control-label" for="backup_s3_endpoint">{{ lang.backup.s3_endpoint }}</label>
</div>
<div class="col-sm-9">
<input type="url" class="form-control" name="s3_endpoint" id="backup_s3_endpoint" maxlength="255" placeholder="https://s3.eu-central-1.amazonaws.com">
</div>
</div>
<div class="row mb-2">
<div class="col-sm-3 d-flex align-items-start justify-content-sm-end pt-sm-2">
<label class="control-label" for="backup_s3_bucket">{{ lang.backup.s3_bucket }}</label>
</div>
<div class="col-sm-9">
<input type="text" class="form-control" name="s3_bucket" id="backup_s3_bucket" maxlength="63">
</div>
</div>
<div class="row mb-2">
<div class="col-sm-3 d-flex align-items-start justify-content-sm-end pt-sm-2">
<label class="control-label" for="backup_s3_path">{{ lang.backup.s3_path }}</label>
</div>
<div class="col-sm-9">
<div class="row g-2">
<div class="col-md-7"><input type="text" class="form-control" name="s3_path" id="backup_s3_path" maxlength="255" placeholder="{{ lang.backup.optional }}"></div>
<div class="col-md-5">
<div class="input-group">
<label class="input-group-text" for="backup_s3_region">{{ lang.backup.s3_region }}</label>
<input type="text" class="form-control" name="s3_region" id="backup_s3_region" maxlength="64" placeholder="{{ lang.backup.optional }}">
</div>
</div>
</div>
</div>
</div>
<div class="row mb-2">
<div class="col-sm-3 d-flex align-items-start justify-content-sm-end pt-sm-2">
<label class="control-label" for="backup_s3_access_key">{{ lang.backup.s3_access_key }}</label>
</div>
<div class="col-sm-9">
<input type="text" class="form-control" name="s3_access_key" id="backup_s3_access_key" maxlength="255">
</div>
</div>
<div class="row mb-2">
<div class="col-sm-3 d-flex align-items-start justify-content-sm-end pt-sm-2">
<label class="control-label" for="backup_s3_secret_key">{{ lang.backup.s3_secret_key }}</label>
</div>
<div class="col-sm-9">
<input type="password" class="form-control backup-secret" name="s3_secret_key" id="backup_s3_secret_key" maxlength="255" autocomplete="new-password" data-formcache-skip>
</div>
</div>
<div class="row mb-3">
<div class="offset-sm-3 col-sm-9">
<div class="form-check">
<input type="checkbox" class="form-check-input" name="s3_path_style" value="1" id="backup_s3_path_style">
<label class="form-check-label" for="backup_s3_path_style">{{ lang.backup.s3_path_style }}</label>
</div>
</div>
</div>
</div>
<div class="backup-target" data-target="sftp">
<div class="row mb-2">
<div class="col-sm-3 d-flex align-items-start justify-content-sm-end pt-sm-2">
<label class="control-label" for="backup_sftp_host">{{ lang.backup.sftp_host }}</label>
</div>
<div class="col-sm-9">
<div class="row g-2">
<div class="col-md-8"><input type="text" class="form-control" name="sftp_host" id="backup_sftp_host" maxlength="253" placeholder="backup.example.org"></div>
<div class="col-md-4">
<div class="input-group">
<label class="input-group-text" for="backup_sftp_port">{{ lang.backup.sftp_port }}</label>
<input type="number" class="form-control" name="sftp_port" id="backup_sftp_port" min="1" max="65535">
</div>
</div>
</div>
</div>
</div>
<div class="row mb-2">
<div class="col-sm-3 d-flex align-items-start justify-content-sm-end pt-sm-2">
<label class="control-label" for="backup_sftp_user">{{ lang.backup.sftp_user }}</label>
</div>
<div class="col-sm-9">
<div class="row g-2">
<div class="col-md-5"><input type="text" class="form-control" name="sftp_user" id="backup_sftp_user" maxlength="64"></div>
<div class="col-md-7">
<div class="input-group">
<label class="input-group-text" for="backup_sftp_path">{{ lang.backup.sftp_path }}</label>
<input type="text" class="form-control" name="sftp_path" id="backup_sftp_path" maxlength="255" placeholder="mailcow" title="{{ lang.backup.sftp_path_info }}">
</div>
</div>
</div>
</div>
</div>
<div class="row mb-3">
<div class="col-sm-3 d-flex align-items-start justify-content-sm-end pt-sm-2">
<i style="font-size: 16px; cursor: pointer;" class="bi bi-patch-question-fill mx-2 ms-0" data-bs-toggle="tooltip" data-bs-placement="bottom" title="{{ lang.backup.sftp_public_key_info }}"></i>
<label class="control-label" for="backup_sftp_public_key">{{ lang.backup.sftp_public_key }}</label>
</div>
<div class="col-sm-9">
<div class="input-group">
<input type="text" class="form-control font-monospace" id="backup_sftp_public_key" readonly>
<button class="btn btn-secondary backup-copy" type="button" data-copy="#backup_sftp_public_key"><i class="bi bi-clipboard"></i> {{ lang.backup.copy }}</button>
</div>
<small class="text-muted d-none backup-key-missing">{{ lang.backup.sftp_public_key_missing }}</small>
<small class="text-muted d-none text-break backup-host-key" title="{{ lang.backup.sftp_host_key_info }}">{{ lang.backup.sftp_host_key }}: <code></code></small>
</div>
</div>
</div>
<div class="backup-target" data-target="rest">
<div class="row mb-2">
<div class="col-sm-3 d-flex align-items-start justify-content-sm-end pt-sm-2">
<i style="font-size: 16px; cursor: pointer;" class="bi bi-patch-question-fill mx-2 ms-0" data-bs-toggle="tooltip" data-bs-placement="bottom" title="{{ lang.backup.rest_url_info }}"></i>
<label class="control-label" for="backup_rest_url">{{ lang.backup.rest_url }}</label>
</div>
<div class="col-sm-9">
<input type="url" class="form-control" name="rest_url" id="backup_rest_url" maxlength="255" placeholder="https://backup.example.org:8000/mailcow">
</div>
</div>
<div class="row mb-3">
<div class="col-sm-3 d-flex align-items-start justify-content-sm-end pt-sm-2">
<label class="control-label" for="backup_rest_user">{{ lang.backup.rest_user }}</label>
</div>
<div class="col-sm-9">
<div class="row g-2">
<div class="col-md-5"><input type="text" class="form-control" name="rest_user" id="backup_rest_user" maxlength="64" placeholder="{{ lang.backup.optional }}"></div>
<div class="col-md-7">
<div class="input-group">
<label class="input-group-text" for="backup_rest_password">{{ lang.backup.rest_password }}</label>
<input type="password" class="form-control backup-secret" name="rest_password" id="backup_rest_password" maxlength="255" autocomplete="new-password" data-formcache-skip>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="backup-target" data-target="local">
<div class="row mb-3">
<div class="col-sm-3 d-flex align-items-start justify-content-sm-end pt-sm-2">
<i style="font-size: 16px; cursor: pointer;" class="bi bi-patch-question-fill mx-2 ms-0" data-bs-toggle="tooltip" data-bs-placement="bottom" title="{{ lang.backup.local_path_info }}"></i>
<label class="control-label" for="backup_local_path">{{ lang.backup.local_path }}</label>
</div>
<div class="col-sm-9">
<input type="text" class="form-control" name="local_path" id="backup_local_path" maxlength="255" placeholder="mailcow">
</div>
</div>
</div>
<div class="row mb-2">
<div class="col-sm-3 d-flex align-items-start justify-content-sm-end pt-sm-2">
<i style="font-size: 16px; cursor: pointer;" class="bi bi-patch-question-fill mx-2 ms-0" data-bs-toggle="tooltip" data-bs-placement="bottom" title="{{ lang.backup.password_info }}"></i>
<label class="control-label" for="backup_password">{{ lang.backup.password }}</label>
</div>
<div class="col-sm-9">
<div class="input-group">
<input type="password" class="form-control backup-secret" name="password" id="backup_password" maxlength="255" autocomplete="new-password" data-formcache-skip>
<button class="btn btn-secondary backup-password-generate" type="button"><i class="bi bi-arrow-repeat"></i> {{ lang.backup.password_generate }}</button>
</div>
<small class="d-none backup-password-change"><i class="bi bi-exclamation-triangle-fill text-warning"></i> {{ lang.backup.password_change }}</small>
<div class="form-check mt-1 backup-password-confirm">
<input type="checkbox" class="form-check-input" name="password_confirmed" value="1" id="backup_password_confirmed">
<label class="form-check-label fw-semibold" for="backup_password_confirmed" title="{{ lang.backup.password_confirmed }}">{{ lang.backup.password_confirmed_short }}</label>
</div>
</div>
</div>
<div class="row mt-3 mb-4">
<div class="col-sm-3 d-flex align-items-start justify-content-sm-end">
<i style="font-size: 16px; cursor: pointer;" class="bi bi-patch-question-fill mx-2 ms-0" data-bs-toggle="tooltip" data-bs-placement="bottom" title="{{ lang.backup.schedule_time_info|format(backup_timezone) }}"></i>
<label class="control-label" for="backup_enabled">{{ lang.backup.schedule }}</label>
</div>
<div class="col-sm-9">
<div class="form-check form-switch mb-3">
<input type="checkbox" class="form-check-input" role="switch" name="enabled" value="1" id="backup_enabled">
<label class="form-check-label" for="backup_enabled">{{ lang.backup.enabled_label }}</label>
</div>
{# a new entry of the schedule (weekdays and time, no names); js/site/backup.js lists the entries below as schedule #}
<div class="d-flex flex-wrap align-items-center column-gap-4 row-gap-2 backup-schedule-new" data-setting="schedule">
<div class="btn-group backup-schedule-days" role="group" aria-label="{{ lang.backup.schedule_days }}">
{% for day in [1, 2, 3, 4, 5, 6, 0] %}
<input type="checkbox" class="btn-check" id="backup_schedule_day_{{ day }}" value="{{ day }}" autocomplete="off">
<label class="btn btn-outline-secondary btn-check-label" for="backup_schedule_day_{{ day }}" title="{{ lang.backup['weekday_' ~ day] }}">{{ lang.backup['weekday_' ~ day]|slice(0, 2) }}</label>
{% endfor %}
</div>
<div class="d-flex flex-wrap align-items-center gap-2">
<span>{{ lang.backup.schedule_at }}</span>
<input type="time" class="form-control w-auto backup-schedule-time" aria-label="{{ lang.backup.schedule_time }}">
<button type="button" class="btn btn-secondary backup-schedule-add"><i class="bi bi-plus-lg"></i> {{ lang.backup.schedule_add }}</button>
</div>
</div>
<ol class="mt-3 mb-0 ps-4 backup-schedule-list" aria-label="{{ lang.backup.schedule }}"></ol>
<div class="mt-3 text-muted d-none backup-schedule-none">{{ lang.backup.schedule_none }}</div>
</div>
</div>
<div class="row mb-2" role="group" aria-labelledby="backup_keep_label">
<div class="col-sm-3 d-flex align-items-start justify-content-sm-end pt-sm-2">
<i style="font-size: 16px; cursor: pointer;" class="bi bi-patch-question-fill mx-2 ms-0" data-bs-toggle="tooltip" data-bs-placement="bottom" title="{{ lang.backup.keep_info }}"></i>
<div class="control-label" id="backup_keep_label">{{ lang.backup.keep }}</div>
</div>
<div class="col-sm-9">
<div class="d-flex flex-wrap align-items-center gap-2">
{% for keep in ['hourly', 'daily', 'weekly', 'monthly', 'yearly'] %}
<div class="input-group w-auto backup-keep-{{ keep }}">
<input type="number" class="form-control" style="max-width:5.5rem" name="keep_{{ keep }}" id="backup_keep_{{ keep }}" min="0" max="1000">
<label class="input-group-text" for="backup_keep_{{ keep }}">{{ lang.backup['keep_' ~ keep] }}</label>
</div>
{% endfor %}
</div>
</div>
</div>
<div class="row mb-3">
<div class="col-sm-3 d-flex align-items-start justify-content-sm-end pt-sm-2">
<i style="font-size: 16px; cursor: pointer;" class="bi bi-patch-question-fill mx-2 ms-0" data-bs-toggle="tooltip" data-bs-placement="bottom" title="{{ lang.backup.notify_email_info }}"></i>
<label class="control-label" for="backup_notify_email">{{ lang.backup.notify_email }}</label>
</div>
<div class="col-sm-9">
<input type="text" class="form-control" name="notify_email" id="backup_notify_email" placeholder="admin@example.org, ops@example.org">
</div>
</div>
<legend class="fs-6 fw-semibold mb-0 mt-3" style="{{ legend_style }}">
<button type="button" class="btn btn-link p-0 text-reset text-decoration-none fw-semibold backup-collapse-toggle" data-bs-toggle="collapse" data-bs-target="#backup_advanced" aria-expanded="false" aria-controls="backup_advanced"><i class="bi bi-plus-square"></i> {{ lang.backup.advanced }}</button>
<small class="text-muted fw-normal">({{ lang.backup.advanced_info }})</small>
</legend>
<hr />
<div id="backup_advanced" class="collapse">
<div class="row mb-2">
<div class="col-sm-3 d-flex align-items-start justify-content-sm-end pt-sm-2">
<i style="font-size: 16px; cursor: pointer;" class="bi bi-patch-question-fill mx-2 ms-0" data-bs-toggle="tooltip" data-bs-placement="bottom" title="{{ lang.backup.prune_weekday_info }}"></i>
<label class="control-label" for="backup_prune_weekday">{{ lang.backup.prune_weekday }}</label>
</div>
<div class="col-sm-9">
<select class="form-control" name="prune_weekday" id="backup_prune_weekday">
{% for day in [1, 2, 3, 4, 5, 6, 0] %}
<option value="{{ day }}">{{ lang.backup['weekday_' ~ day] }}</option>
{% endfor %}
<option value="">{{ lang.backup.weekday_never }}</option>
</select>
</div>
</div>
<div class="row mb-2">
<div class="col-sm-3 d-flex align-items-start justify-content-sm-end pt-sm-2">
<i style="font-size: 16px; cursor: pointer;" class="bi bi-patch-question-fill mx-2 ms-0" data-bs-toggle="tooltip" data-bs-placement="bottom" title="{{ lang.backup.max_unused_info }}"></i>
<label class="control-label" for="backup_max_unused">{{ lang.backup.max_unused }}</label>
</div>
<div class="col-sm-9">
<div class="input-group w-auto d-inline-flex">
<input type="number" class="form-control" style="max-width:6rem" name="max_unused" id="backup_max_unused" min="0" max="100">
<span class="input-group-text">%</span>
</div>
</div>
</div>
<div class="row mb-2">
<div class="col-sm-3 d-flex align-items-start justify-content-sm-end pt-sm-2">
<i style="font-size: 16px; cursor: pointer;" class="bi bi-patch-question-fill mx-2 ms-0" data-bs-toggle="tooltip" data-bs-placement="bottom" title="{{ lang.backup.check_subset_info }}"></i>
<label class="control-label" for="backup_check_subset">{{ lang.backup.check_subset }}</label>
</div>
<div class="col-sm-9">
<div class="input-group w-auto d-inline-flex">
<input type="number" class="form-control" style="max-width:6rem" name="check_subset" id="backup_check_subset" min="0" max="365">
<span class="input-group-text">{{ lang.backup.check_subset_unit }}</span>
</div>
</div>
</div>
<div class="row mb-2">
<div class="offset-sm-3 col-sm-9">
<div class="form-check">
<input type="checkbox" class="form-check-input" name="include_index" value="1" id="backup_include_index">
<label class="form-check-label" for="backup_include_index">{{ lang.backup.include_index }}</label>
</div>
<div class="form-check ms-4">
<input type="checkbox" class="form-check-input" name="include_fts" value="1" id="backup_include_fts">
<label class="form-check-label" for="backup_include_fts">{{ lang.backup.include_fts }}</label>
</div>
</div>
</div>
<div class="row mb-3">
<div class="col-sm-3 d-flex align-items-start justify-content-sm-end pt-sm-2">
<i style="font-size: 16px; cursor: pointer;" class="bi bi-patch-question-fill mx-2 ms-0" data-bs-toggle="tooltip" data-bs-placement="bottom" title="{{ lang.backup.limit_upload_info }}"></i>
<label class="control-label" for="backup_limit_upload">{{ lang.backup.limit_upload }}</label>
</div>
<div class="col-sm-9">
<div class="input-group w-auto d-inline-flex">
<input type="number" class="form-control" style="max-width:9rem" name="limit_upload" id="backup_limit_upload" min="0" max="100000000">
<span class="input-group-text">KiB/s</span>
</div>
</div>
</div>
</div>
<div class="row">
<div class="offset-sm-3 col-sm-9">
<button type="submit" class="btn btn-sm btn-xs-lg d-block d-sm-inline btn-success backup-settings-save"><i class="bi bi-check-lg"></i> {{ lang.backup.save }}</button>
</div>
</div>
</form>
<div class="backup-jobs-section d-none mt-4">
<legend class="fs-5 fw-semibold mb-0" style="{{ legend_style }}">{{ lang.backup.jobs }}</legend>
<hr />
<table id="backup_jobs_table" class="table table-striped dt-responsive w-100"></table>
</div>
<div class="backup-snapshots-section d-none mt-4">
<legend class="fs-5 fw-semibold mb-0" style="{{ legend_style }}">
<button type="button" class="btn btn-link p-0 text-reset text-decoration-none fw-semibold fs-5 backup-collapse-toggle" data-bs-toggle="collapse" data-bs-target="#backup_snapshots_collapse" aria-expanded="false" aria-controls="backup_snapshots_collapse"><i class="bi bi-plus-square"></i> {{ lang.backup.snapshots }} (<span class="backup-snapshot-count">0</span>)</button>
</legend>
<hr />
<div id="backup_snapshots_collapse" class="collapse">
<table id="backup_snapshots_table" class="table table-striped dt-responsive w-100"></table>
</div>
</div>
</div>
</div>
</div>
+2
View File
@@ -75,6 +75,7 @@
<a href="#" class="nav-link dropdown-toggle" data-bs-toggle="dropdown" role="button" aria-expanded="false">{{ lang.header.email }}</a>
<ul class="dropdown-menu">
<li><a href="/admin/mailbox" class="dropdown-item {% if is_uri('mailbox') %}active{% endif %}">{{ lang.header.mailcow_config }}</a></li>
<li><a href="/admin/restore" class="dropdown-item {% if is_uri('restore') %}active{% endif %}">{{ lang.backup.nav }}</a></li>
<li><a href="/quarantine" class="dropdown-item {% if is_uri('quarantine') %}active{% endif %}">{{ lang.header.quarantine }}</a></li>
<li><a href="/admin/queue" class="dropdown-item {% if is_uri('queue') %}active{% endif %}">{{ lang.queue.queue_manager }}</a></li>
<li><a href="#" class="dropdown-item" data-bs-toggle="modal" data-container="sogo-mailcow" data-bs-target="#RestartContainer">{{ lang.header.restart_sogo }}</a></li>
@@ -96,6 +97,7 @@
<a href="#" class="nav-link dropdown-toggle" data-bs-toggle="dropdown" role="button" aria-expanded="false">{{ lang.header.email }}</a>
<ul class="dropdown-menu">
<li><a href="/domainadmin/mailbox" class="dropdown-item {% if is_uri('mailbox') %}active{% endif %}">{{ lang.header.mailcow_config }}</a></li>
<li><a href="/domainadmin/restore" class="dropdown-item {% if is_uri('restore') %}active{% endif %}">{{ lang.backup.nav }}</a></li>
<li><a href="/quarantine" class="dropdown-item {% if is_uri('quarantine') %}active{% endif %}">{{ lang.header.quarantine }}</a></li>
</ul>
</li>
+19
View File
@@ -0,0 +1,19 @@
{# Details of a backup job: overview, restore result per mailbox and item, log; filled by js/site/backup.js #}
<div class="modal fade" id="backupJobModal" tabindex="-1" role="dialog" aria-labelledby="backupJobModalTitle" aria-hidden="true">
<div class="modal-dialog modal-xl modal-dialog-scrollable">
<div class="modal-content">
<div class="modal-header">
<h3 class="modal-title" id="backupJobModalTitle"></h3>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="{{ lang.backup.close }}"></button>
</div>
<div class="modal-body">
<div class="backup-job-body"></div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-sm btn-xs-lg btn-danger me-auto backup-job-cancel d-none"><i class="bi bi-x-circle"></i> {{ lang.backup.cancel_job }}</button>
<button type="button" class="btn btn-sm btn-xs-lg btn-secondary backup-job-refresh d-none"><i class="bi bi-arrow-clockwise"></i> {{ lang.backup.refresh }}</button>
<button type="button" class="btn btn-sm btn-xs-lg btn-secondary" data-bs-dismiss="modal">{{ lang.backup.close }}</button>
</div>
</div>
</div>
</div>
@@ -0,0 +1,112 @@
{# Restore modal of E-Mail > Restore: what is restored, the items, the mail target; js/site/backup.js opens and sends it.
Domain admins only see the items they have the ACL for (the API refuses the others as well). For deleted targets the
overwrite block is hidden and SOGo moves into "Calendars and contacts" (nothing to overwrite). #}
{% macro component(key, label, note, sub) %}
<div class="form-check{% if sub %} ms-4{% endif %}">
<input class="form-check-input backup-component" type="checkbox" value="{{ key }}" id="backup_restore_{{ key }}"{% if sub %} data-parent="{{ sub }}"{% endif %}>
<label class="form-check-label" for="backup_restore_{{ key }}">{{ label }}</label>
{% if note %}<div class="small text-muted">{{ note }}</div>{% endif %}
</div>
{% endmacro %}
{% import _self as restore %}
{% set is_admin = mailcow_cc_role == 'admin' %}
<div class="modal fade" id="backupRestoreModal" tabindex="-1" role="dialog" aria-labelledby="backupRestoreModalTitle" aria-hidden="true">
<div class="modal-dialog modal-xl modal-dialog-scrollable">
<form class="modal-content" id="backup_restore_form" role="form" autocomplete="off">
<div class="modal-header">
<div class="text-break">
<h3 class="modal-title" id="backupRestoreModalTitle">{{ lang.backup.restore_title }}</h3>
<div class="small text-muted backup-restore-subtitle"></div>
</div>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="{{ lang.backup.close }}"></button>
</div>
<div class="modal-body">
<div class="row mb-3">
<div class="col-sm-3 col-lg-2 text-sm-end">{{ lang.backup.restore_what }}</div>
<div class="col-sm-9 col-lg-10">
<div class="backup-restore-summary text-break"></div>
<p class="text-muted small mb-1 mt-2">{{ lang.backup.restore_info }}</p>
<p class="text-muted small mb-1"><i class="bi bi-info-circle"></i> {{ lang.backup.note_inactive }}</p>
<div class="alert alert-info py-2 mb-0 d-none backup-create-note"><i class="bi bi-info-circle"></i> {{ lang.backup.create_note }}</div>
</div>
</div>
<div class="row mb-2">
<div class="col-sm-3 col-lg-2 text-sm-end">{{ lang.backup.components }}</div>
<div class="col-sm-9 col-lg-10">
<p class="small mb-2 d-none backup-no-mail-note"><i class="bi bi-exclamation-triangle-fill text-warning"></i> {{ lang.backup.no_mail_data }}</p>
<div class="row">
<div class="col-lg-6">
<fieldset class="mb-3">
<legend class="fs-6 fw-bold mb-1">{{ lang.backup.group_mail }}</legend>
{{ restore.component('mails', lang.backup.component_mails) }}
<div class="ms-4 mb-1 backup-mail-target" role="radiogroup" aria-label="{{ lang.backup.mail_target }}">
<div class="form-check">
<input class="form-check-input" type="radio" name="mail_target" value="folder" id="backup_restore_target_folder" checked>
<label class="form-check-label" for="backup_restore_target_folder">{{ lang.backup.mail_target_folder|e|format('<span class="backup-folder-name"></span>')|raw }}</label>
</div>
<div class="form-check">
<input class="form-check-input" type="radio" name="mail_target" value="original" id="backup_restore_target_original">
<label class="form-check-label" for="backup_restore_target_original">{{ lang.backup.mail_target_original }}</label>
</div>
</div>
{{ restore.component('imap_acl', lang.backup.component_imap_acl) }}
</fieldset>
<fieldset class="mb-3 d-none backup-sogo-normal">
<legend class="fs-6 fw-bold mb-1">{{ lang.backup.group_calendar }}</legend>
</fieldset>
<fieldset class="mb-3">
<legend class="fs-6 fw-bold mb-1">{{ lang.backup.group_addresses }}</legend>
{{ restore.component('aliases', lang.backup.component_aliases) }}
{% if is_admin or acl.mailbox_groups == 1 %}{{ restore.component('groups', lang.backup.component_groups) }}{% endif %}
{{ restore.component('sender_acl', lang.backup.component_sender_acl) }}
{% if is_admin or acl.mail_policies == 1 %}{{ restore.component('mail_policies', lang.backup.component_mail_policies) }}{% endif %}
{% if is_admin or acl.retention == 1 %}{{ restore.component('retention_rules', lang.backup.component_retention_rules, lang.backup.note_retention_rules) }}{% endif %}
</fieldset>
</div>
<div class="col-lg-6">
{% if is_admin or acl.filters == 1 or acl.spam_policy == 1 or acl.bcc_maps == 1 %}
<fieldset class="mb-3">
<legend class="fs-6 fw-bold mb-1">{{ lang.backup.group_filters }}</legend>
{% if is_admin or acl.filters == 1 %}
{{ restore.component('filters', lang.backup.component_filters) }}
{{ restore.component('sieve_activate', lang.backup.component_sieve_activate, lang.backup.note_sieve_activate, 'filters') }}
{% endif %}
{% if is_admin or acl.spam_policy == 1 %}{{ restore.component('spam', lang.backup.component_spam) }}{% endif %}
{% if is_admin or acl.bcc_maps == 1 %}{{ restore.component('bcc', lang.backup.component_bcc) }}{% endif %}
</fieldset>
{% endif %}
<fieldset class="mb-3">
<legend class="fs-6 fw-bold mb-1">{{ lang.backup.group_access }}</legend>
{% if is_admin or acl.syncjobs == 1 %}{{ restore.component('syncjobs', lang.backup.component_syncjobs) }}{% endif %}
{% if is_admin or acl.app_passwds == 1 %}{{ restore.component('app_passwords', lang.backup.component_app_passwords, lang.backup.note_app_passwords) }}{% endif %}
{{ restore.component('tfa', lang.backup.component_tfa, lang.backup.note_tfa) }}
</fieldset>
<fieldset class="border border-warning rounded px-3 py-2 mb-3 backup-overwrite">
<legend class="fs-6 fw-bold mb-1 float-none w-auto px-1"><i class="bi bi-exclamation-triangle-fill text-warning"></i> {{ lang.backup.overwrite }}</legend>
<p class="small text-muted mb-1">{{ lang.backup.overwrite_info }}</p>
<div class="form-check backup-sogo-item">
<input class="form-check-input backup-component" type="checkbox" value="sogo" id="backup_restore_sogo">
<label class="form-check-label" for="backup_restore_sogo" data-overwrite="{{ lang.backup.component_sogo }}" data-normal="{{ lang.backup.component_sogo_new }}">{{ lang.backup.component_sogo }}</label>
<div class="small text-muted backup-sogo-note">
{{ lang.backup.note_sogo }}
<button type="button" class="btn btn-link btn-sm p-0 align-baseline" data-bs-toggle="collapse" data-bs-target="#backup_sogo_more" aria-expanded="false" aria-controls="backup_sogo_more">{{ lang.backup.more }}</button>
<div class="collapse" id="backup_sogo_more">{{ lang.backup.note_sogo_more }}</div>
</div>
</div>
{{ restore.component('settings', lang.backup.component_settings, lang.backup.note_settings) }}
{{ restore.component('password', lang.backup.component_password) }}
</fieldset>
</div>
</div>
</div>
</div>
<div class="text-danger small d-none backup-restore-error" role="alert"></div>
</div>
<div class="modal-footer">
<small class="text-muted me-auto">{{ lang.backup.restore_background }}</small>
<button type="button" class="btn btn-sm btn-xs-lg btn-secondary" data-bs-dismiss="modal">{{ lang.backup.cancel }}</button>
<button type="submit" class="btn btn-sm btn-xs-lg btn-success backup-restore-submit" data-normal="{{ lang.backup.restore }}" data-overwrite="{{ lang.backup.restore_overwrite }}"><i class="bi bi-clock-history"></i> <span>{{ lang.backup.restore }}</span></button>
</div>
</form>
</div>
</div>
+81
View File
@@ -0,0 +1,81 @@
{% extends 'base.twig' %}
{# E-Mail > Restore (admins, domain admins): snapshot, a tree of its domains and mailboxes (also deleted ones) with
a selection, the restore modal, the restores. Filled by js/site/backup.js. #}
{% block content %}
{% set is_admin = mailcow_cc_role == 'admin' %}
<div id="backup_restore_page">
<div class="card mb-4">
<div class="card-header d-flex align-items-center fs-5">
<span>{{ lang.backup.restore_card }}</span>
</div>
<div class="card-body">
<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">
<label class="fw-semibold mb-0" for="backup_restore_snapshot">{{ lang.backup.snapshot_label }}</label>
<div class="flex-grow-1" style="min-width:16rem;max-width:28rem">
<select class="form-control" id="backup_restore_snapshot" data-live-search="true" data-show-subtext="true" data-width="100%"></select>
</div>
</div>
<div class="backup-tree-panel d-none">
<div class="backup-content-note"></div>
<div class="backup-restore-queued"></div>
<div class="position-sticky py-2 px-2 rounded backup-tree-toolbar" style="top:0;z-index:5;background:var(--bs-card-bg,var(--bs-body-bg))">
<div class="d-flex flex-wrap align-items-center gap-2">
<div class="flex-grow-1" style="min-width:12rem;max-width:28rem">
<input type="search" class="form-control form-control-sm backup-tree-search" placeholder="{{ lang.backup.search_placeholder }}" aria-label="{{ lang.backup.search_placeholder }}" autocomplete="off">
</div>
<div class="form-check form-switch mb-0">
<input class="form-check-input backup-tree-deleted" type="checkbox" role="switch" id="backup_tree_deleted">
<label class="form-check-label small" for="backup_tree_deleted">{{ lang.backup.only_deleted }}</label>
</div>
<div class="d-flex flex-wrap align-items-center gap-2 ms-auto">
<span class="small text-muted backup-selection-text" aria-live="polite"></span>
<button type="button" class="btn btn-sm btn-link px-1 backup-selection-clear d-none">{{ lang.backup.clear_selection }}</button>
<button type="button" class="btn btn-sm btn-xs-lg btn-success backup-selection-restore" disabled><i class="bi bi-clock-history"></i> {{ lang.backup.restore }}</button>
</div>
</div>
<div class="small mt-1 d-none backup-selection-hint" role="status"></div>
</div>
<table class="table table-sm table-hover align-middle mb-0 backup-tree" id="backup_tree">
<thead>
<tr>
<th>{{ lang.backup.name }}</th>
<th class="d-none d-md-table-cell">{{ lang.backup.state }}</th>
<th class="d-none d-sm-table-cell">{{ lang.backup.mailboxes }}</th>
<th class="text-end"><span class="visually-hidden">{{ lang.backup.restore }}</span></th>
</tr>
</thead>
{# not empty: mailcow removes empty tbody elements on load #}
<tbody><tr class="backup-tree-message"><td colspan="4" class="text-muted">{{ lang.footer.loading }}</td></tr></tbody>
</table>
</div>
</div>
</div>
<div class="card mb-4">
<div class="card-header d-flex align-items-center fs-5">
<span>{{ lang.backup.restores }}</span>
<button type="button" class="btn btn-xs btn-secondary ms-auto backup-table-refresh" data-table="backup_restores_table" title="{{ lang.backup.refresh }}" aria-label="{{ lang.backup.refresh }}"><i class="bi bi-arrow-clockwise"></i></button>
</div>
<div class="card-body">
<p class="text-muted small">{{ lang.backup.restores_info }}</p>
<table id="backup_restores_table" class="table table-striped dt-responsive w-100"></table>
</div>
</div>
</div>
{% include 'modals/backup-restore.twig' %}
{% include 'modals/backup-job.twig' %}
<script type='text/javascript'>
var lang = {backup: {{ lang_backup|raw }}};
var lang_datatables = {{ lang_datatables|raw }};
var csrf_token = '{{ csrf_token }}';
var pagination_size = Math.trunc('{{ pagination_size }}');
var role = '{{ role }}';
var backup_configured = {{ backup_configured ? 'true' : 'false' }};
var backup_timezone = '{{ backup_timezone|e('js') }}';
</script>
{% endblock %}