Перейти до змісту

Backup & Recovery — резервне копіювання бази даних

Статус: 📋 архітектурне рішення фіксується, реалізація в беклозі.

Контекст: для single-user desktop-поставки через desktop-installer.md (SQLite у Docker-volume) і для production-поставки (PostgreSQL на Render/власному сервері) потрібне уніфіковане рішення «резервні копії як частина продукту» — не зовнішній скрипт, а керована функція DOP з UI в Admin Tools.


Зміст


Цілі

  1. Захист від технічних збоїв — пошкоджений SQLite WAL після power loss, збій диска, corrupted vhdx на WSL2.
  2. Захист від людської помилки — помилково видалений документ, неправильний seed --reset, провалена міграція.
  3. Disaster recovery — загублений ноутбук бухгалтера, крипто-локер на диску, злетіла ОС.
  4. Міграція між середовищами — експорт з demo (SQLite) → імпорт у production (PostgreSQL), міграція між хмарами.
  5. 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:

sqlite3 db.sqlite3 ".backup output.db"
  • Безпечний під 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:

pg_dump --format=custom --compress=9 --file=backup.dump $DATABASE_URL
  • --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)

Варіанти тригера

  1. З UI (Admin Tools → Backup → History → Restore).
  2. З CLI для disaster recovery (python manage.py dop_restore --file backup.dump --confirm).
  3. Автоматично при 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). Бекап може бути:

  1. Full DB — усі tenants разом (технічне копіювання файлу / схеми). Default для desktop.
  2. Per-tenantdumpdata + фільтр 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/

Посилання


🔮 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.