diff options
| author | Anders Betts <anders.betts@gmail.com> | 2026-09-20 09:51:28 +0200 |
|---|---|---|
| committer | Anders Betts <anders.betts@gmail.com> | 2026-09-20 09:51:28 +0200 |
| commit | b5349b52e757399d8ddb6325120a58e7d9e6e1f6 (patch) | |
| tree | fa6771abbcaf5c654968d5e8c5ff27c0ec292564 /src/db.c | |
| parent | 67179a7ae9f6e399701434775a2ef345367b76bf (diff) | |
| download | bokf-b5349b52e757399d8ddb6325120a58e7d9e6e1f6.tar.gz bokf-b5349b52e757399d8ddb6325120a58e7d9e6e1f6.zip | |
db: snapshot the database before forward migrations
When db_open finds an older schema version, write a consistent
VACUUM INTO copy to <backup_dir>/pre-migration-v<old>-<UTC>.db
before the first migration statement. A taken name gets a numeric
suffix; if the snapshot fails, the open and the migration abort.
Diffstat (limited to 'src/db.c')
| -rw-r--r-- | src/db.c | 76 |
1 files changed, 76 insertions, 0 deletions
@@ -1,9 +1,13 @@ #include "db.h" +#include <errno.h> #include <stdarg.h> #include <stdio.h> #include <stdlib.h> #include <string.h> +#include <sys/stat.h> +#include <time.h> +#include <unistd.h> #include "auth.h" #include "config.h" @@ -469,6 +473,74 @@ static int db_upgrade(sqlite3 *db, int from, char **err) return 0; } +static int mkdir_p(const char *path, mode_t mode) +{ + char tmp[4096]; + if (!path || strlen(path) >= sizeof tmp) + return -1; + strcpy(tmp, path); + for (char *p = tmp + 1; *p; p++) { + if (*p == '/') { + *p = '\0'; + if (mkdir(tmp, mode) != 0 && errno != EEXIST) + return -1; + *p = '/'; + } + } + if (mkdir(tmp, mode) != 0 && errno != EEXIST) + return -1; + return 0; +} + +static int pre_migration_snapshot(sqlite3 *db, int old_version, char **err) +{ + if (mkdir_p(g_cfg.backup_dir, 0700) != 0) { + set_err(err, "pre-migration snapshot: cannot create backup directory %s", + g_cfg.backup_dir); + return -1; + } + char stamp[32]; + time_t t = (time_t)util_now(); + struct tm tm; + gmtime_r(&t, &tm); + strftime(stamp, sizeof stamp, "%Y%m%dT%H%M%SZ", &tm); + char path[4096]; + for (int n = 0;; n++) { + int len = n == 0 + ? snprintf(path, sizeof path, + "%s/pre-migration-v%d-%s.db", + g_cfg.backup_dir, old_version, stamp) + : snprintf(path, sizeof path, + "%s/pre-migration-v%d-%s-%d.db", + g_cfg.backup_dir, old_version, stamp, n); + if (len < 0 || (size_t)len >= sizeof path) { + set_err(err, "pre-migration snapshot path is too long"); + return -1; + } + if (access(path, F_OK) != 0) + break; + if (n >= 1000) { + set_err(err, "cannot find a free pre-migration snapshot name in %s", + g_cfg.backup_dir); + return -1; + } + } + char *sql = sqlite3_mprintf("VACUUM INTO %Q", path); + if (!sql) { + set_err(err, "out of memory"); + return -1; + } + int rc = db_exec(db, sql, err); + sqlite3_free(sql); + if (rc != 0) { + set_err(err, "pre-migration snapshot to %s failed: %s", path, + err && *err ? *err : sqlite3_errmsg(db)); + return -1; + } + log_info("pre-migration snapshot: %s", path); + return 0; +} + int db_open(const char *path, sqlite3 **out, char **err) { sqlite3 *db = NULL; @@ -508,6 +580,10 @@ int db_open(const char *path, sqlite3 **out, char **err) return -1; } if (version < BOKF_SCHEMA_VERSION) { + if (pre_migration_snapshot(db, version, err) != 0) { + sqlite3_close(db); + return -1; + } if (db_upgrade(db, version, err) != 0) { sqlite3_close(db); return -1; |
