Backup & Recovery — резервне копіювання бази даних¶
Статус: 📋 архітектурне рішення фіксується, реалізація в беклозі.
Контекст: для single-user desktop-поставки через desktop-installer.md (SQLite у Docker-volume) і для production-поставки (PostgreSQL на Render/власному сервері) потрібне уніфіковане рішення «резервні копії як частина продукту» — не зовнішній скрипт, а керована функція DOP з UI в Admin Tools.
Зміст¶
- Цілі
- Два бекенди БД — два підходи
- UI — розділ «Backup & Recovery» в Admin Tools
- Призначення бекапу (куди зберігати)
- Розклад і ротація
- Відновлення (Recovery)
- Integrity check і self-healing
- Security / privacy
- Multi-tenant нюанси
- Backend-архітектура
- Посилання
- 🔮 Deferred / Ideas
Цілі¶
- Захист від технічних збоїв — пошкоджений SQLite WAL після power loss, збій диска, corrupted vhdx на WSL2.
- Захист від людської помилки — помилково видалений документ, неправильний
seed --reset, провалена міграція. - Disaster recovery — загублений ноутбук бухгалтера, крипто-локер на диску, злетіла ОС.
- Міграція між середовищами — експорт з demo (SQLite) → імпорт у production (PostgreSQL), міграція між хмарами.
- Audit / compliance — історична копія стану БД на кожен місяць / рік для бухгалтерських перевірок.
Не цілі (out of scope першої ітерації): - Continuous PITR (Point-in-Time Recovery) — для DOP цього рівня ретельності не потрібно, достатньо daily/hourly snapshots. - Replication / HA — окрема тема (production deployment), не фіча.
Два бекенди БД — два підходи¶
SQLite (demo / desktop / single-user)¶
Файл db.sqlite3 у volume dop-data. Основний механізм — SQLite online backup API:
- Безпечний під live-write (блокує WAL тільки на мілісекунди на чекпоінтах).
- Результат — валідна SQLite-база, яку можна відкрити будь-яким клієнтом.
- Швидкий (копіювання сторінок без re-encoding).
Альтернатива для експорту між БД: python manage.py dumpdata --natural-foreign --natural-primary -o dump.json — JSON fixture, повільніше, але незалежне від engine (підходить для міграції SQLite → PostgreSQL).
PostgreSQL (production / multi-user)¶
Стандартний pg_dump:
--format=custom— бінарний формат, підтримує parallel restore (pg_restore -j 4).- Flexible: можна бекапити окремі схеми, таблиці, без даних тощо.
pg_dumpallдодатково — для ролей і tablespaces.
Для великих БД: pg_basebackup + WAL archiving — але це вже рівень full PITR, у беклозі окремим пунктом.
Уніфікація¶
Django management command manage.py dop_backup — єдина точка входу, вибирає правильний механізм через connection.vendor:
if connection.vendor == "sqlite":
run_sqlite_online_backup(...)
elif connection.vendor == "postgresql":
run_pg_dump(...)
Результат в обох випадках — один файл (*.db або *.dump) + метадані (*.meta.json з версією схеми, часом, contest).
UI — розділ «Backup & Recovery» в Admin Tools¶
Нова секція у сторінці AdminToolsPage — вкладка «Резервне копіювання».
Екран 1 — налаштування¶
| Поле | Контрол | Опис |
|---|---|---|
| Auto backup | Switch | Увімкнено/вимкнено |
| Frequency | SegmentedControl | Hourly / Daily / Weekly / Custom cron |
| Time of day | TimeInput | Для Daily/Weekly |
| Retention — daily | NumberInput | Скільки щоденних тримати (default 7) |
| Retention — weekly | NumberInput | Скільки тижневих (default 4) |
| Retention — monthly | NumberInput | Скільки місячних (default 12) |
| Destination type | Select | Local folder / SMB share / S3 / OneDrive / Google Drive / Email |
| Destination path | TextInput / OAuth button | Параметри вибраного типу |
| Encryption | Switch + PasswordInput | Шифрувати AES-256 перед записом у destination |
| Test backup now | Button | Одноразовий запуск для валідації налаштувань |
Екран 2 — історія¶
Таблиця зі всіма виконаними бекапами:
| Колонка | Джерело |
|---|---|
| Дата/час | BackupRun.started_at |
| Тип | manual / scheduled |
| Статус | success / failed / in_progress |
| Розмір | BackupRun.size_bytes |
| Місце | destination_url (масковане) |
| Тривалість | finished_at - started_at |
| Дії | Restore / Download / Delete |
Екран 3 — відновлення (wizard)¶
- Вибрати backup з історії (або завантажити файл вручну).
- Показати warning: «Поточні дані будуть замінені на стан від [дата]. Буде створено safety-backup поточного стану».
- Підтвердити паролем admin-користувача.
- Виконати: safety-backup → stop tenants → restore → validate → restart.
- Логи кроків.
Призначення бекапу (куди зберігати)¶
1. Local folder (default для desktop)¶
- Windows:
%USERPROFILE%\Documents\DOP-Backups\ - Linux:
~/DOP-Backups/або/var/backups/dop/ - macOS:
~/Documents/DOP-Backups/
Нуль налаштувань, працює офлайн. Слабко проти disk failure — тому не можна бути єдиним.
2. SMB share (малий офіс)¶
\\fileserver\backups\dop\ — бухгалтер / IT-админ налаштовує раз, далі забуває. Credentials зберігаються в OS credential store (не в БД DOP).
3. S3 / S3-compatible (Backblaze B2, Wasabi, MinIO)¶
Найдешевший (~$6/TB/міс у B2) і найнадійніший варіант. Потребує: - Access key + secret (через Django settings / encrypted storage). - Bucket policy з versioning + lifecycle rules (автоматичне переміщення у Glacier через N днів). - Server-side encryption (SSE-S3 або SSE-KMS).
4. OneDrive / Google Drive / Dropbox (для бухгалтера-одинака)¶
Найпростіший варіант для desktop-поставки: просто поставити Destination = %USERPROFILE%\OneDrive\DOP-Backups\ — далі cloud-sync клієнт сам забере файли. Нуль credentials у DOP, нуль OAuth flow.
OAuth-інтеграція (пряма API) — опційно, коли треба щоб DOP писав напряму навіть якщо OneDrive client не встановлено.
5. Email (для параноїків / audit)¶
Відправка .dump.gz + .meta.json на заданий email. Обмеження розміру (Gmail 25 MB, SMTP різний) — розумно тільки для малих БД (SQLite <50 MB). Для великих — лінк на S3 + email з повідомленням.
Перевикористання: у DOP уже є EmailSmtpSettings у backend/shop/ для Store Manager. Той самий SMTP можна reuse для backup-email-sender.
Розклад і ротація¶
Scheduler¶
- На production (Linux/Docker): Celery Beat або systemd timer (вже є
redisуrender.yamlдля Channels — можна reuse як Celery broker). - На desktop (single-user): Django-Q2 або APScheduler в окремому
supervisord programусередині контейнера. Або Windows Task Scheduler, що викликаєdocker exec dop python manage.py dop_backup.
Вибір — після прототипу; обидва рішення рівноцінні.
Ротація (grandfather-father-son)¶
За замовчуванням:
| Рівень | Кількість | Утримання |
|---|---|---|
| Hourly | 24 | 1 день |
| Daily | 7 | 1 тиждень |
| Weekly | 4 | 1 місяць |
| Monthly | 12 | 1 рік |
| Yearly | ∞ | назавжди (опційно) |
Реалізується через BackupRun.retention_tier + management command manage.py dop_backup_prune, що видаляє зайві записи + файли у destination.
Відновлення (Recovery)¶
Варіанти тригера¶
- З UI (Admin Tools → Backup → History → Restore).
- З CLI для disaster recovery (
python manage.py dop_restore --file backup.dump --confirm). - Автоматично при detected corruption (див. Integrity check) —
launcher.exeпропонує відновити з найсвіжішого валідного бекапу.
Процедура (SQLite)¶
1. docker exec dop python manage.py dop_backup --reason=pre-restore-safety
2. docker exec dop supervisorctl stop daphne
3. docker exec dop sh -c "sqlite3 db.sqlite3 '.restore restore-source.db'"
або простіше: cp restore-source.db db.sqlite3 (бо Daphne зупинений)
4. docker exec dop python manage.py migrate --noinput # щоб schema відповідала коду
5. docker exec dop supervisorctl start daphne
Процедура (PostgreSQL)¶
1. python manage.py dop_backup --reason=pre-restore-safety
2. Зупинити Daphne/Celery
3. dropdb dop && createdb dop
4. pg_restore -d dop backup.dump
5. python manage.py migrate --noinput
6. Запустити Daphne
Cross-engine restore (SQLite → PostgreSQL)¶
dumpdataз source →loaddataв target (через природні ключі).- Повільно, можливі колізії з auto-generated PKs → документувати обмеження.
Integrity check і self-healing¶
Check¶
SQLite: sqlite3 db.sqlite3 "PRAGMA integrity_check; PRAGMA foreign_key_check;"
PostgreSQL: pg_amcheck --all --install-missing-extensions
Запускати:
- Перед кожним бекапом (не бекапимо пошкоджене).
- При старті launcher.exe (desktop) — якщо не ok, показуємо recovery wizard.
- За розкладом раз на тиждень (фоном, без downtime).
Self-healing flow (desktop)¶
launcher.exe start
├─ docker start dop
├─ wait for /api/health/
├─ POST /api/v1/admin/backup/integrity-check/
│ └─ if NOT ok → show modal:
│ "БД пошкоджена. Знайдено останній валідний бекап від [дата].
│ Відновити зараз? [Так] [Ні, спробую вручну]"
└─ отвід у браузер
Security / privacy¶
Encryption at rest¶
- AES-256-GCM перед записом у destination (особливо для S3 / cloud).
- Ключ — user password через PBKDF2 або окремий master key у OS keyring (Windows Credential Manager, macOS Keychain, libsecret).
- Ніколи не зберігати ключ у БД (inception problem — бекап тоді не відкриєш).
PII у бекапах¶
- Client, Person, CustomerProfile містять ПІБ, email, телефон, адресу — GDPR / український Закон про захист персональних даних.
- У налаштуваннях — опція «Anonymize on export» (для dev-копій): замінити персональні дані на фіктивні перед бекапом.
Audit trail¶
- Кожен
BackupRunіRestoreRun→ запис у core.AuditLog: who, when, reason, outcome. - Restore — завжди вимагає повторного вводу admin-паролю.
Multi-tenant нюанси¶
DOP — multi-tenant (core.Tenant, TenantAwareModel, TenantFilterMixin). Бекап може бути:
- Full DB — усі tenants разом (технічне копіювання файлу / схеми). Default для desktop.
- Per-tenant —
dumpdata+ фільтрtenant_id=Xчерез @alan2207/tenant-aware-dumpdata (кастомний management command). Для SaaS — відновлення одного клієнта без торкання інших.
Перший варіант — швидкий, другий — складніший (хто відновлюється, таблиці без tenant_id типу User вимагають окремої логіки). Починати з Full DB, per-tenant — окремим пунктом беклогу.
Backend-архітектура¶
Нові моделі (в core/ або окремому backup/ app)¶
class BackupDestination(TenantAwareModel):
name: str
kind: Literal["local", "smb", "s3", "onedrive", "gdrive", "email"]
config_json: dict # encrypted (per-destination credentials / URLs)
encryption_enabled: bool
encryption_key_ref: str # посилання на OS keyring entry
class BackupSchedule(TenantAwareModel):
destination: FK
frequency: Literal["hourly", "daily", "weekly", "cron"]
cron_expr: str | None
retention_hourly: int = 24
retention_daily: int = 7
retention_weekly: int = 4
retention_monthly: int = 12
enabled: bool
class BackupRun(TenantAwareModel):
destination: FK
schedule: FK | None # null для manual runs
kind: Literal["manual", "scheduled", "pre-restore-safety"]
started_at, finished_at: datetime
status: Literal["in_progress", "success", "failed"]
size_bytes: int
destination_url: str # masked для UI
checksum_sha256: str
db_vendor: str # "sqlite" | "postgresql"
schema_version: str # django-migrations hash
error_message: str | None
class RestoreRun(TenantAwareModel):
source_backup: FK(BackupRun) | None # null для uploaded file
uploaded_filename: str | None
safety_backup: FK(BackupRun) | None
started_at, finished_at: datetime
status: Literal["in_progress", "success", "failed"]
initiated_by: FK(User)
error_message: str | None
Нові management commands¶
dop_backup [--destination=NAME] [--reason=...]— виконати бекап.dop_backup_prune— застосувати retention (чистить BackupRun + файли).dop_backup_test [--destination=NAME]— перевірити, що credentials/шлях валідні.dop_restore [--file=PATH | --backup-run=ID] [--confirm]— відновити.dop_integrity_check— PRAGMA integrity_check / pg_amcheck.
API¶
GET /api/v1/admin/backup/destinations/
POST /api/v1/admin/backup/destinations/
GET /api/v1/admin/backup/schedules/
POST /api/v1/admin/backup/schedules/
GET /api/v1/admin/backup/runs/
POST /api/v1/admin/backup/runs/ # manual backup now
GET /api/v1/admin/backup/runs/:id/download/
POST /api/v1/admin/backup/runs/:id/restore/
POST /api/v1/admin/backup/integrity-check/
Посилання¶
- eswf/infrastructure/desktop-installer.md — для desktop-поставки; поле
Destination = %USERPROFILE%\DOP-Backups\ставиться за замовчуванням під час установки - eswf/infrastructure/docker.md — де фізично лежить SQLite у volume
- SQLite Online Backup API
- PostgreSQL
pg_dump - AdminToolsPage.tsx — місце інтеграції UI
🔮 Deferred / Ideas¶
Частина 1 — MVP для desktop (SQLite + local folder)¶
Мотивація: покрити критичний ризик «бухгалтер втратив БД після збою живлення» одразу при першій desktop-поставці.
Скоуп:
- Management command dop_backup для SQLite.
- UI в Admin Tools: Frequency/Time/Retention + Destination=local folder + Test/History/Restore.
- Self-healing при старті launcher.exe (integrity check + recovery modal).
- Default destination: %USERPROFILE%\Documents\DOP-Backups\.
Чому відкладено: ще немає жодної desktop-інсталяції; без реальних користувачів неможливо валідувати UX.
Trigger: перша desktop-інсталяція бухгалтеру (разом з desktop-installer.md MVP).
Частина 2 — PostgreSQL + cloud destinations (S3, OneDrive, GDrive)¶
Мотивація: для production-поставки на Render / власному сервері локальна тека безсенсу — треба хмара.
Скоуп:
- pg_dump варіант команди.
- S3 destination (через boto3).
- OneDrive / Google Drive через OAuth (або просто «папка синхронізована клієнтом»).
- Celery Beat scheduler.
Чому відкладено: поки production-деплоймент — тільки на Render з їхніми автоматичними Postgres-бекапами; DOP-level бекап не потрібен, поки немає self-hosted клієнтів.
Trigger: перший self-hosted production customer.
Частина 3 — Encryption + audit compliance¶
Мотивація: GDPR / комерційні клієнти вимагають encrypted backups і повний audit trail.
Скоуп:
- AES-256-GCM перед завантаженням у destination.
- Master key в OS keyring (через keyring Python lib).
- AuditLog records для всіх backup/restore events.
- Anonymize-on-export для PII.
Чому відкладено: без реальних клієнтів compliance вимоги невідомі.
Trigger: перший paid customer з compliance вимогами (GDPR, ISO 27001).
Частина 4 — Per-tenant backup/restore¶
Мотивація: у SaaS-моделі треба вміти відновити одного клієнта без впливу на решту.
Скоуп:
- Кастомний dumpdata фільтр по tenant_id.
- Restore-flow з merge (новий tenant_id → уникнення колізій).
- UI: вибір конкретного tenant при backup/restore.
Чому відкладено: DOP поки single-tenant на інсталяцію; multi-tenant SaaS — не пріоритет.
Trigger: multi-tenant SaaS deployment (коли 1 інсталяція обслуговує >1 клієнта).
Частина 5 — Point-in-Time Recovery (PITR)¶
Мотивація: можливість відкотитись на довільну секунду, не на найближчий щоденний snapshot.
Скоуп (PostgreSQL): WAL archiving + pg_basebackup + recovery.conf / recovery_target_time.
Скоуп (SQLite): WAL-mode + continuous .wal shipping (складніше — нативно не підтримується).
Чому відкладено: overkill для бухгалтерського ERP; hourly snapshots достатньо.
Trigger: клієнт з вимогою RPO < 1 година.
Частина 6 — Cross-engine migration tool¶
Мотивація: бухгалтер пробував DOP на SQLite-demo, хоче перенести дані у production PostgreSQL.
Скоуп: management command dop_migrate_engine --from=sqlite --to=postgres з merge PKs + FK-reconciliation.
Чому відкладено: поки немає конверсій demo → prod у реальному кейсі.
Trigger: перший клієнт робить апгрейд з demo на self-hosted production.