mirror of
https://github.com/zadam/trilium.git
synced 2026-05-06 17:17:37 +02:00
chore(backup): implement standalone regular backup
This commit is contained in:
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user