aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorAnders Betts <anders.betts@gmail.com>2026-09-20 09:51:28 +0200
committerAnders Betts <anders.betts@gmail.com>2026-09-20 09:51:28 +0200
commitb5349b52e757399d8ddb6325120a58e7d9e6e1f6 (patch)
treefa6771abbcaf5c654968d5e8c5ff27c0ec292564 /src
parent67179a7ae9f6e399701434775a2ef345367b76bf (diff)
downloadbokf-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')
-rw-r--r--src/db.c76
1 files changed, 76 insertions, 0 deletions
diff --git a/src/db.c b/src/db.c
index d3bd15f..627efbc 100644
--- a/src/db.c
+++ b/src/db.c
@@ -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;