feat(api): ежедневный бэкап, копия в личку, восстановление через бота
backup.ts: db.backup() → gzip в backups/ рядом с базой, имя по времени Минска (Intl), 14 ежедневных и pre-restore на 30 дней; планировщик в 03:00 по Минску и при старте, если сегодня копии нет; файл админу, итог в «Систему». Восстановление: gunzip → integrity_check → версия схемы → пауза приёма (503) → pre-restore → атомарная подмена файла → миграции; при сбое возвращаем прежнюю базу. createApp и очередь берут базу через геттер, чтобы увидеть подменённую. Бот в личке: /backups с кнопками, /backup, присланный .sqlite.gz, подтверждение со списком пропадающих заявок, живёт 10 минут.
This commit is contained in:
+3
-2
@@ -8,7 +8,8 @@ import { notifyLater, type Notifier } from './notifier.js';
|
||||
import { clientIp, LOOPBACK, type RateLimiter } from './rateLimit.js';
|
||||
|
||||
export type AppDeps = {
|
||||
db: Db;
|
||||
/** Геттер, а не объект: восстановление из бэкапа подменяет базу на лету. */
|
||||
getDb: () => Db;
|
||||
now: () => Date;
|
||||
rateLimiter: RateLimiter;
|
||||
notifier: Notifier;
|
||||
@@ -79,7 +80,7 @@ export function createApp(deps: AppDeps) {
|
||||
if (result.data.honeypot) return reply({ ok: true, duplicate: false }, 201);
|
||||
|
||||
const userAgent = c.req.header('user-agent')?.slice(0, USER_AGENT_MAX) ?? null;
|
||||
const saved = saveLead(deps.db, result.data, { now: deps.now(), userAgent });
|
||||
const saved = saveLead(deps.getDb(), result.data, { now: deps.now(), userAgent });
|
||||
if (saved.kind === 'unchanged') return reply({ ok: true, duplicate: true, clarified: false }, 200);
|
||||
notifyLater(deps.notifier, saved.event);
|
||||
if (saved.kind === 'clarified') return reply({ ok: true, duplicate: true, clarified: true }, 200);
|
||||
|
||||
Reference in New Issue
Block a user