mirror of
https://github.com/mailcow/mailcow-dockerized.git
synced 2026-11-10 10:31:01 +00:00
Merge branch 'feat/backup' into nightly
This commit is contained in:
66 files changed
+15359
-97
No files matched your search
@@ -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>
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
@@ -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 %}
|
||||
Reference in new issue
Block a user