bober-bbq-bot/bober_bbq/templates/admin/backups.html
byrsapty e773b19508 OAuth Google Drive: use a plain folder path instead of a folder ID
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>
2026-09-05 23:26:38 +03:00

257 lines
17 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

{% 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 &amp; Services → Library).</li>
<li>Створіть сервісний акаунт: IAM &amp; 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 = ...&#10;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 %}