diff --git a/apps/client-standalone/src/lightweight/backup_provider.ts b/apps/client-standalone/src/lightweight/backup_provider.ts index 2c93b6fc2e..d29bf77d8a 100644 --- a/apps/client-standalone/src/lightweight/backup_provider.ts +++ b/apps/client-standalone/src/lightweight/backup_provider.ts @@ -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. diff --git a/apps/server/src/backup_provider.ts b/apps/server/src/backup_provider.ts index 2dbbaa66a6..3af14dceec 100644 --- a/apps/server/src/backup_provider.ts +++ b/apps/server/src/backup_provider.ts @@ -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 { // we don't want to back up DB in the middle of sync with potentially inconsistent DB state diff --git a/packages/trilium-core/src/services/backup.ts b/packages/trilium-core/src/services/backup.ts index ee09d1441b..9323134aef 100644 --- a/packages/trilium-core/src/services/backup.ts +++ b/packages/trilium-core/src/services/backup.ts @@ -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["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 { + 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 { + 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 { + 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;