A real OAuth-authorized account has its own "My Drive" with actual storage, so unlike the service-account path (which has none of its own and can only write into an explicitly shared-by-ID folder), there's no need to hunt down a numeric folder ID — a plain path segment works and rclone creates it automatically on first upload if it doesn't exist yet. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
257 lines
17 KiB
HTML
257 lines
17 KiB
HTML
{% extends "admin/base.html" %}
|
||
{% block page_title %}Резервне копіювання{% endblock %}
|
||
{% block content %}
|
||
<h1>☁️ Резервне копіювання</h1>
|
||
|
||
{% if not is_sqlite %}
|
||
<div class="flash error">Автоматичне копіювання з цієї сторінки підтримується лише для SQLite. Для PostgreSQL налаштуйте pg_dump за розкладом (див. docs/DEPLOYMENT.md).</div>
|
||
{% endif %}
|
||
|
||
<div class="card">
|
||
<div class="card-head">
|
||
<h3>Доступні копії ({{ backups|length }})</h3>
|
||
<form method="post" action="{{ url_for('admin.backups_create') }}">
|
||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
||
<button class="btn" type="submit" {{ 'disabled' if not is_sqlite }}>☁️ Створити резервну копію зараз</button>
|
||
</form>
|
||
</div>
|
||
<div class="table-wrap">
|
||
<table class="responsive-table">
|
||
<tr><th>Файл</th><th>Розмір</th><th>Дата</th><th></th></tr>
|
||
{% for b in backups %}
|
||
<tr>
|
||
<td data-label="Файл" style="word-break:break-all;">{{ b.name }}</td>
|
||
<td data-label="Розмір">{{ b.size_kb }} КБ</td>
|
||
<td data-label="Дата" class="muted">{{ b.created_at.strftime('%d.%m.%Y %H:%M') }}</td>
|
||
<td data-label="">
|
||
<div style="display:flex;gap:6px;flex-wrap:wrap;justify-content:flex-end;">
|
||
<a class="btn small secondary" href="{{ url_for('admin.backups_download', filename=b.name) }}">Завантажити</a>
|
||
<button type="button" class="btn small" onclick="openRestoreDialog('{{ b.name }}')">Відновити</button>
|
||
<form method="post" action="{{ url_for('admin.backups_delete', filename=b.name) }}"
|
||
onsubmit="return confirm('Видалити резервну копію {{ b.name }}? Це незворотньо.');" style="display:inline;">
|
||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
||
<button class="btn small danger" type="submit">Видалити</button>
|
||
</form>
|
||
</div>
|
||
</td>
|
||
</tr>
|
||
{% else %}
|
||
<tr><td colspan="4" class="muted">Резервних копій ще немає</td></tr>
|
||
{% endfor %}
|
||
</table>
|
||
</div>
|
||
</div>
|
||
|
||
<h2>☁️ Автоматичні резервні копії (rclone)</h2>
|
||
<div class="card" style="max-width:640px;">
|
||
{% if remote_last_at %}
|
||
<p class="muted" style="margin-top:0;">
|
||
Останній бекап: {{ remote_last_at.strftime('%d.%m.%Y %H:%M') }} —
|
||
{% if remote_last_status == 'ok' %}<span style="color:var(--success);">успішно ✅</span>{% else %}<span style="color:var(--danger);">помилка: {{ remote_last_status }}</span>{% endif %}
|
||
</p>
|
||
{% endif %}
|
||
|
||
<form method="post" action="{{ url_for('admin.backups_remote_settings') }}">
|
||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
||
<label style="display:flex;align-items:center;gap:8px;">
|
||
<input type="checkbox" name="backup_remote_enabled" class="admin-checkbox" {{ 'checked' if remote_values.backup_remote_enabled == '1' }}>
|
||
Увімкнути автоматичні резервні копії
|
||
</label>
|
||
|
||
<label>Тип сховища</label>
|
||
<select name="backup_remote_type" id="backup-remote-type" onchange="syncBackupRemoteFields()">
|
||
<option value="" {{ 'selected' if remote_values.backup_remote_type == '' }}>— не налаштовано —</option>
|
||
<option value="gdrive" {{ 'selected' if remote_values.backup_remote_type == 'gdrive' }}>Google Drive</option>
|
||
<option value="s3" {{ 'selected' if remote_values.backup_remote_type == 's3' }}>S3-сумісне сховище</option>
|
||
<option value="custom" {{ 'selected' if remote_values.backup_remote_type == 'custom' }}>Інше (для просунутих)</option>
|
||
</select>
|
||
|
||
<div class="row">
|
||
<div>
|
||
<label>Інтервал бекапів, год</label>
|
||
<input type="number" min="1" name="backup_remote_interval_hours" value="{{ remote_values.backup_remote_interval_hours }}">
|
||
</div>
|
||
<div>
|
||
<label>Скільки копій зберігати (ротація)</label>
|
||
<input type="number" min="1" name="backup_remote_retention_count" value="{{ remote_values.backup_remote_retention_count }}">
|
||
</div>
|
||
</div>
|
||
|
||
<div id="fields-gdrive" class="backup-remote-fields">
|
||
<p class="muted field-hint" style="margin-top:0;">Заповніть <b>або</b> «Сервісний акаунт», <b>або</b> «OAuth» нижче — не обидва одразу.</p>
|
||
|
||
<label>Сервісний акаунт: ключ Google (JSON)</label>
|
||
<textarea name="backup_gdrive_service_account_json" rows="4" placeholder='{"type": "service_account", "client_email": "...", ...}'>{{ remote_values.backup_gdrive_service_account_json }}</textarea>
|
||
|
||
<label>ID папки Google Drive <span class="muted">(обов'язково для сервісного акаунта — у нього немає власного диска, тож без явно розшареної папки йому нема куди писати)</span></label>
|
||
<input type="text" name="backup_gdrive_folder_id" value="{{ remote_values.backup_gdrive_folder_id }}" placeholder="1AbCдEfGhIjKlMnOpQrStUvWxYz">
|
||
|
||
<details style="margin-top:12px;">
|
||
<summary style="cursor:pointer;font-weight:700;font-size:13px;">Сервісний акаунт — як налаштувати (крок за кроком)</summary>
|
||
<ol class="muted" style="font-size:12.5px;line-height:1.8;padding-left:20px;margin-top:10px;">
|
||
<li>Відкрийте <a href="https://console.cloud.google.com/" target="_blank">Google Cloud Console</a>, створіть проєкт (або оберіть існуючий).</li>
|
||
<li>Увімкніть <b>Google Drive API</b> для цього проєкту (APIs & Services → Library).</li>
|
||
<li>Створіть сервісний акаунт: IAM & Admin → Service Accounts → Create Service Account.</li>
|
||
<li>Відкрийте створений акаунт → Keys → Add Key → Create new key → JSON. Завантажиться файл.</li>
|
||
<li>Відкрийте цей файл будь-яким текстовим редактором і вставте весь вміст у поле «Ключ сервісного акаунта» вище.</li>
|
||
<li>У Google Drive створіть папку для бекапів, поділіться нею (Share) з email сервісного акаунта (поле <code>client_email</code> у JSON-файлі), давши права «Редактор».</li>
|
||
<li>Скопіюйте ID папки з адресного рядка (частина після <code>/folders/</code>) у поле «ID папки» вище.</li>
|
||
<li>Оберіть «Google Drive» вище, збережіть, натисніть «Перевірити підключення» — якщо все гаразд, спробуйте «Backup зараз».</li>
|
||
</ol>
|
||
</details>
|
||
|
||
<h3 style="margin-top:18px;margin-bottom:6px;font-size:14px;">OAuth (якщо вже маєте дані з <code>rclone config</code>)</h3>
|
||
<div class="row">
|
||
<div>
|
||
<label>client_id</label>
|
||
<input type="text" name="backup_gdrive_client_id" value="{{ remote_values.backup_gdrive_client_id }}">
|
||
</div>
|
||
<div>
|
||
<label>client_secret</label>
|
||
<input type="password" name="backup_gdrive_client_secret" value="{{ remote_values.backup_gdrive_client_secret }}" autocomplete="off">
|
||
</div>
|
||
</div>
|
||
<label>scope <span class="muted">(необов'язково, напр. <code>drive</code>)</span></label>
|
||
<input type="text" name="backup_gdrive_scope" value="{{ remote_values.backup_gdrive_scope }}">
|
||
<label>token</label>
|
||
<textarea name="backup_gdrive_token" rows="3" placeholder='{"access_token": "...", "refresh_token": "...", ...}'>{{ remote_values.backup_gdrive_token }}</textarea>
|
||
|
||
<label>Шлях до папки <span class="muted">(необов'язково — просто назва, без ID; порожньо = корінь «Мій диск». Якщо такої папки ще немає, rclone створить її сам)</span></label>
|
||
<input type="text" name="backup_gdrive_folder_path" value="{{ remote_values.backup_gdrive_folder_path }}" placeholder="напр. Бекапи/BoberBBQ">
|
||
|
||
<details style="margin-top:12px;">
|
||
<summary style="cursor:pointer;font-weight:700;font-size:13px;">OAuth — як налаштувати (крок за кроком)</summary>
|
||
<ol class="muted" style="font-size:12.5px;line-height:1.8;padding-left:20px;margin-top:10px;">
|
||
<li>Встановіть rclone на своєму комп'ютері (не на сервері) — <a href="https://rclone.org/downloads/" target="_blank">rclone.org/downloads</a>.</li>
|
||
<li>Запустіть <code>rclone config</code>, оберіть «New remote» → тип <b>drive</b>, погодьтесь на автентифікацію через браузер (увійдіть у свій Google-акаунт).</li>
|
||
<li>Коли готово, відкрийте створений конфіг (шлях покаже сама команда, зазвичай <code>~/.config/rclone/rclone.conf</code>) і скопіюйте значення <code>client_id</code>, <code>client_secret</code>, <code>scope</code>, <code>token</code> у поля вище.</li>
|
||
<li>«Шлях до папки» — звичайна назва (не ID); можна лишити порожнім, і бекапи ляжуть у корінь «Мій диск» того акаунта, з яким ви увійшли.</li>
|
||
<li>Збережіть, натисніть «Перевірити підключення».</li>
|
||
</ol>
|
||
</details>
|
||
</div>
|
||
|
||
<div id="fields-s3" class="backup-remote-fields">
|
||
<label>Провайдер</label>
|
||
<select name="backup_s3_provider">
|
||
<option value="AWS" {{ 'selected' if remote_values.backup_s3_provider == 'AWS' }}>AWS S3</option>
|
||
<option value="Wasabi" {{ 'selected' if remote_values.backup_s3_provider == 'Wasabi' }}>Wasabi</option>
|
||
<option value="Minio" {{ 'selected' if remote_values.backup_s3_provider == 'Minio' }}>MinIO</option>
|
||
<option value="Other" {{ 'selected' if remote_values.backup_s3_provider == 'Other' }}>Інше (напр. Backblaze B2, DigitalOcean Spaces)</option>
|
||
</select>
|
||
|
||
<div class="row">
|
||
<div>
|
||
<label>Access Key ID</label>
|
||
<input type="text" name="backup_s3_access_key_id" value="{{ remote_values.backup_s3_access_key_id }}" autocomplete="off">
|
||
</div>
|
||
<div>
|
||
<label>Secret Access Key</label>
|
||
<input type="password" name="backup_s3_secret_access_key" value="{{ remote_values.backup_s3_secret_access_key }}" autocomplete="off">
|
||
</div>
|
||
</div>
|
||
|
||
<label>Endpoint <span class="muted">(порожньо для справжнього AWS S3)</span></label>
|
||
<input type="text" name="backup_s3_endpoint" value="{{ remote_values.backup_s3_endpoint }}" placeholder="напр. s3.eu-central-003.backblazeb2.com">
|
||
|
||
<div class="row">
|
||
<div>
|
||
<label>Регіон <span class="muted">(необов'язково)</span></label>
|
||
<input type="text" name="backup_s3_region" value="{{ remote_values.backup_s3_region }}">
|
||
</div>
|
||
<div>
|
||
<label>Бакет</label>
|
||
<input type="text" name="backup_s3_bucket" value="{{ remote_values.backup_s3_bucket }}">
|
||
</div>
|
||
</div>
|
||
|
||
<label>Тека всередині бакета <span class="muted">(необов'язково)</span></label>
|
||
<input type="text" name="backup_s3_prefix" value="{{ remote_values.backup_s3_prefix }}" placeholder="напр. backups">
|
||
|
||
<details style="margin-top:12px;">
|
||
<summary style="cursor:pointer;font-weight:700;font-size:13px;">Як налаштувати (крок за кроком)</summary>
|
||
<ol class="muted" style="font-size:12.5px;line-height:1.8;padding-left:20px;margin-top:10px;">
|
||
<li>Створіть бакет у своєму провайдері (AWS S3, Backblaze B2, Wasabi, DigitalOcean Spaces тощо).</li>
|
||
<li>Створіть ключ доступу (Access Key / Application Key) з правом запису в цей бакет.</li>
|
||
<li>Для AWS S3 залиште поле «Endpoint» порожнім. Для інших провайдерів — вкажіть їхній S3-endpoint (є в панелі провайдера; для Backblaze B2 і подібних оберіть провайдера «Інше»).</li>
|
||
<li>Заповніть Access Key ID, Secret Access Key, бакет — і збережіть.</li>
|
||
<li>Натисніть «Перевірити підключення», потім «Backup зараз».</li>
|
||
</ol>
|
||
</details>
|
||
</div>
|
||
|
||
<div id="fields-custom" class="backup-remote-fields">
|
||
<label>Тип rclone-backend'у</label>
|
||
<input type="text" name="backup_custom_type" value="{{ remote_values.backup_custom_type }}" placeholder="напр. sftp, b2, azureblob">
|
||
|
||
<label>Додаткові параметри <span class="muted">(по одному <code>ключ = значення</code> на рядок)</span></label>
|
||
<textarea name="backup_custom_config" rows="4" placeholder="account = ... key = ...">{{ remote_values.backup_custom_config }}</textarea>
|
||
|
||
<label>Шлях призначення</label>
|
||
<input type="text" name="backup_custom_path" value="{{ remote_values.backup_custom_path }}" placeholder="напр. mybucket/backups">
|
||
|
||
<details style="margin-top:12px;">
|
||
<summary style="cursor:pointer;font-weight:700;font-size:13px;">Як налаштувати (крок за кроком)</summary>
|
||
<p class="muted" style="font-size:12.5px;line-height:1.7;">
|
||
Для нетипових сховищ, не перелічених вище. Тип і назви параметрів беруться з
|
||
<a href="https://rclone.org/overview/" target="_blank">документації rclone для конкретного сховища</a>
|
||
(наприклад, для SFTP: тип <code>sftp</code>, параметри <code>host</code>/<code>user</code>/<code>pass</code>).
|
||
Кожен рядок у полі «Додаткові параметри» стає одним параметром конфігурації цього сховища.
|
||
</p>
|
||
</details>
|
||
</div>
|
||
|
||
<div style="margin-top:14px;display:flex;gap:10px;flex-wrap:wrap;">
|
||
<button class="btn" type="submit">Зберегти</button>
|
||
</div>
|
||
</form>
|
||
|
||
<div style="margin-top:14px;display:flex;gap:10px;flex-wrap:wrap;">
|
||
<form method="post" action="{{ url_for('admin.backups_remote_test') }}" style="display:inline;">
|
||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
||
<button class="btn secondary small" type="submit">🔌 Перевірити підключення</button>
|
||
</form>
|
||
<form method="post" action="{{ url_for('admin.backups_remote_backup_now') }}" style="display:inline;">
|
||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
||
<button class="btn secondary small" type="submit">☁️ Backup зараз</button>
|
||
</form>
|
||
</div>
|
||
</div>
|
||
|
||
<script>
|
||
function syncBackupRemoteFields() {
|
||
var type = document.getElementById('backup-remote-type').value;
|
||
document.querySelectorAll('.backup-remote-fields').forEach(function (el) {
|
||
el.style.display = (el.id === 'fields-' + type) ? 'block' : 'none';
|
||
});
|
||
}
|
||
syncBackupRemoteFields();
|
||
</script>
|
||
|
||
<dialog id="restore-dialog" style="border:1px solid var(--border);border-radius:var(--radius);background:var(--panel);color:var(--text);padding:0;max-width:420px;width:90%;">
|
||
<form method="post" id="restore-form" style="padding:20px;display:flex;flex-direction:column;gap:12px;">
|
||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
||
<h3 style="margin:0;color:var(--danger);">⚠️ Відновити базу даних</h3>
|
||
<p class="muted" style="margin:0;">
|
||
Файл: <b id="restore-filename"></b><br>
|
||
Поточний стан бази буде замінено вмістом цієї копії (перед цим автоматично створюється резервна копія поточного стану).
|
||
</p>
|
||
<div>
|
||
<label>Введіть слово <b>ВІДНОВИТИ</b> для підтвердження</label>
|
||
<input type="text" name="confirm" placeholder="ВІДНОВИТИ" autocomplete="off">
|
||
</div>
|
||
<div style="display:flex;gap:10px;justify-content:flex-end;">
|
||
<button type="button" class="btn secondary" onclick="document.getElementById('restore-dialog').close()">Скасувати</button>
|
||
<button class="btn danger" type="submit">Відновити базу даних</button>
|
||
</div>
|
||
</form>
|
||
</dialog>
|
||
|
||
<script>
|
||
function openRestoreDialog(filename) {
|
||
document.getElementById('restore-filename').textContent = filename;
|
||
document.getElementById('restore-form').action = '/admin/backups/restore/' + encodeURIComponent(filename);
|
||
document.getElementById('restore-dialog').showModal();
|
||
}
|
||
</script>
|
||
{% endblock %}
|