diff --git a/README.md b/README.md
index 559ca0a..23e424a 100644
--- a/README.md
+++ b/README.md
@@ -102,9 +102,10 @@
градієнтів) підбирається автоматично.
- **Розсилка** повідомлення всім клієнтам бота.
- **Резервні копії** БД: створення, відновлення, видалення з адмінки,
- плюс опційні **автоматичні бекапи в Google Drive** через сервісний
- акаунт (без інтерактивного логіну) з ротацією — старі копії
- видаляються, коли їх стає більше за налаштовану кількість.
+ плюс опційні **автоматичні бекапи через rclone** (Google Drive, S3-сумісне
+ сховище або будь-який інший rclone-backend, налаштовується прямо у формі)
+ з ротацією — старі копії видаляються, коли їх стає більше за налаштовану
+ кількість.
- **Адміністратори**: ролі admin/manager (RBAC) — менеджер не бачить
Налаштування, Адміністраторів, Резервні копії, Статус системи й
Журнал дій.
@@ -133,7 +134,7 @@ bober-bbq-bot/
│ │ ├── inventory.py # склад: інгредієнти, рецептура, заявки на поповнення
│ │ ├── customers.py # клієнти й статистика по них
│ │ ├── statistics.py # звіти за період + графік виручки
-│ │ ├── system.py # статус системи, бекапи (локальні + Google Drive), очищення БД
+│ │ ├── system.py # статус системи, бекапи (локальні + rclone), очищення БД
│ │ ├── telegram_chats.py # перевірка підключення чатів доставки/самовивозу
│ │ ├── broadcast.py # розсилка повідомлення всім клієнтам
│ │ ├── admins.py # керування адміністраторами/менеджерами
@@ -142,7 +143,7 @@ bober-bbq-bot/
│ ├── utils/
│ │ ├── reconciliation.py # фонові job'и: страховка оплати + health-check токена
│ │ ├── inventory.py # автосписання/повернення складу, автозаявки на поповнення
-│ │ ├── gdrive_backup.py # автобекапи БД у Google Drive з ротацією
+│ │ ├── backup_remote.py # автобекапи БД через rclone (Google Drive/S3/інше) з ротацією
│ │ ├── notify_customer.py # авто-сповіщення клієнта про статус + запит оцінки
│ │ ├── notify_manager.py # сповіщення менеджера (нове/скасоване замовлення), staff_chat_id() по типу замовлення
│ │ ├── telegram_notify.py # низькорівневий sendMessage/sendDocument (HTTP, поза aiogram)
@@ -380,7 +381,7 @@ Better Uptime тощо), щоб дізнатись про падіння сай
|---|---|---|
| `reconcile_payments` | кожні 10 хв | звіряє неоплачені card-замовлення за останні 6 год напряму з monobank — страховка на випадок пропущеного вебхука |
| `monobank_token_health` | щогодини | пінгує токен monobank; якщо робочий токен раптом перестав діяти — пише власнику (`OWNER_IDS`) в Telegram |
-| `gdrive_backup` | щогодини (перевіряє) | якщо увімкнено й минув налаштований інтервал — вивантажує свіжий бекап у Google Drive і чистить застарілі копії; при збої пише власнику в Telegram |
+| `backup_remote` | щогодини (перевіряє) | якщо увімкнено й минув налаштований інтервал — вивантажує свіжий бекап через rclone (Google Drive/S3/інше) і чистить застарілі копії; при збої пише власнику в Telegram |
## Налаштування в адмінці (без доступу до сервера)
@@ -411,9 +412,9 @@ Better Uptime тощо), щоб дізнатись про падіння сай
розподіл по зірках, фільтр і пагінація.
- Розсилка повідомлення всім клієнтам бота.
- Резервні копії БД: створення, відновлення, видалення — прямо з адмінки,
- плюс автоматичні бекапи в Google Drive з ротацією (Адмінка →
- Резервні копії → секція «Google Drive», з покроковою інструкцією
- налаштування сервісного акаунта прямо на сторінці).
+ плюс автоматичні бекапи через rclone з ротацією (Адмінка →
+ Резервні копії → тип сховища: Google Drive / S3-сумісне / інше,
+ з покроковою інструкцією налаштування прямо на сторінці для кожного).
- Облік сировини: перемикач «Увімкнути облік сировини», нижче в тому
ж розділі — при увімкненні в меню з'являється «Склад».
diff --git a/bober_bbq/admin/system.py b/bober_bbq/admin/system.py
index d58e134..8963aff 100644
--- a/bober_bbq/admin/system.py
+++ b/bober_bbq/admin/system.py
@@ -13,7 +13,7 @@ from bober_bbq.config import BASE_DIR, config
from bober_bbq.extensions import db
from bober_bbq.models import AuditLog, CartItem, Order, OrderItem, Product, Setting, TelegramUser
from bober_bbq.utils.audit import log_action
-from bober_bbq.utils.gdrive_backup import backup_now, gdrive_enabled, test_connection
+from bober_bbq.utils.backup_remote import backup_now, backup_remote_enabled, test_connection
APP_VERSION = "1.0.0"
BACKUP_DIR = BASE_DIR / "backups"
@@ -93,61 +93,84 @@ def backups_list():
]
sqlite_path = _sqlite_path()
- gdrive_last_at_raw = Setting.get("gdrive_last_backup_at", "")
- gdrive_last_at = None
- if gdrive_last_at_raw:
+ remote_last_at_raw = Setting.get("backup_remote_last_backup_at", "")
+ remote_last_at = None
+ if remote_last_at_raw:
try:
- gdrive_last_at = datetime.fromisoformat(gdrive_last_at_raw).replace(tzinfo=timezone.utc).astimezone(_tz)
+ remote_last_at = datetime.fromisoformat(remote_last_at_raw).replace(tzinfo=timezone.utc).astimezone(_tz)
except ValueError:
- gdrive_last_at = None
- gdrive_last_status = Setting.get("gdrive_last_backup_status", "")
+ remote_last_at = None
+ remote_last_status = Setting.get("backup_remote_last_backup_status", "")
+ remote_keys = [
+ "backup_remote_enabled",
+ "backup_remote_type",
+ "backup_remote_interval_hours",
+ "backup_remote_retention_count",
+ "backup_gdrive_service_account_json",
+ "backup_gdrive_folder_id",
+ "backup_s3_provider",
+ "backup_s3_access_key_id",
+ "backup_s3_secret_access_key",
+ "backup_s3_endpoint",
+ "backup_s3_region",
+ "backup_s3_bucket",
+ "backup_s3_prefix",
+ "backup_custom_type",
+ "backup_custom_config",
+ "backup_custom_path",
+ ]
return render_template(
"admin/backups.html",
backups=backups,
is_sqlite=bool(sqlite_path),
- gdrive_values={
- "gdrive_backup_enabled": Setting.get("gdrive_backup_enabled", "0"),
- "gdrive_service_account_json": Setting.get("gdrive_service_account_json", ""),
- "gdrive_folder_id": Setting.get("gdrive_folder_id", ""),
- "gdrive_backup_interval_hours": Setting.get("gdrive_backup_interval_hours", "24"),
- "gdrive_retention_count": Setting.get("gdrive_retention_count", "14"),
- },
- gdrive_last_at=gdrive_last_at,
- gdrive_last_status=gdrive_last_status,
+ remote_values={key: Setting.get(key, "") for key in remote_keys},
+ remote_last_at=remote_last_at,
+ remote_last_status=remote_last_status,
)
-@admin_bp.route("/backups/gdrive-settings", methods=["POST"])
+@admin_bp.route("/backups/remote-settings", methods=["POST"])
@admin_required
-def backups_gdrive_settings():
- Setting.set("gdrive_backup_enabled", "1" if request.form.get("gdrive_backup_enabled") else "0")
- Setting.set("gdrive_service_account_json", request.form.get("gdrive_service_account_json", "").strip())
- Setting.set("gdrive_folder_id", request.form.get("gdrive_folder_id", "").strip())
- Setting.set("gdrive_backup_interval_hours", request.form.get("gdrive_backup_interval_hours", "24") or "24")
- Setting.set("gdrive_retention_count", request.form.get("gdrive_retention_count", "14") or "14")
+def backups_remote_settings():
+ Setting.set("backup_remote_enabled", "1" if request.form.get("backup_remote_enabled") else "0")
+ Setting.set("backup_remote_type", request.form.get("backup_remote_type", "").strip())
+ Setting.set("backup_remote_interval_hours", request.form.get("backup_remote_interval_hours", "24") or "24")
+ Setting.set("backup_remote_retention_count", request.form.get("backup_remote_retention_count", "14") or "14")
+ Setting.set("backup_gdrive_service_account_json", request.form.get("backup_gdrive_service_account_json", "").strip())
+ Setting.set("backup_gdrive_folder_id", request.form.get("backup_gdrive_folder_id", "").strip())
+ Setting.set("backup_s3_provider", request.form.get("backup_s3_provider", "AWS").strip() or "AWS")
+ Setting.set("backup_s3_access_key_id", request.form.get("backup_s3_access_key_id", "").strip())
+ Setting.set("backup_s3_secret_access_key", request.form.get("backup_s3_secret_access_key", "").strip())
+ Setting.set("backup_s3_endpoint", request.form.get("backup_s3_endpoint", "").strip())
+ Setting.set("backup_s3_region", request.form.get("backup_s3_region", "").strip())
+ Setting.set("backup_s3_bucket", request.form.get("backup_s3_bucket", "").strip())
+ Setting.set("backup_s3_prefix", request.form.get("backup_s3_prefix", "").strip())
+ Setting.set("backup_custom_type", request.form.get("backup_custom_type", "").strip())
+ Setting.set("backup_custom_config", request.form.get("backup_custom_config", "").strip())
+ Setting.set("backup_custom_path", request.form.get("backup_custom_path", "").strip())
db.session.commit()
- log_action("gdrive_settings_update", "Оновлено налаштування резервного копіювання в Google Drive")
- flash("Налаштування Google Drive збережено", "success")
+ log_action("backup_remote_settings_update", "Оновлено налаштування резервного копіювання")
+ flash("Налаштування резервного копіювання збережено", "success")
return redirect(url_for("admin.backups_list"))
-@admin_bp.route("/backups/gdrive-test", methods=["POST"])
+@admin_bp.route("/backups/remote-test", methods=["POST"])
@admin_required
-def backups_gdrive_test():
+def backups_remote_test():
ok, message = test_connection()
flash(message, "success" if ok else "error")
return redirect(url_for("admin.backups_list"))
-@admin_bp.route("/backups/gdrive-backup-now", methods=["POST"])
+@admin_bp.route("/backups/remote-backup-now", methods=["POST"])
@admin_required
-def backups_gdrive_backup_now():
- if not gdrive_enabled():
- flash("Спершу увімкніть і збережіть налаштування Google Drive.", "error")
+def backups_remote_backup_now():
+ if not backup_remote_enabled():
+ flash("Спершу увімкніть і збережіть налаштування резервного копіювання.", "error")
return redirect(url_for("admin.backups_list"))
ok, message = backup_now()
if ok:
- log_action("gdrive_backup_manual", "Створено позачергову резервну копію в Google Drive")
+ log_action("backup_remote_manual", "Створено позачергову резервну копію")
flash(message, "success" if ok else "error")
return redirect(url_for("admin.backups_list"))
diff --git a/bober_bbq/models.py b/bober_bbq/models.py
index 95b9c56..032680f 100644
--- a/bober_bbq/models.py
+++ b/bober_bbq/models.py
@@ -626,11 +626,25 @@ DEFAULT_SETTINGS = {
"liqpay_private_key": "",
"liqpay_sandbox_mode": "0",
"payment_destination_template": "Оплата замовлення {number}",
- "gdrive_backup_enabled": "0",
- "gdrive_service_account_json": "",
- "gdrive_folder_id": "",
- "gdrive_backup_interval_hours": "24",
- "gdrive_retention_count": "14",
+ # Off-site backups via rclone — see bober_bbq/utils/backup_remote.py.
+ # backup_remote_type selects which of the field groups below is
+ # actually used: "" (not configured) | "gdrive" | "s3" | "custom".
+ "backup_remote_enabled": "0",
+ "backup_remote_type": "",
+ "backup_remote_interval_hours": "24",
+ "backup_remote_retention_count": "14",
+ "backup_gdrive_service_account_json": "",
+ "backup_gdrive_folder_id": "",
+ "backup_s3_provider": "AWS",
+ "backup_s3_access_key_id": "",
+ "backup_s3_secret_access_key": "",
+ "backup_s3_endpoint": "",
+ "backup_s3_region": "",
+ "backup_s3_bucket": "",
+ "backup_s3_prefix": "",
+ "backup_custom_type": "",
+ "backup_custom_config": "",
+ "backup_custom_path": "",
"admin_theme_accent": "#ff7a3d",
"webapp_theme_accent": "#ff7a3d",
"sidebar_collapsible_menu": "1",
diff --git a/bober_bbq/static/admin/css/admin.css b/bober_bbq/static/admin/css/admin.css
index d1108ca..53cff9d 100644
--- a/bober_bbq/static/admin/css/admin.css
+++ b/bober_bbq/static/admin/css/admin.css
@@ -617,6 +617,7 @@ td { vertical-align: middle; }
.recipe-row input { width: 130px; flex-shrink: 0; margin: 0; }
.order-type-fields { display: none; }
+.backup-remote-fields { display: none; }
/* ---------- responsive / mobile ---------- */
diff --git a/bober_bbq/templates/admin/backups.html b/bober_bbq/templates/admin/backups.html
index ea6650f..2a456fe 100644
--- a/bober_bbq/templates/admin/backups.html
+++ b/bober_bbq/templates/admin/backups.html
@@ -42,70 +42,160 @@
-
☁️ Автоматичні резервні копії в Google Drive
+☁️ Автоматичні резервні копії (rclone)
- {% if gdrive_last_at %}
+ {% if remote_last_at %}
- Останній бекап: {{ gdrive_last_at.strftime('%d.%m.%Y %H:%M') }} —
- {% if gdrive_last_status == 'ok' %}успішно ✅{% else %}помилка: {{ gdrive_last_status }}{% endif %}
+ Останній бекап: {{ remote_last_at.strftime('%d.%m.%Y %H:%M') }} —
+ {% if remote_last_status == 'ok' %}успішно ✅{% else %}помилка: {{ remote_last_status }}{% endif %}
{% endif %}
-
-
-
-
-
- Як налаштувати (крок за кроком)
-
- - Відкрийте Google Cloud Console, створіть проєкт (або оберіть існуючий).
- - Увімкніть Google Drive API для цього проєкту (APIs & Services → Library).
- - Створіть сервісний акаунт: IAM & Admin → Service Accounts → Create Service Account.
- - Відкрийте створений акаунт → Keys → Add Key → Create new key → JSON. Завантажиться файл.
- - Відкрийте цей файл будь-яким текстовим редактором і вставте весь вміст у поле «Ключ сервісного акаунта» вище.
- - У Google Drive створіть папку для бекапів, поділіться нею (Share) з email сервісного акаунта (поле
client_email у JSON-файлі), давши права «Редактор».
- - Скопіюйте ID папки з адресного рядка (частина після
/folders/) у поле «ID папки» вище.
- - Увімкніть перемикач, збережіть, натисніть «Перевірити підключення» — якщо все гаразд, спробуйте «Backup зараз».
-
-
+
+