mirror of
https://github.com/fosrl/pangolin.git
synced 2026-09-16 23:59:53 +02:00
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.
This commit is contained in:
@@ -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);
|
||||||
|
}
|
||||||
@@ -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}`;
|
||||||
|
}
|
||||||
@@ -5,6 +5,7 @@ import path from "path";
|
|||||||
import semver from "semver";
|
import semver from "semver";
|
||||||
import { versionMigrations } from "../db/sqlite";
|
import { versionMigrations } from "../db/sqlite";
|
||||||
import { __DIRNAME, APP_PATH, APP_VERSION } from "@server/lib/consts";
|
import { __DIRNAME, APP_PATH, APP_VERSION } from "@server/lib/consts";
|
||||||
|
import { formatBackupTimestamp } from "@server/lib/backupFileName";
|
||||||
import { SqliteError } from "better-sqlite3";
|
import { SqliteError } from "better-sqlite3";
|
||||||
import fs from "fs";
|
import fs from "fs";
|
||||||
import { build } from "@server/build";
|
import { build } from "@server/build";
|
||||||
@@ -121,7 +122,7 @@ function backupDb() {
|
|||||||
// copy the db.sqlite file to backups
|
// copy the db.sqlite file to backups
|
||||||
// add the date to the filename
|
// add the date to the filename
|
||||||
const date = new Date();
|
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 dbPath = path.join(dbDir, "db.sqlite");
|
||||||
const backupPath = path.join(backupsDir, `db_${dateString}.sqlite`);
|
const backupPath = path.join(backupsDir, `db_${dateString}.sqlite`);
|
||||||
fs.copyFileSync(dbPath, backupPath);
|
fs.copyFileSync(dbPath, backupPath);
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import {
|
|||||||
users
|
users
|
||||||
} from "../../db/sqlite";
|
} from "../../db/sqlite";
|
||||||
import { APP_PATH, configFilePath1, configFilePath2 } from "@server/lib/consts";
|
import { APP_PATH, configFilePath1, configFilePath2 } from "@server/lib/consts";
|
||||||
|
import { formatBackupTimestamp } from "@server/lib/backupFileName";
|
||||||
import { eq, sql } from "drizzle-orm";
|
import { eq, sql } from "drizzle-orm";
|
||||||
import fs from "fs";
|
import fs from "fs";
|
||||||
import * as yaml from "js-yaml";
|
import * as yaml from "js-yaml";
|
||||||
@@ -34,7 +35,7 @@ export default async function migration() {
|
|||||||
// copy the db.sqlite file to backups
|
// copy the db.sqlite file to backups
|
||||||
// add the date to the filename
|
// add the date to the filename
|
||||||
const date = new Date();
|
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 dbPath = path.join(dbDir, "db.sqlite");
|
||||||
const backupPath = path.join(backupsDir, `db_${dateString}.sqlite`);
|
const backupPath = path.join(backupsDir, `db_${dateString}.sqlite`);
|
||||||
fs.copyFileSync(dbPath, backupPath);
|
fs.copyFileSync(dbPath, backupPath);
|
||||||
|
|||||||
Reference in New Issue
Block a user