mirror of
https://github.com/zadam/trilium.git
synced 2026-05-07 01:06:00 +02:00
core(standalone): integrate backup management using provider
This commit is contained in:
@@ -3,6 +3,7 @@ import { readFileSync } from "fs";
|
||||
import { join } from "path";
|
||||
import { initializeCore } 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";
|
||||
import NodejsCryptoProvider from "../src/crypto_provider.js";
|
||||
import NodejsZipProvider from "../src/zip_provider.js";
|
||||
@@ -42,6 +43,7 @@ beforeAll(async () => {
|
||||
schema: readFileSync(require.resolve("@triliumnext/core/src/assets/schema.sql"), "utf-8"),
|
||||
platform: new ServerPlatformProvider(),
|
||||
translations: initializeTranslationsWithParams,
|
||||
inAppHelp: new NodejsInAppHelpProvider()
|
||||
inAppHelp: new NodejsInAppHelpProvider(),
|
||||
backup: new ServerBackupService()
|
||||
});
|
||||
});
|
||||
|
||||
@@ -122,8 +122,6 @@ export default async function buildApp() {
|
||||
const { sync, consistency_checks, scheduler } = await import("@triliumnext/core");
|
||||
sync.startSyncTimer();
|
||||
|
||||
await import("./services/backup.js");
|
||||
|
||||
consistency_checks.startConsistencyChecks();
|
||||
scheduler.startScheduler();
|
||||
|
||||
|
||||
91
apps/server/src/backup_provider.ts
Normal file
91
apps/server/src/backup_provider.ts
Normal file
@@ -0,0 +1,91 @@
|
||||
import type { DatabaseBackup, OptionNames } 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)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return fs
|
||||
.readdirSync(dataDir.BACKUP_DIR)
|
||||
.filter((fileName) => fileName.includes("backup"))
|
||||
.map((fileName) => {
|
||||
const filePath = path.resolve(dataDir.BACKUP_DIR, fileName);
|
||||
const stat = fs.statSync(filePath);
|
||||
|
||||
return { fileName, filePath, mtime: stat.mtime };
|
||||
});
|
||||
}
|
||||
|
||||
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());
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
return await syncMutexService.doExclusively(async () => {
|
||||
const backupFile = path.resolve(`${dataDir.BACKUP_DIR}/backup-${name}.db`);
|
||||
|
||||
if (!fs.existsSync(dataDir.BACKUP_DIR)) {
|
||||
fs.mkdirSync(dataDir.BACKUP_DIR, 0o700);
|
||||
}
|
||||
|
||||
log.info("Creating backup...");
|
||||
await sql.copyDatabase(backupFile);
|
||||
log.info(`Created backup at ${backupFile}`);
|
||||
|
||||
return backupFile;
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,11 +1,11 @@
|
||||
import { getBackup } from "@triliumnext/core";
|
||||
import type { Router } from "express";
|
||||
|
||||
import backupService from "../services/backup.js";
|
||||
import eu from "./etapi_utils.js";
|
||||
|
||||
function register(router: Router) {
|
||||
eu.route<{ backupName: string }>(router, "put", "/etapi/backup/:backupName", (req, res, next) => {
|
||||
backupService.backupNow(req.params.backupName)
|
||||
getBackup().backupNow(req.params.backupName)
|
||||
.then(() => res.sendStatus(204))
|
||||
.catch(() => res.sendStatus(500));
|
||||
});
|
||||
|
||||
@@ -8,6 +8,7 @@ import fs from "fs";
|
||||
import { t } from "i18next";
|
||||
import path from "path";
|
||||
|
||||
import ServerBackupService from "./backup_provider.js";
|
||||
import ClsHookedExecutionContext from "./cls_provider.js";
|
||||
import { getIntegrationTestDbPath, loadCoreSchema } from "./core_assets.js";
|
||||
import NodejsCryptoProvider from "./crypto_provider.js";
|
||||
@@ -80,6 +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(),
|
||||
extraAppInfo: {
|
||||
nodeVersion: process.version,
|
||||
dataDirectory: path.resolve(dataDirs.TRILIUM_DATA_DIR)
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
import { BackupDatabaseNowResponse, DatabaseCheckIntegrityResponse } from "@triliumnext/commons";
|
||||
import { becca_loader, ValidationError } from "@triliumnext/core";
|
||||
import { becca_loader, getBackup, ValidationError } from "@triliumnext/core";
|
||||
import type { Request, Response } from "express";
|
||||
import fs, { readFileSync } from "fs";
|
||||
import path from "path";
|
||||
|
||||
import { getIntegrationTestDbPath } from "../../core_assets.js";
|
||||
import anonymizationService from "../../services/anonymization.js";
|
||||
import backupService from "../../services/backup.js";
|
||||
import consistencyChecksService from "../../services/consistency_checks.js";
|
||||
import dataDir from "../../services/data_dir.js";
|
||||
import log from "../../services/log.js";
|
||||
@@ -14,12 +13,12 @@ import sql from "../../services/sql.js";
|
||||
import sql_init from "../../services/sql_init.js";
|
||||
|
||||
function getExistingBackups() {
|
||||
return backupService.getExistingBackups();
|
||||
return getBackup().getExistingBackups();
|
||||
}
|
||||
|
||||
async function backupDatabase() {
|
||||
return {
|
||||
backupFile: await backupService.backupNow("now")
|
||||
backupFile: await getBackup().backupNow("now")
|
||||
} satisfies BackupDatabaseNowResponse;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,93 +0,0 @@
|
||||
import type { DatabaseBackup, OptionNames } from "@triliumnext/commons";
|
||||
import { sync_mutex as syncMutexService } from "@triliumnext/core";
|
||||
import fs from "fs";
|
||||
import path from "path";
|
||||
|
||||
import cls from "./cls.js";
|
||||
import dataDir from "./data_dir.js";
|
||||
import dateUtils from "./date_utils.js";
|
||||
import log from "./log.js";
|
||||
import optionService from "./options.js";
|
||||
import sql from "./sql.js";
|
||||
|
||||
type BackupType = "daily" | "weekly" | "monthly";
|
||||
|
||||
function getExistingBackups(): DatabaseBackup[] {
|
||||
if (!fs.existsSync(dataDir.BACKUP_DIR)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return fs
|
||||
.readdirSync(dataDir.BACKUP_DIR)
|
||||
.filter((fileName) => fileName.includes("backup"))
|
||||
.map((fileName) => {
|
||||
const filePath = path.resolve(dataDir.BACKUP_DIR, fileName);
|
||||
const stat = fs.statSync(filePath);
|
||||
|
||||
return { fileName, filePath, mtime: stat.mtime };
|
||||
});
|
||||
}
|
||||
|
||||
function regularBackup() {
|
||||
cls.init(() => {
|
||||
periodBackup("lastDailyBackupDate", "daily", 24 * 3600);
|
||||
|
||||
periodBackup("lastWeeklyBackupDate", "weekly", 7 * 24 * 3600);
|
||||
|
||||
periodBackup("lastMonthlyBackupDate", "monthly", 30 * 24 * 3600);
|
||||
});
|
||||
}
|
||||
|
||||
function isBackupEnabled(backupType: BackupType) {
|
||||
let optionName: OptionNames;
|
||||
switch (backupType) {
|
||||
case "daily":
|
||||
optionName = "dailyBackupEnabled";
|
||||
break;
|
||||
case "weekly":
|
||||
optionName = "weeklyBackupEnabled";
|
||||
break;
|
||||
case "monthly":
|
||||
optionName = "monthlyBackupEnabled";
|
||||
break;
|
||||
}
|
||||
|
||||
return optionService.getOptionBool(optionName);
|
||||
}
|
||||
|
||||
function periodBackup(optionName: "lastDailyBackupDate" | "lastWeeklyBackupDate" | "lastMonthlyBackupDate", backupType: BackupType, periodInSeconds: number) {
|
||||
if (!isBackupEnabled(backupType)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const now = new Date();
|
||||
const lastBackupDate = dateUtils.parseDateTime(optionService.getOption(optionName));
|
||||
|
||||
if (now.getTime() - lastBackupDate.getTime() > periodInSeconds * 1000) {
|
||||
backupNow(backupType);
|
||||
|
||||
optionService.setOption(optionName, dateUtils.utcNowDateTime());
|
||||
}
|
||||
}
|
||||
|
||||
async function backupNow(name: string) {
|
||||
// we don't want to back up DB in the middle of sync with potentially inconsistent DB state
|
||||
return await syncMutexService.doExclusively(async () => {
|
||||
const backupFile = path.resolve(`${dataDir.BACKUP_DIR}/backup-${name}.db`);
|
||||
|
||||
if (!fs.existsSync(dataDir.BACKUP_DIR)) {
|
||||
fs.mkdirSync(dataDir.BACKUP_DIR, 0o700);
|
||||
}
|
||||
|
||||
log.info("Creating backup...");
|
||||
await sql.copyDatabase(backupFile);
|
||||
log.info(`Created backup at ${backupFile}`);
|
||||
|
||||
return backupFile;
|
||||
});
|
||||
}
|
||||
export default {
|
||||
getExistingBackups,
|
||||
backupNow,
|
||||
regularBackup
|
||||
};
|
||||
@@ -1,6 +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 { initSql } from "./services/sql/index";
|
||||
import { SqlService, SqlServiceParams } from "./services/sql/sql";
|
||||
import { initMessaging, MessagingProvider } from "./services/messaging/index";
|
||||
@@ -15,6 +16,7 @@ import { type InAppHelpProvider, initInAppHelp } from "./services/in_app_help";
|
||||
|
||||
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 type * from "./services/sql/types";
|
||||
export * from "./services/sql/index";
|
||||
export { default as sql_init } from "./services/sql_init";
|
||||
@@ -122,7 +124,7 @@ export { default as scriptService } from "./services/script";
|
||||
export { default as BackendScriptApi, type Api as BackendScriptApiInterface } from "./services/backend_script_api";
|
||||
export * as scheduler from "./services/scheduler";
|
||||
|
||||
export async function initializeCore({ dbConfig, executionContext, crypto, zip, zipExportProviderFactory, translations, messaging, request, schema, extraAppInfo, platform, getDemoArchive, inAppHelp, log }: {
|
||||
export async function initializeCore({ dbConfig, executionContext, crypto, zip, zipExportProviderFactory, translations, messaging, request, schema, extraAppInfo, platform, getDemoArchive, inAppHelp, log, backup }: {
|
||||
dbConfig: SqlServiceParams,
|
||||
executionContext: ExecutionContext,
|
||||
crypto: CryptoProvider,
|
||||
@@ -140,9 +142,11 @@ export async function initializeCore({ dbConfig, executionContext, crypto, zip,
|
||||
dataDirectory: string;
|
||||
};
|
||||
log?: LogService;
|
||||
backup?: BackupService;
|
||||
}) {
|
||||
initPlatform(platform);
|
||||
initLog(log);
|
||||
initBackup(backup);
|
||||
await initTranslations(translations);
|
||||
initCrypto(crypto);
|
||||
initZipProvider(zip);
|
||||
|
||||
@@ -20,7 +20,7 @@ import type BRevision from "../becca/entities/brevision.js";
|
||||
import appInfo from "./app_info.js";
|
||||
import attributeService from "./attributes.js";
|
||||
import type { ApiParams } from "./backend_script_api_interface.js";
|
||||
import backupService from "./backup.js";
|
||||
import { getBackup } from "./backup.js";
|
||||
import cloningService from "./cloning.js";
|
||||
import config from "./config.js";
|
||||
import dateNoteService from "./date_notes.js";
|
||||
@@ -717,7 +717,7 @@ function BackendScriptApi(this: Api, currentNote: BNote, apiParams: ApiParams) {
|
||||
};
|
||||
|
||||
this.runOutsideOfSync = syncMutex.doExclusively;
|
||||
this.backupNow = backupService.backupNow;
|
||||
this.backupNow = (name: string) => getBackup().backupNow(name);
|
||||
this.duplicateSubtree = noteService.duplicateSubtree;
|
||||
|
||||
this.__private = {
|
||||
|
||||
@@ -1,6 +1,49 @@
|
||||
export default {
|
||||
async backupNow(name: string) {
|
||||
console.warn("Backup not yet available.");
|
||||
return "backup-" + name + "-" + new Date().toISOString() + ".zip";
|
||||
import type { DatabaseBackup } from "@triliumnext/commons";
|
||||
|
||||
/**
|
||||
* Base backup service class.
|
||||
* Provides default (no-op) implementations.
|
||||
* Platform-specific implementations extend this class.
|
||||
*/
|
||||
export default class BackupService {
|
||||
/**
|
||||
* Create a backup with the given name.
|
||||
* Returns the backup file path/name.
|
||||
*/
|
||||
async backupNow(name: string): Promise<string> {
|
||||
console.warn("Backup not available - no backup provider configured.");
|
||||
return `backup-${name}-${new Date().toISOString()}.db`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Perform regular scheduled backups (daily, weekly, monthly).
|
||||
* Called periodically by the scheduler.
|
||||
*/
|
||||
regularBackup(): void {
|
||||
// No-op in base implementation
|
||||
}
|
||||
|
||||
/**
|
||||
* Get list of existing backups.
|
||||
* Returns empty array if not supported.
|
||||
*/
|
||||
getExistingBackups(): DatabaseBackup[] {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
let backupService: BackupService = new BackupService();
|
||||
|
||||
/**
|
||||
* Get the current backup service instance.
|
||||
*/
|
||||
export function getBackup(): BackupService {
|
||||
return backupService;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize the backup service with a platform-specific provider.
|
||||
*/
|
||||
export function initBackup(provider?: BackupService): void {
|
||||
backupService = provider ?? new BackupService();
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import backupService from "./backup.js";
|
||||
import { getBackup } from "./backup.js";
|
||||
import { getSql } from "./sql/index.js";
|
||||
import { getLog } from "./log.js";
|
||||
import { getPlatform } from "./platform.js";
|
||||
@@ -26,7 +26,7 @@ async function migrate() {
|
||||
|
||||
// backup before attempting migration
|
||||
if (!getPlatform().getEnv("TRILIUM_INTEGRATION_TEST")) {
|
||||
await backupService.backupNow(
|
||||
await getBackup().backupNow(
|
||||
// creating a special backup for version 0.60.4, the changes in 0.61 are major.
|
||||
currentDbVersion === 214 ? `before-migration-v060` : "before-migration"
|
||||
);
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { deferred, OptionRow } from "@triliumnext/commons";
|
||||
import { getSql } from "./sql";
|
||||
import { getLog } from "./log";
|
||||
import { getBackup } from "./backup";
|
||||
import optionService from "./options";
|
||||
import eventService from "./events";
|
||||
import { getContext } from "./context";
|
||||
@@ -105,22 +106,16 @@ function initializeDb() {
|
||||
getContext().init(initDbConnection);
|
||||
|
||||
dbReady.then(() => {
|
||||
// TODO: Re-enable backup.
|
||||
// if (config.General && config.General.noBackup === true) {
|
||||
// log.info("Disabling scheduled backups.");
|
||||
// Run regular backups every 4 hours
|
||||
setInterval(() => getBackup().regularBackup(), 4 * 60 * 60 * 1000);
|
||||
|
||||
// return;
|
||||
// }
|
||||
// Kickoff first backup soon after start up
|
||||
setTimeout(() => getBackup().regularBackup(), 5 * 60 * 1000);
|
||||
|
||||
// setInterval(() => backup.regularBackup(), 4 * 60 * 60 * 1000);
|
||||
// Optimize is usually inexpensive no-op, so running it semi-frequently is not a big deal
|
||||
setTimeout(() => optimize(), 60 * 60 * 1000);
|
||||
|
||||
// // kickoff first backup soon after start up
|
||||
// setTimeout(() => backup.regularBackup(), 5 * 60 * 1000);
|
||||
|
||||
// // optimize is usually inexpensive no-op, so running it semi-frequently is not a big deal
|
||||
// setTimeout(() => optimize(), 60 * 60 * 1000);
|
||||
|
||||
// setInterval(() => optimize(), 10 * 60 * 60 * 1000);
|
||||
setInterval(() => optimize(), 10 * 60 * 60 * 1000);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user