Source module: clarity:backend/src-tauri/src/backup/ (6 files)
The backup system protects two independent data stores: the SQLite application database and the binary time-series data directory. Each type has its own scheduler interval, archiving logic, and restore procedure.
| File | Role |
|---|---|
backup/mod.rs |
Module entry: global statics (BACKUP_STATE, BACKUP_CONFIG), init_backup_module, get_backup_routes |
backup/state.rs |
BackupConfig, BackupState, BackupResult, RestoreResult, BackupFileInfo types; load/save helpers |
backup/scheduler.rs |
run_scheduler — async loop that fires SQLite and timeseries backups on their respective intervals |
backup/sqlite_backup.rs |
run_sqlite_backup, run_rolling_sqlite_backup, cleanup_old_backups |
backup/timeseries_backup.rs |
run_timeseries_backup (full), run_incremental_timeseries_backup |
backup/api.rs |
Six warp route handlers under /api/admin/backup/ |
mod.rs exports two once_cell::sync::Lazy<Arc<Mutex<…>>> globals — BACKUP_STATE and BACKUP_CONFIG — used to share state between the scheduler thread and the HTTP handlers.
clarity:backend/src-tauri/src/backup/mod.rs:16-22
init_backup_module(storage) is called from main.rs during server startup.
clarity:backend/src-tauri/src/main.rs:2295-2303
Steps performed (clarity:backend/src-tauri/src/backup/mod.rs:24-63):
app_data_dir from storage.get_base_path().BackupConfig from <app_data_dir>/backup_config.json (falls back to defaults if absent).backup_path from config.<backup_path>/sqlite/ and <backup_path>/timeseries/ directories.BackupState from <backup_path>/backup_state.json.std::thread::spawn; that thread creates its own tokio::runtime::Runtime and runs scheduler::run_scheduler on it indefinitely.Loaded from <app_data_dir>/backup_config.json. When the file is absent the struct defaults are used.
clarity:backend/src-tauri/src/backup/state.rs:7-87
{
"backup_path": null,
"scheduler": {
"check_interval_seconds": 3600,
"timeseries_backup_interval_hours": 24
},
"sqlite_backup": {
"enabled": true,
"interval_minutes": 180,
"num_backups_to_keep": 6
},
"timeseries_backup": {
"enabled": true,
"incremental_only": true
}
}
| Field | Type | Default | Notes |
|---|---|---|---|
backup_path |
Option<String> |
null |
When null, resolves to <app_data_dir>/backups |
scheduler.check_interval_seconds |
u64 |
3600 |
Sleep duration between scheduler loop iterations |
scheduler.timeseries_backup_interval_hours |
u32 |
24 |
Minimum hours between timeseries backups |
sqlite_backup.enabled |
bool |
true |
Set false to disable SQLite backups entirely |
sqlite_backup.interval_minutes |
u32 |
180 |
Minimum minutes between SQLite backups |
sqlite_backup.num_backups_to_keep |
u32 |
6 |
Rolling retention count for SQLite archives |
timeseries_backup.enabled |
bool |
true |
Set false to disable timeseries backups entirely |
timeseries_backup.incremental_only |
bool |
true |
When true, only modified .bin files are archived |
NOTE: Old doc said
sqlite_backup.interval_minutesdefault = 10. Source code shows the runtime default function returns 180 (state.rs:46:fn default_sqlite_interval() -> u32 { 180 }). The previously-stale unit test was fixed inc41a03b—state.rs:242now asserts 180, matching the runtime default.
CLARITY_BACKUP_PATH environment variable, when set, overrides backup_path in all API handlers. It is checked at request time, not at startup.
clarity:backend/src-tauri/src/backup/api.rs:58-63
Persisted to <backup_path>/backup_state.json after each backup attempt.
clarity:backend/src-tauri/src/backup/state.rs:119-165
| Field | Type | Description |
|---|---|---|
last_backup_time |
Option<DateTime<Utc>> |
Set when a SQLite backup completes |
last_sqlite_backup |
Option<BackupResult> |
Result of the most recent SQLite backup attempt |
last_timeseries_backup |
Option<BackupResult> |
Result of the most recent timeseries backup attempt |
last_timeseries_backup_time |
Option<DateTime<Utc>> |
Set when a timeseries backup completes |
is_running |
bool |
Currently always false at rest (not updated during backup) |
BackupResult fields: success: bool, timestamp: DateTime<Utc>, file_name: Option<String>, file_size: Option<u64>, checksum: Option<String>, error_message: Option<String>.
clarity:backend/src-tauri/src/backup/state.rs:128-136
clarity:backend/src-tauri/src/backup/scheduler.rs:10-101
The scheduler runs in a dedicated thread (own Tokio runtime) as an infinite loop:
check_interval_seconds from config.should_run_sqlite_backup(interval_minutes) — true if last_sqlite_backup is None or elapsed minutes ≥ interval_minutes.sqlite_backup.enabled and due: calls run_rolling_sqlite_backup, updates state, persists state.should_run_timeseries_backup(timeseries_backup_interval_hours) — true if last_timeseries_backup_time is None or elapsed hours ≥ interval.timeseries_backup.enabled and due: calls run_incremental_timeseries_backup (when incremental_only=true) or run_timeseries_backup (full), updates state, persists state.check_interval_seconds seconds.The loop reads config and state under tokio::sync::Mutex locks; it does not hold locks across the backup operations.
Source: clarity:backend/src-tauri/src/backup/sqlite_backup.rs
run_rolling_sqlite_backup (sqlite_backup.rs:68-96):
last_backup_time; if interval not met, returns a success=false result with a message — no archive is created.run_sqlite_backup to create the archive.cleanup_old_backups to enforce num_backups_to_keep.run_sqlite_backup (sqlite_backup.rs:9-66):
<app_data_dir>/pulse-db.sqlite.sqlite_backup.rs:33-35<backup_path>/sqlite/sqlite-YYYY-MM-DD_HHMMSS.tar.gz using flate2::GzEncoder + tar::Builder.#[cfg(unix)]): sets file permissions to 0o600. sqlite_backup.rs:44-51<archive>.tar.sha256 (i.e. replaces the .gz extension). sqlite_backup.rs:53-54BackupResult with final archive size.cleanup_old_backups (sqlite_backup.rs:98-133):
<backup_path>/sqlite/, collects .tar.gz files excluding those containing "incremental" in the name.num_backups_to_keep, including their .tar.gz.sha256 sidecar.Source: clarity:backend/src-tauri/src/backup/timeseries_backup.rs
Incremental backup — run_incremental_timeseries_backup (timeseries_backup.rs:65-166):
.bin files whose mtime > last_backup_time. When last_backup_time is None, all .bin files are collected.success=false with message "No new files to backup" — no archive is created.<backup_path>/timeseries/timeseries-incremental-YYYY-MM-DD_HHMMSS.tar.gz, preserving relative paths within the archive.0o600. timeseries_backup.rs:139-145timeseries_backup.rs:148-151<archive>.tar.sha256 sidecar.NOTE: The
incremental_only: boolparameter is accepted byrun_incremental_timeseries_backupbut is not used inside the function body — it always performs incremental logic regardless. Theincremental_onlyconfig field controls which function the scheduler calls, not behaviour within the function.timeseries_backup.rs:69
Full backup — run_timeseries_backup (timeseries_backup.rs:8-63):
storage.get_base_path() directory as timeseries-YYYY-MM-DD.tar.gz.0o600..tar.sha256 sidecar.No rolling cleanup is implemented for timeseries backups.
Registered in main.rs via backup::get_backup_routes(storage_arc.clone()).
clarity:backend/src-tauri/src/main.rs:3143
All routes are defined in clarity:backend/src-tauri/src/backup/api.rs.
Authentication: Admin JWT required on all six routes. The api.rs handler functions contain no inline auth filter; authentication is applied at route composition in main.rs via admin_middleware. Confirmed from wiki/dev/architecture/api-server.md (backup route table, verified from source).
| Method | Path | Request body | Response |
|---|---|---|---|
GET |
/api/admin/backup/status |
— | BackupState as JSON (last times, is_running, sqlite_backup result, timeseries_backup result) |
GET |
/api/admin/backup/list |
— | {sqlite_backups: [...], timeseries_backups: [...]} — each entry has name, size, created, checksum, is_incremental |
GET |
/api/admin/backup/config |
— | Current BackupConfig as JSON |
PUT |
/api/admin/backup/config |
BackupConfig JSON |
{success: true} or {success: false, error: "..."} |
POST |
/api/admin/backup/restore/sqlite |
{"file_name": "<archive>.tar.gz"} |
{success: true/false, message: "..."} |
POST |
/api/admin/backup/restore/timeseries |
{"file_name": "<archive>.tar.gz"} |
{success: true/false, message: "..."} |
clarity:backend/src-tauri/src/backup/api.rs:20-313
CLARITY_BACKUP_PATH environment variable is checked at request time in backup_list, get_config, update_config, restore_sqlite, and restore_timeseries. When set, it takes precedence over the value in backup_config.json.
clarity:backend/src-tauri/src/backup/api.rs:58-63, 161-163, 178-180, 198-199, 256-257
POST /api/admin/backup/restore/sqlite)clarity:backend/src-tauri/src/backup/api.rs:193-249
<backup_path>/sqlite/<file_name>.pulse-db.sqlite exists, renames it to pulse-db.sqlite.bak before extraction..tar.gz archive into app_data_dir — this overwrites pulse-db.sqlite.{success: true, message: "SQLite restored successfully"}.The .bak file is not automatically cleaned up. A failed extraction leaves the .bak in place.
POST /api/admin/backup/restore/timeseries)clarity:backend/src-tauri/src/backup/api.rs:251-306
<backup_path>/timeseries/<file_name>.data/ directory exists, renames it to data_backup_temp/ before extraction..tar.gz archive into app_data_dir.{success: true, message: "Timeseries restored successfully"}.The data_backup_temp/ directory is not automatically cleaned up after a successful restore.
<backup_path>/
backup_state.json ← persisted BackupState
sqlite/
sqlite-2024-01-15_143022.tar.gz ← SQLite archive
sqlite-2024-01-15_143022.tar.sha256 ← SHA-256 of raw SQLite bytes
...
timeseries/
timeseries-2024-01-15.tar.gz ← full backup archive
timeseries-2024-01-15.tar.sha256
timeseries-incremental-2024-01-15_160000.tar.gz ← incremental
timeseries-incremental-2024-01-15_160000.tar.sha256
...
<app_data_dir>/
backup_config.json ← BackupConfig (separate from backup_path)
pulse-db.sqlite
data/
...
backup_config.json is stored in app_data_dir, not inside backup_path. backup_path is derived from config (or defaults to <app_data_dir>/backups).
clarity:backend/src-tauri/src/backup/state.rs:89-116
| Variable | Effect |
|---|---|
CLARITY_BACKUP_PATH |
Overrides backup_path config field at request time in all API handlers |
Scheduler loop with two parallel backup branches — SQLite and timeseries — evaluated independently on each iteration.
Sources: clarity:backend/src-tauri/src/backup/scheduler.rs, clarity:backend/src-tauri/src/backup/sqlite_backup.rs, clarity:backend/src-tauri/src/backup/timeseries_backup.rs
Diagram generated from source. If implementation has changed, update the Mermaid source in this file directly.
Last updated: 2026-07-11 from clarity@c41a03b