refactor(backup): constructor-based dependency injection for options

This commit is contained in:
Elian Doran
2026-04-12 18:36:21 +03:00
parent 35317b3dab
commit f034454ec9
9 changed files with 36 additions and 43 deletions

View File

@@ -1,5 +1,5 @@
import type { DatabaseBackup } from "@triliumnext/commons";
import { BackupService, getSql } from "@triliumnext/core";
import { BackupOptionsService, BackupService, getSql } from "@triliumnext/core";
const BACKUP_DIR_NAME = "backups";
const BACKUP_FILE_PATTERN = /^backup-.*\.db$/;
@@ -13,6 +13,10 @@ export default class StandaloneBackupService extends BackupService {
private backupDir: FileSystemDirectoryHandle | null = null;
private opfsAvailable: boolean | null = null;
constructor(getOptions: () => BackupOptionsService) {
super(getOptions);
}
private isOpfsAvailable(): boolean {
if (this.opfsAvailable === null) {
this.opfsAvailable = typeof navigator !== "undefined"

View File

@@ -176,7 +176,7 @@ async function initialize(): Promise<void> {
request: new FetchRequestProvider(),
platform: new StandalonePlatformProvider(queryString),
log: logService,
backup: new StandaloneBackupService(),
backup: new StandaloneBackupService(() => coreModule!.options),
translations: translationProvider,
schema: schemaModule.default,
getDemoArchive: async () => {

View File

@@ -2,7 +2,7 @@ import { createRequire } from "node:module";
import { readFileSync } from "node:fs";
import { fileURLToPath } from "node:url";
import { initializeCore } from "@triliumnext/core";
import { initializeCore, options } from "@triliumnext/core";
import schemaSql from "@triliumnext/core/src/assets/schema.sql?raw";
import HappyDomHtmlParser from "happy-dom/lib/html-parser/HTMLParser.js";
import serverEnTranslations from "../../server/src/assets/translations/en/server.json";
@@ -130,7 +130,7 @@ beforeAll(async () => {
});
},
platform: new StandalonePlatformProvider(""),
backup: new StandaloneBackupService(),
backup: new StandaloneBackupService(() => options),
schema: schemaSql,
dbConfig: {
provider: sqlProvider,

View File

@@ -4,7 +4,7 @@ import NodejsCryptoProvider from "@triliumnext/server/src/crypto_provider.js";
import { loadCoreSchema } from "@triliumnext/server/src/core_assets.js";
import NodejsInAppHelpProvider from "@triliumnext/server/src/in_app_help_provider.js";
import dataDirs from "@triliumnext/server/src/services/data_dir.js";
import options from "@triliumnext/server/src/services/options.js";
import { options } from "@triliumnext/core";
import port from "@triliumnext/server/src/services/port.js";
import NodeRequestProvider from "@triliumnext/server/src/services/request.js";
import { RESOURCE_DIR } from "@triliumnext/server/src/services/resource_dir.js";
@@ -151,7 +151,7 @@ async function main() {
// both source and bundled-production modes.
getDemoArchive: async () => fs.readFileSync(path.join(RESOURCE_DIR, "db", "demo.zip")),
inAppHelp: new NodejsInAppHelpProvider(),
backup: new ServerBackupService(),
backup: new ServerBackupService(() => options),
image: (await import("@triliumnext/server/src/services/image_provider.js")).serverImageProvider,
extraAppInfo: {
nodeVersion: process.version,

View File

@@ -1,7 +1,7 @@
import { beforeAll } from "vitest";
import { readFileSync } from "fs";
import { join } from "path";
import { initializeCore } from "@triliumnext/core";
import { initializeCore, options } from "@triliumnext/core";
import { serverZipExportProviderFactory } from "../src/services/export/zip/factory.js";
import ServerBackupService from "../src/backup_provider.js";
import ClsHookedExecutionContext from "../src/cls_provider.js";
@@ -44,6 +44,6 @@ beforeAll(async () => {
platform: new ServerPlatformProvider(),
translations: initializeTranslationsWithParams,
inAppHelp: new NodejsInAppHelpProvider(),
backup: new ServerBackupService()
backup: new ServerBackupService(() => options)
});
});

View File

@@ -1,5 +1,5 @@
import type { DatabaseBackup } from "@triliumnext/commons";
import { BackupService, sync_mutex as syncMutexService } from "@triliumnext/core";
import { BackupOptionsService, BackupService, sync_mutex as syncMutexService } from "@triliumnext/core";
import fs from "fs";
import path from "path";
@@ -8,6 +8,10 @@ import log from "./services/log.js";
import sql from "./services/sql.js";
export default class ServerBackupService extends BackupService {
constructor(getOptions: () => BackupOptionsService) {
super(getOptions);
}
override getExistingBackups(): DatabaseBackup[] {
if (!fs.existsSync(dataDir.BACKUP_DIR)) {
return [];

View File

@@ -3,7 +3,7 @@
* are loaded later and will result in an empty string.
*/
import { getLog, initializeCore, sql_init } from "@triliumnext/core";
import { getLog, initializeCore, options, sql_init } from "@triliumnext/core";
import fs from "fs";
import { t } from "i18next";
import path from "path";
@@ -81,7 +81,7 @@ async function startApplication() {
// both source and bundled-production modes.
getDemoArchive: async () => fs.readFileSync(path.join(RESOURCE_DIR, "db", "demo.zip")),
inAppHelp: new NodejsInAppHelpProvider(),
backup: new ServerBackupService(),
backup: new ServerBackupService(() => options),
image: (await import("./services/image_provider.js")).serverImageProvider,
extraAppInfo: {
nodeVersion: process.version,

View File

@@ -1,7 +1,7 @@
import { ExecutionContext, initContext } from "./services/context";
import { CryptoProvider, initCrypto } from "./services/encryption/crypto";
import LogService, { getLog, initLog } from "./services/log";
import BackupService, { initBackup } from "./services/backup";
import BackupService, { initBackup, type BackupOptionsService } from "./services/backup";
import { initSql } from "./services/sql/index";
import { SqlService, SqlServiceParams } from "./services/sql/sql";
import { initMessaging, MessagingProvider } from "./services/messaging/index";
@@ -17,7 +17,7 @@ import { type ImageProvider, initImageProvider } from "./services/image_provider
export { default as LogService, getLog } from "./services/log";
export { default as FileBasedLogService, type LogFileInfo } from "./services/file_based_log";
export { default as BackupService, getBackup, initBackup } from "./services/backup";
export { default as BackupService, getBackup, initBackup, type BackupOptionsService } from "./services/backup";
export type * from "./services/sql/types";
export * from "./services/sql/index";
export { default as sql_init } from "./services/sql_init";

View File

@@ -1,17 +1,13 @@
import type { DatabaseBackup, OptionNames } from "@triliumnext/commons";
import type { DatabaseBackup, FilterOptionsByType, 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!;
export interface BackupOptionsService {
getOption(name: OptionNames): string;
getOptionBool(name: FilterOptionsByType<boolean>): boolean;
setOption(name: OptionNames, value: string): void;
}
/**
@@ -19,6 +15,8 @@ async function getOptions() {
* Platform-specific implementations must extend this class.
*/
export default abstract class BackupService {
constructor(protected readonly getOptions: () => BackupOptionsService) {}
/**
* Create a backup with the given name.
* Returns the backup file path/name.
@@ -32,7 +30,6 @@ export default abstract class BackupService {
*/
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);
});
@@ -46,7 +43,6 @@ export default abstract class BackupService {
/**
* 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);
@@ -57,22 +53,13 @@ export default abstract class BackupService {
/**
* 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;
}
protected isBackupEnabled(backupType: BackupType): boolean {
const optionName: FilterOptionsByType<boolean> =
backupType === "daily" ? "dailyBackupEnabled" :
backupType === "weekly" ? "weeklyBackupEnabled" :
"monthlyBackupEnabled";
return options.getOptionBool(optionName);
return this.getOptions().getOptionBool(optionName);
}
/**
@@ -83,12 +70,11 @@ export default abstract class BackupService {
backupType: BackupType,
periodInSeconds: number
): Promise<void> {
if (!(await this.isBackupEnabled(backupType))) {
if (!this.isBackupEnabled(backupType)) {
return;
}
const options = await getOptions();
const options = this.getOptions();
const now = new Date();
const lastBackupDate = dateUtils.parseDateTime(options.getOption(optionName));
@@ -103,7 +89,6 @@ let backupService: BackupService | undefined;
/**
* Get the current backup service instance.
* Throws if no provider has been initialized.
*/
export function getBackup(): BackupService {
if (!backupService) {