From 9298ec7cdb2fea861ea0261a65a2c96a09c8b10f Mon Sep 17 00:00:00 2001 From: Hayyan Hajwani <129366924+Hayyan612@users.noreply.github.com> Date: Wed, 16 Sep 2026 13:46:57 +0530 Subject: [PATCH] fix(setup): correct month index and zero-pad database backup file names Backup names were built inline from Date#getMonth, which is zero-indexed, so a backup taken on 12 September 2026 was written as db_2026-8-12_20-35-56.sqlite. No field was zero-padded either, giving names like db_2026-8-12_20-36-2.sqlite. Extract formatBackupTimestamp into server/lib and use it from both places that built the string: the backupDb helper in migrationsSqlite.ts and the inline copy in the 1.0.0-beta9 setup script. Padding every field also makes the names sort lexicographically in the order the backups were taken. Adds tests covering both reported names, single-digit padding and sort order. Reverting the helper to the old formula fails them with the exact name from the report. --- server/lib/backupFileName.test.ts | 101 ++++++++++++++++++++++ server/lib/backupFileName.ts | 28 ++++++ server/setup/migrationsSqlite.ts | 3 +- server/setup/scriptsSqlite/1.0.0-beta9.ts | 3 +- 4 files changed, 133 insertions(+), 2 deletions(-) create mode 100644 server/lib/backupFileName.test.ts create mode 100644 server/lib/backupFileName.ts diff --git a/server/lib/backupFileName.test.ts b/server/lib/backupFileName.test.ts new file mode 100644 index 000000000..01fc4796a --- /dev/null +++ b/server/lib/backupFileName.test.ts @@ -0,0 +1,101 @@ +import { formatBackupTimestamp } from "./backupFileName"; +import { assertEquals } from "@test/assert"; + +// Local-time constructors are used throughout, matching formatBackupTimestamp, +// so these cases do not depend on the machine's timezone. + +function testMonthIsOneIndexed() { + console.log("Running month indexing tests..."); + + // The case from the report: a backup taken on 12 September 2026 was named + // db_2026-8-12_... because Date#getMonth is zero-indexed. + { + const result = formatBackupTimestamp(new Date(2026, 8, 12, 20, 35, 56)); + assertEquals( + result, + "2026-09-12_20-35-56", + "September must render as 09, not 8" + ); + } + + // The other reported name, db_2026-0-23_..., was a January backup. + { + const result = formatBackupTimestamp(new Date(2026, 0, 23, 20, 25, 49)); + assertEquals( + result, + "2026-01-23_20-25-49", + "January must render as 01, not 0" + ); + } + + { + const result = formatBackupTimestamp(new Date(2026, 11, 31, 23, 59, 59)); + assertEquals( + result, + "2026-12-31_23-59-59", + "December must render as 12" + ); + } +} + +function testEveryFieldIsZeroPadded() { + console.log("Running zero padding tests..."); + + // db_2026-8-12_20-36-2 in the report: a single-digit second was not padded. + { + const result = formatBackupTimestamp(new Date(2026, 8, 12, 20, 36, 2)); + assertEquals( + result, + "2026-09-12_20-36-02", + "Single-digit seconds must be padded" + ); + } + + { + const result = formatBackupTimestamp(new Date(2026, 0, 1, 0, 0, 0)); + assertEquals( + result, + "2026-01-01_00-00-00", + "Midnight on the first of the month must pad every field" + ); + } +} + +function testNamesSortChronologically() { + console.log("Running sort order tests..."); + + // Zero padding means a plain lexicographic sort of the backups directory + // lists the backups in the order they were taken. + const taken = [ + new Date(2026, 8, 12, 20, 36, 2), + new Date(2026, 0, 23, 20, 25, 49), + new Date(2026, 8, 12, 20, 35, 56), + new Date(2026, 11, 31, 23, 59, 59) + ]; + + const sorted = taken + .map((date) => formatBackupTimestamp(date)) + .sort(); + + assertEquals( + sorted.join(","), + [ + "2026-01-23_20-25-49", + "2026-09-12_20-35-56", + "2026-09-12_20-36-02", + "2026-12-31_23-59-59" + ].join(","), + "Backup names must sort into the order the backups were taken" + ); +} + +// Run all tests +try { + testMonthIsOneIndexed(); + testEveryFieldIsZeroPadded(); + testNamesSortChronologically(); + console.log("All tests passed successfully!"); +} catch (error) { + console.error("Test failed:", error); + process.exit(1); +} diff --git a/server/lib/backupFileName.ts b/server/lib/backupFileName.ts new file mode 100644 index 000000000..a5888450a --- /dev/null +++ b/server/lib/backupFileName.ts @@ -0,0 +1,28 @@ +/** + * Builds the timestamp segment of a database backup file name. + * + * `Date#getMonth` is zero-indexed, so building this inline produced names like + * `db_2026-8-12_...` for a backup taken on 12 September 2026. Every field is + * also zero-padded, which keeps the names unambiguous and makes them sort + * lexicographically in the order they were taken. + * + * @param date The moment the backup is being taken. Defaults to now. + * @returns A timestamp of the form `YYYY-MM-DD_HH-MM-SS`. + */ +export function formatBackupTimestamp(date: Date = new Date()): string { + const pad = (value: number): string => String(value).padStart(2, "0"); + + const datePart = [ + date.getFullYear(), + pad(date.getMonth() + 1), + pad(date.getDate()) + ].join("-"); + + const timePart = [ + pad(date.getHours()), + pad(date.getMinutes()), + pad(date.getSeconds()) + ].join("-"); + + return `${datePart}_${timePart}`; +} diff --git a/server/setup/migrationsSqlite.ts b/server/setup/migrationsSqlite.ts index 19e968074..6645d617f 100644 --- a/server/setup/migrationsSqlite.ts +++ b/server/setup/migrationsSqlite.ts @@ -5,6 +5,7 @@ import path from "path"; import semver from "semver"; import { versionMigrations } from "../db/sqlite"; import { __DIRNAME, APP_PATH, APP_VERSION } from "@server/lib/consts"; +import { formatBackupTimestamp } from "@server/lib/backupFileName"; import { SqliteError } from "better-sqlite3"; import fs from "fs"; import { build } from "@server/build"; @@ -121,7 +122,7 @@ function backupDb() { // copy the db.sqlite file to backups // add the date to the filename const date = new Date(); - const dateString = `${date.getFullYear()}-${date.getMonth()}-${date.getDate()}_${date.getHours()}-${date.getMinutes()}-${date.getSeconds()}`; + const dateString = formatBackupTimestamp(date); const dbPath = path.join(dbDir, "db.sqlite"); const backupPath = path.join(backupsDir, `db_${dateString}.sqlite`); fs.copyFileSync(dbPath, backupPath); diff --git a/server/setup/scriptsSqlite/1.0.0-beta9.ts b/server/setup/scriptsSqlite/1.0.0-beta9.ts index d2629afc3..6766c0756 100644 --- a/server/setup/scriptsSqlite/1.0.0-beta9.ts +++ b/server/setup/scriptsSqlite/1.0.0-beta9.ts @@ -10,6 +10,7 @@ import { users } from "../../db/sqlite"; import { APP_PATH, configFilePath1, configFilePath2 } from "@server/lib/consts"; +import { formatBackupTimestamp } from "@server/lib/backupFileName"; import { eq, sql } from "drizzle-orm"; import fs from "fs"; import * as yaml from "js-yaml"; @@ -34,7 +35,7 @@ export default async function migration() { // copy the db.sqlite file to backups // add the date to the filename const date = new Date(); - const dateString = `${date.getFullYear()}-${date.getMonth()}-${date.getDate()}_${date.getHours()}-${date.getMinutes()}-${date.getSeconds()}`; + const dateString = formatBackupTimestamp(date); const dbPath = path.join(dbDir, "db.sqlite"); const backupPath = path.join(backupsDir, `db_${dateString}.sqlite`); fs.copyFileSync(dbPath, backupPath);