From cf6056cede0b890485f6ed2a9ed2a004e9300851 Mon Sep 17 00:00:00 2001 From: Randi <55005611+rdself@users.noreply.github.com> Date: Mon, 30 Mar 2026 23:12:23 -0400 Subject: [PATCH] Add env flag to disable automatic SQLite backups (#846) * feat(db): allow disabling sqlite auto backups * chore(db): rename sqlite auto backup env flag --- .env.example | 1 + README.md | 34 +++++++++++++++--------------- docs/USER_GUIDE.md | 49 ++++++++++++++++++++++---------------------- src/lib/db/backup.ts | 36 ++++++++++++++++++++------------ 4 files changed, 66 insertions(+), 54 deletions(-) diff --git a/.env.example b/.env.example index 989b1c17d4..00d3853846 100644 --- a/.env.example +++ b/.env.example @@ -21,6 +21,7 @@ STORAGE_ENCRYPTION_KEY_VERSION=v1 LOG_RETENTION_DAYS=90 SQLITE_MAX_SIZE_MB=2048 SQLITE_CLEAN_LEGACY_FILES=true +DISABLE_SQLITE_AUTO_BACKUP=false # Recommended runtime variables # Canonical/base port (keeps backward compatibility) diff --git a/README.md b/README.md index d950f3866f..1a5d4b79de 100644 --- a/README.md +++ b/README.md @@ -409,7 +409,7 @@ Installing, configuring, and maintaining an AI proxy across different environmen - **Electron Desktop App** — Native app for Windows/macOS/Linux with system tray, auto-start, offline mode - **Split-Port Mode** — API and Dashboard on separate ports for advanced scenarios (reverse proxy, container networking) - **Cloud Sync** — Config synchronization across devices via Cloudflare Workers -- **DB Backups** — Automatic backup, restore, export and import of all settings +- **DB Backups** — Automatic backup, restore, export and import of all settings, with `DISABLE_SQLITE_AUTO_BACKUP` for externally managed backups @@ -1276,22 +1276,22 @@ OmniRoute v2.0 is built as an operational platform, not just a relay proxy. ### ☁️ Deployment & Platform -| Feature | What It Does | -| ----------------------------- | --------------------------------------------------------- | -| 🌐 **Deploy Anywhere** | Localhost, VPS, Docker, Cloud environments | -| 🚇 **Cloudflare Tunnel** 🆕 | One-click Quick Tunnel integration from the dashboard | -| 🔑 **API Key Model Filtering** | Native /v1/models response filtered via assigned Bearer context roles | -| ⚡ **Smart Cache Bypass** | Configurable TTL heuristics and forced refetch controls | -| 🔄 **Backup/Restore** | Export/import and disaster recovery flows | -| 🧙 **Onboarding Wizard** | First-run guided setup | -| 🔧 **CLI Tools Dashboard** | One-click setup for popular coding tools | -| 🎮 **Model Playground** | Test any provider/model/endpoint from the dashboard | -| 🔏 **CLI Fingerprint Toggle** | Per-provider fingerprint matching in Settings > Security | -| 🌐 **i18n (30 languages)** | Full dashboard + docs language support with RTL coverage | -| 🧹 **Clear All Models** | One-click model list clearing in provider details | -| 👁️ **Sidebar Controls** 🆕 | Hide components and integrations from Appearance Settings | -| 📋 **Issue Templates** | Standardized GitHub templates for bugs and features | -| 📂 **Custom Data Directory** | `DATA_DIR` override for storage location | +| Feature | What It Does | +| ------------------------------ | --------------------------------------------------------------------- | +| 🌐 **Deploy Anywhere** | Localhost, VPS, Docker, Cloud environments | +| 🚇 **Cloudflare Tunnel** 🆕 | One-click Quick Tunnel integration from the dashboard | +| 🔑 **API Key Model Filtering** | Native /v1/models response filtered via assigned Bearer context roles | +| ⚡ **Smart Cache Bypass** | Configurable TTL heuristics and forced refetch controls | +| 🔄 **Backup/Restore** | Export/import and disaster recovery flows | +| 🧙 **Onboarding Wizard** | First-run guided setup | +| 🔧 **CLI Tools Dashboard** | One-click setup for popular coding tools | +| 🎮 **Model Playground** | Test any provider/model/endpoint from the dashboard | +| 🔏 **CLI Fingerprint Toggle** | Per-provider fingerprint matching in Settings > Security | +| 🌐 **i18n (30 languages)** | Full dashboard + docs language support with RTL coverage | +| 🧹 **Clear All Models** | One-click model list clearing in provider details | +| 👁️ **Sidebar Controls** 🆕 | Hide components and integrations from Appearance Settings | +| 📋 **Issue Templates** | Standardized GitHub templates for bugs and features | +| 📂 **Custom Data Directory** | `DATA_DIR` override for storage location | ### Feature Deep Dive diff --git a/docs/USER_GUIDE.md b/docs/USER_GUIDE.md index 44d4855453..e149dbac77 100644 --- a/docs/USER_GUIDE.md +++ b/docs/USER_GUIDE.md @@ -507,25 +507,26 @@ post_install() { ### Environment Variables -| Variable | Default | Description | -| ------------------------- | ------------------------------------ | ---------------------------------------------------------------- | -| `JWT_SECRET` | `omniroute-default-secret-change-me` | JWT signing secret (**change in production**) | -| `INITIAL_PASSWORD` | `123456` | First login password | -| `DATA_DIR` | `~/.omniroute` | Data directory (db, usage, logs) | -| `PORT` | framework default | Service port (`20128` in examples) | -| `HOSTNAME` | framework default | Bind host (Docker defaults to `0.0.0.0`) | -| `NODE_ENV` | runtime default | Set `production` for deploy | -| `BASE_URL` | `http://localhost:20128` | Server-side internal base URL | -| `CLOUD_URL` | `https://omniroute.dev` | Cloud sync endpoint base URL | -| `API_KEY_SECRET` | `endpoint-proxy-api-key-secret` | HMAC secret for generated API keys | -| `REQUIRE_API_KEY` | `false` | Enforce Bearer API key on `/v1/*` | -| `ALLOW_API_KEY_REVEAL` | `false` | Allow Api Manager to copy full API keys on demand | -| `ENABLE_REQUEST_LOGS` | `false` | Enables request/response logs | -| `AUTH_COOKIE_SECURE` | `false` | Force `Secure` auth cookie (behind HTTPS reverse proxy) | -| `CLOUDFLARED_BIN` | unset | Use an existing `cloudflared` binary instead of managed download | -| `OMNIROUTE_MEMORY_MB` | `512` | Node.js heap limit in MB | -| `PROMPT_CACHE_MAX_SIZE` | `50` | Max prompt cache entries | -| `SEMANTIC_CACHE_MAX_SIZE` | `100` | Max semantic cache entries | +| Variable | Default | Description | +| -------------------------------------- | ------------------------------------ | ------------------------------------------------------------------------------------------ | +| `JWT_SECRET` | `omniroute-default-secret-change-me` | JWT signing secret (**change in production**) | +| `INITIAL_PASSWORD` | `123456` | First login password | +| `DATA_DIR` | `~/.omniroute` | Data directory (db, usage, logs) | +| `PORT` | framework default | Service port (`20128` in examples) | +| `HOSTNAME` | framework default | Bind host (Docker defaults to `0.0.0.0`) | +| `NODE_ENV` | runtime default | Set `production` for deploy | +| `BASE_URL` | `http://localhost:20128` | Server-side internal base URL | +| `CLOUD_URL` | `https://omniroute.dev` | Cloud sync endpoint base URL | +| `API_KEY_SECRET` | `endpoint-proxy-api-key-secret` | HMAC secret for generated API keys | +| `REQUIRE_API_KEY` | `false` | Enforce Bearer API key on `/v1/*` | +| `ALLOW_API_KEY_REVEAL` | `false` | Allow Api Manager to copy full API keys on demand | +| `DISABLE_SQLITE_AUTO_BACKUP` | `false` | Disable automatic SQLite snapshots before writes/import/restore; manual backups still work | +| `ENABLE_REQUEST_LOGS` | `false` | Enables request/response logs | +| `AUTH_COOKIE_SECURE` | `false` | Force `Secure` auth cookie (behind HTTPS reverse proxy) | +| `CLOUDFLARED_BIN` | unset | Use an existing `cloudflared` binary instead of managed download | +| `OMNIROUTE_MEMORY_MB` | `512` | Node.js heap limit in MB | +| `PROMPT_CACHE_MAX_SIZE` | `50` | Max prompt cache entries | +| `SEMANTIC_CACHE_MAX_SIZE` | `100` | Max semantic cache entries | For the full environment variable reference, see the [README](../README.md). @@ -768,11 +769,11 @@ OmniRoute implements provider-level resilience with four components: Manage database backups in **Dashboard → Settings → System & Storage**. -| Action | Description | -| ------------------------ | ------------------------------------------------------------------------------------------------------------------------------ | -| **Export Database** | Downloads the current SQLite database as a `.sqlite` file | -| **Export All (.tar.gz)** | Downloads a full backup archive including: database, settings, combos, provider connections (no credentials), API key metadata | -| **Import Database** | Upload a `.sqlite` file to replace the current database. A pre-import backup is automatically created | +| Action | Description | +| ------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------- | +| **Export Database** | Downloads the current SQLite database as a `.sqlite` file | +| **Export All (.tar.gz)** | Downloads a full backup archive including: database, settings, combos, provider connections (no credentials), API key metadata | +| **Import Database** | Upload a `.sqlite` file to replace the current database. A pre-import backup is automatically created unless `DISABLE_SQLITE_AUTO_BACKUP=true` | ```bash # API: Export database diff --git a/src/lib/db/backup.ts b/src/lib/db/backup.ts index 4526b4a40e..8e09346b3b 100644 --- a/src/lib/db/backup.ts +++ b/src/lib/db/backup.ts @@ -23,6 +23,13 @@ type CountRow = { cnt?: number }; let _lastBackupAt = 0; const BACKUP_THROTTLE_MS = 60 * 60 * 1000; // 60 minutes const MAX_DB_BACKUPS = 20; +const TRUE_ENV_VALUES = new Set(["1", "true", "yes", "on"]); + +function isSqliteAutoBackupDisabled() { + const value = process.env.DISABLE_SQLITE_AUTO_BACKUP; + if (!value) return false; + return TRUE_ENV_VALUES.has(value.trim().toLowerCase()); +} function sleep(ms: number) { return new Promise((resolve) => setTimeout(resolve, ms)); @@ -59,6 +66,7 @@ export function backupDbFile(reason = "auto") { try { if (isBuildPhase || isCloud) return null; if (!SQLITE_FILE || !fs.existsSync(SQLITE_FILE)) return null; + if (reason !== "manual" && isSqliteAutoBackupDisabled()) return null; const stat = fs.statSync(SQLITE_FILE); if (stat.size < 4096) { @@ -227,19 +235,21 @@ export async function restoreDbBackup(backupId: string) { } // Force pre-restore backup (bypass throttle) and await so the DB is not closed while backup runs - _lastBackupAt = 0; - const backupDirForPre = DB_BACKUPS_DIR || path.join(DATA_DIR, "db_backups"); - if (SQLITE_FILE && fs.existsSync(SQLITE_FILE)) { - const stat = fs.statSync(SQLITE_FILE); - if (stat.size >= 4096) { - if (!fs.existsSync(backupDirForPre)) fs.mkdirSync(backupDirForPre, { recursive: true }); - const preBackupPath = path.join( - backupDirForPre, - `db_${new Date().toISOString().replace(/[:.]/g, "-")}_pre-restore.sqlite` - ); - const dbForBackup = getDbInstance(); - await dbForBackup.backup(preBackupPath); - _lastBackupAt = Date.now(); + if (!isSqliteAutoBackupDisabled()) { + _lastBackupAt = 0; + const backupDirForPre = DB_BACKUPS_DIR || path.join(DATA_DIR, "db_backups"); + if (SQLITE_FILE && fs.existsSync(SQLITE_FILE)) { + const stat = fs.statSync(SQLITE_FILE); + if (stat.size >= 4096) { + if (!fs.existsSync(backupDirForPre)) fs.mkdirSync(backupDirForPre, { recursive: true }); + const preBackupPath = path.join( + backupDirForPre, + `db_${new Date().toISOString().replace(/[:.]/g, "-")}_pre-restore.sqlite` + ); + const dbForBackup = getDbInstance(); + await dbForBackup.backup(preBackupPath); + _lastBackupAt = Date.now(); + } } }