diff --git a/apps/server/src/services/config.ts b/apps/server/src/services/config.ts index d88236b52..84e859a68 100644 --- a/apps/server/src/services/config.ts +++ b/apps/server/src/services/config.ts @@ -3,7 +3,7 @@ import fs from "fs"; import dataDir from "./data_dir.js"; import path from "path"; import resourceDir from "./resource_dir.js"; -import { envToBoolean } from "./utils.js"; +import { envToBoolean, stringToInt } from "./utils.js"; const configSampleFilePath = path.resolve(resourceDir.RESOURCE_DIR, "config-sample.ini"); @@ -50,8 +50,16 @@ export interface TriliumConfig { oauthIssuerName: string; oauthIssuerIcon: string; }; + Logging: { + /** + * The number of days to keep the log files around. When rotating the logs, log files created by Trilium older than the specified amount of time will be deleted. + */ + retentionDays: number; + } } +export const LOGGING_DEFAULT_RETENTION_DAYS = 90; + //prettier-ignore const config: TriliumConfig = { @@ -136,6 +144,13 @@ const config: TriliumConfig = { oauthIssuerIcon: process.env.TRILIUM_OAUTH_ISSUER_ICON || iniConfig?.MultiFactorAuthentication?.oauthIssuerIcon || "" + }, + + Logging: { + retentionDays: + stringToInt(process.env.TRILIUM_LOGGING_RETENTION_DAYS) ?? + stringToInt(iniConfig?.Logging?.retentionDays) ?? + LOGGING_DEFAULT_RETENTION_DAYS } }; diff --git a/apps/server/src/services/log.ts b/apps/server/src/services/log.ts index b7206bc8f..bcea56500 100644 --- a/apps/server/src/services/log.ts +++ b/apps/server/src/services/log.ts @@ -6,6 +6,7 @@ import path from "path"; import { EOL } from "os"; import dataDir from "./data_dir.js"; import cls from "./cls.js"; +import config, { LOGGING_DEFAULT_RETENTION_DAYS } from "./config.js"; if (!fs.existsSync(dataDir.LOG_DIR)) { fs.mkdirSync(dataDir.LOG_DIR, 0o700); @@ -18,7 +19,6 @@ const MINUTE = 60 * SECOND; const HOUR = 60 * MINUTE; const DAY = 24 * HOUR; -const DEFAULT_RETENTION_DAYS = 90; const MINIMUM_FILES_TO_KEEP = 7; let todaysMidnight!: Date; @@ -34,17 +34,10 @@ function getTodaysMidnight() { async function cleanupOldLogFiles() { try { // Get retention days from environment or options - const envRetention = process.env.TRILIUM_LOG_RETENTION_DAYS; - let retentionDays = DEFAULT_RETENTION_DAYS; - - if (envRetention) { - const parsed = parseInt(envRetention, 10); - if (!isNaN(parsed) && parsed > 0 && parsed <= 3650) { - retentionDays = parsed; - } - } else { - const optionService = (await import("./options.js")).default; - retentionDays = optionService.getOptionInt("logRetentionDays", DEFAULT_RETENTION_DAYS); + let retentionDays = LOGGING_DEFAULT_RETENTION_DAYS; + const customRetentionDays = config.Logging.retentionDays; + if (customRetentionDays > 0) { + retentionDays = customRetentionDays; } const cutoffDate = new Date(); diff --git a/apps/server/src/services/utils.ts b/apps/server/src/services/utils.ts index 5d5559d25..8f9893dff 100644 --- a/apps/server/src/services/utils.ts +++ b/apps/server/src/services/utils.ts @@ -281,6 +281,25 @@ export function envToBoolean(val: string | undefined) { return undefined; } +/** + * Parses a string value to an integer. If the resulting number is NaN or undefined, the result is also undefined. + * + * @param val the value to parse. + * @returns the parsed value. + */ +export function stringToInt(val: string | undefined) { + if (!val) { + return undefined; + } + + const parsed = parseInt(val, 10); + if (Number.isNaN(parsed)) { + return undefined; + } + + return parsed; +} + /** * Returns the directory for resources. On Electron builds this corresponds to the `resources` subdirectory inside the distributable package. * On development builds, this simply refers to the src directory of the application. @@ -378,7 +397,7 @@ export function safeExtractMessageAndStackFromError(err: unknown): [errMessage: /** * Normalizes URL by removing trailing slashes and fixing double slashes. * Preserves the protocol (http://, https://) but removes trailing slashes from the rest. - * + * * @param url The URL to normalize * @returns The normalized URL without trailing slashes */ @@ -389,26 +408,26 @@ export function normalizeUrl(url: string | null | undefined): string | null | un // Trim whitespace url = url.trim(); - + if (!url) { return url; } // Fix double slashes (except in protocol) first url = url.replace(/([^:]\/)\/+/g, '$1'); - + // Remove trailing slash, but preserve protocol if (url.endsWith('/') && !url.match(/^https?:\/\/$/)) { url = url.slice(0, -1); } - + return url; } /** * Normalizes a path pattern for custom request handlers. * Ensures both trailing slash and non-trailing slash versions are handled. - * + * * @param pattern The original pattern from customRequestHandler attribute * @returns An array of patterns to match both with and without trailing slash */ @@ -418,7 +437,7 @@ export function normalizeCustomHandlerPattern(pattern: string | null | undefined } pattern = pattern.trim(); - + if (!pattern) { return [pattern]; } @@ -431,7 +450,7 @@ export function normalizeCustomHandlerPattern(pattern: string | null | undefined // If pattern ends with $, handle it specially if (pattern.endsWith('$')) { const basePattern = pattern.slice(0, -1); - + // If already ends with slash, create both versions if (basePattern.endsWith('/')) { const withoutSlash = basePattern.slice(0, -1) + '$';