chore(backup): implement standalone regular backup

This commit is contained in:
Elian Doran
2026-04-12 18:30:57 +03:00
parent 745374050e
commit 0d5c9986b6
3 changed files with 82 additions and 59 deletions

View File

@@ -53,10 +53,10 @@ export default class StandaloneBackupService extends BackupService {
// Serialize the database
const data = getSql().serialize();
// Write to OPFS (convert to Blob for compatibility)
// Write to OPFS
const fileHandle = await dir.getFileHandle(fileName, { create: true });
const writable = await fileHandle.createWritable();
await writable.write(new Blob([data]));
await writable.write(data);
await writable.close();
console.log(`[Backup] Created backup: ${fileName} (${data.byteLength} bytes)`);
@@ -68,13 +68,6 @@ export default class StandaloneBackupService extends BackupService {
}
}
override regularBackup(): void {
// For standalone, we don't implement scheduled backups yet
// since we don't have easy access to the options service for
// checking daily/weekly/monthly settings.
// Manual backups via backupNow() still work.
}
override getExistingBackups(): DatabaseBackup[] {
// This method is synchronous in the interface, but OPFS is async.
// Return empty array - the UI can use an async method if needed.

View File

@@ -1,17 +1,12 @@
import type { DatabaseBackup, OptionNames } from "@triliumnext/commons";
import type { DatabaseBackup } from "@triliumnext/commons";
import { BackupService, sync_mutex as syncMutexService } from "@triliumnext/core";
import fs from "fs";
import path from "path";
import cls from "./services/cls.js";
import dataDir from "./services/data_dir.js";
import dateUtils from "./services/date_utils.js";
import log from "./services/log.js";
import optionService from "./services/options.js";
import sql from "./services/sql.js";
type BackupType = "daily" | "weekly" | "monthly";
export default class ServerBackupService extends BackupService {
override getExistingBackups(): DatabaseBackup[] {
if (!fs.existsSync(dataDir.BACKUP_DIR)) {
@@ -29,48 +24,7 @@ export default class ServerBackupService extends BackupService {
});
}
override regularBackup(): void {
cls.init(() => {
this.periodBackup("lastDailyBackupDate", "daily", 24 * 3600);
this.periodBackup("lastWeeklyBackupDate", "weekly", 7 * 24 * 3600);
this.periodBackup("lastMonthlyBackupDate", "monthly", 30 * 24 * 3600);
});
}
private isBackupEnabled(backupType: BackupType): boolean {
let optionName: OptionNames;
switch (backupType) {
case "daily":
optionName = "dailyBackupEnabled";
break;
case "weekly":
optionName = "weeklyBackupEnabled";
break;
case "monthly":
optionName = "monthlyBackupEnabled";
break;
}
return optionService.getOptionBool(optionName);
}
private periodBackup(
optionName: "lastDailyBackupDate" | "lastWeeklyBackupDate" | "lastMonthlyBackupDate",
backupType: BackupType,
periodInSeconds: number
): void {
if (!this.isBackupEnabled(backupType)) {
return;
}
const now = new Date();
const lastBackupDate = dateUtils.parseDateTime(optionService.getOption(optionName));
if (now.getTime() - lastBackupDate.getTime() > periodInSeconds * 1000) {
this.backupNow(backupType);
optionService.setOption(optionName, dateUtils.utcNowDateTime());
}
}
// regularBackup() inherited from BackupService - uses getContext().init()
override async backupNow(name: string): Promise<string> {
// we don't want to back up DB in the middle of sync with potentially inconsistent DB state

View File

@@ -1,4 +1,18 @@
import type { DatabaseBackup } from "@triliumnext/commons";
import type { DatabaseBackup, OptionNames } from "@triliumnext/commons";
import { getContext } from "./context.js";
import dateUtils from "./utils/date.js";
type BackupType = "daily" | "weekly" | "monthly";
// Lazy-loaded to avoid circular dependency (options -> becca -> entities)
let optionsModule: Awaited<typeof import("./options.js")>["default"] | null = null;
async function getOptions() {
if (!optionsModule) {
optionsModule = (await import("./options.js")).default;
}
return optionsModule!;
}
/**
* Abstract backup service class.
@@ -14,13 +28,75 @@ export default abstract class BackupService {
/**
* Perform regular scheduled backups (daily, weekly, monthly).
* Called periodically by the scheduler.
* Default implementation runs inside an execution context.
*/
abstract regularBackup(): void;
regularBackup(): void {
getContext().init(() => {
// Fire and forget - the async work runs in background
this.runScheduledBackups().catch(err => {
console.error("[Backup] Error running scheduled backups:", err);
});
});
}
/**
* Get list of existing backups.
*/
abstract getExistingBackups(): DatabaseBackup[];
/**
* Run the scheduled backup checks for daily, weekly, and monthly backups.
* Can be overridden by subclasses if they need custom behavior.
*/
protected async runScheduledBackups(): Promise<void> {
await this.periodBackup("lastDailyBackupDate", "daily", 24 * 3600);
await this.periodBackup("lastWeeklyBackupDate", "weekly", 7 * 24 * 3600);
await this.periodBackup("lastMonthlyBackupDate", "monthly", 30 * 24 * 3600);
}
/**
* Check if a specific backup type is enabled via options.
*/
protected async isBackupEnabled(backupType: BackupType): Promise<boolean> {
const options = await getOptions();
let optionName: OptionNames;
switch (backupType) {
case "daily":
optionName = "dailyBackupEnabled";
break;
case "weekly":
optionName = "weeklyBackupEnabled";
break;
case "monthly":
optionName = "monthlyBackupEnabled";
break;
}
return options.getOptionBool(optionName);
}
/**
* Check if a periodic backup is due and create it if so.
*/
protected async periodBackup(
optionName: "lastDailyBackupDate" | "lastWeeklyBackupDate" | "lastMonthlyBackupDate",
backupType: BackupType,
periodInSeconds: number
): Promise<void> {
if (!(await this.isBackupEnabled(backupType))) {
return;
}
const options = await getOptions();
const now = new Date();
const lastBackupDate = dateUtils.parseDateTime(options.getOption(optionName));
if (now.getTime() - lastBackupDate.getTime() > periodInSeconds * 1000) {
await this.backupNow(backupType);
options.setOption(optionName, dateUtils.utcNowDateTime());
}
}
}
let backupService: BackupService | undefined;