From 26121ace103109d8a651e2a49cb7336ecfc16992 Mon Sep 17 00:00:00 2001 From: FoxxMD Date: Tue, 14 Jul 2026 19:04:44 +0000 Subject: [PATCH] feat: Separate config and data directories and use OS-standard paths, if present * Break configDir into config and data directories so config and database/cache can be stored in different directories * Use `env-paths` to fallback to OS-standard paths for config and data if CONFIG_DIR and DATA_DIR envs are not present * Additionally fallback to process CWD/config if none of the above are present for some reason #616 --- Dockerfile | 1 + drizzle.config.ts | 4 ++-- src/backend/common/Cache.ts | 14 ++++++------- src/backend/common/database/Database.ts | 4 ++-- src/backend/common/index.ts | 28 +++++++++++++++++++++---- src/backend/common/logging.ts | 15 ++++++------- src/backend/index.ts | 10 ++++----- src/backend/ioc.ts | 6 ++---- src/backend/tests/tealfm/tealfm.test.ts | 4 ++-- 9 files changed, 51 insertions(+), 35 deletions(-) diff --git a/Dockerfile b/Dockerfile index 3d04bb40..d429a237 100644 --- a/Dockerfile +++ b/Dockerfile @@ -56,6 +56,7 @@ RUN npm install -g concurrently ARG data_dir=/config VOLUME $data_dir ENV CONFIG_DIR=$data_dir +ENV DATA_DIR=$data_dir COPY docker/root / diff --git a/drizzle.config.ts b/drizzle.config.ts index 4ca638ee..1da91e4f 100644 --- a/drizzle.config.ts +++ b/drizzle.config.ts @@ -1,6 +1,6 @@ import 'dotenv/config'; import { defineConfig } from 'drizzle-kit'; -import { configDir } from './src/backend/common/index.js'; +import { getDataDir } from './src/backend/common/index.js'; import * as path from 'path'; import { projectRootDir } from './src/core/Atomic.ts'; @@ -9,6 +9,6 @@ export default defineConfig({ out: path.resolve(projectRootDir, 'src/backend/common/database/drizzle/migrations'), dialect: 'sqlite', dbCredentials: { - url: path.resolve(configDir, 'ms.db'), + url: path.resolve(getDataDir(), 'ms.db'), }, }); \ No newline at end of file diff --git a/src/backend/common/Cache.ts b/src/backend/common/Cache.ts index 5077c36a..f696573a 100644 --- a/src/backend/common/Cache.ts +++ b/src/backend/common/Cache.ts @@ -10,7 +10,7 @@ import timezone from 'dayjs/plugin/timezone.js'; import utc from 'dayjs/plugin/utc.js'; import clone from 'clone'; import { childLogger, type Logger } from '@foxxmd/logging'; -import { projectDir } from './index.ts'; +import { getConfigDir } from './index.ts'; import path from 'path'; import { cacheFunctions } from "@foxxmd/regex-buddy-core"; import { fileOrDirectoryIsWriteable } from '../utils/FSUtils.ts'; @@ -18,7 +18,6 @@ import { asCacheConfig, type CacheAuthProvider, type CacheConfig, type CacheConf import { Typeson } from 'typeson'; import { builtin } from 'typeson-registry'; import { loggerNoop } from './MaybeLogger.ts'; -const configDir = process.env.CONFIG_DIR || path.resolve(projectDir, `./config`); import type { Gauge } from 'prom-client'; import prom from 'prom-client'; import { nonEmptyStringOrDefault } from '../../core/StringUtils.ts'; @@ -277,11 +276,12 @@ export class MSCache { throw e; } } + const confDir = getConfigDir(); if (config.provider === 'file') { - logger.debug(`Building file cache from ${path.join(config.connection ?? configDir, `${namespace}.cache`)}`); + logger.debug(`Building file cache from ${path.join(config.connection ?? confDir, `${namespace}.cache`)}`); try { - const [keyvFile] = await initFileCache({ ...config, cacheDir: config.connection ?? configDir, cacheId: `${namespace}.cache` }, {ttl: config.ttl}, logger); + const [keyvFile] = await initFileCache({ ...config, cacheDir: config.connection ?? confDir, cacheId: `${namespace}.cache` }, {ttl: config.ttl}, logger); return keyvFile; } catch (e) { throw e; @@ -538,7 +538,7 @@ export const parseUserConfig = (config: CacheConfigUser = {}, parentLogger: Logg // } = {}, scrobble: { provider: sProvider = (process.env.CACHE_SCROBBLE as (CacheScrobbleProvider | undefined) ?? 'file'), - connection = (process.env.CACHE_SCROBBLE_CONN ?? configDir), + connection = (process.env.CACHE_SCROBBLE_CONN ?? getConfigDir()), ...restScrobble } = {}, auth: { @@ -563,7 +563,7 @@ export const parseUserConfig = (config: CacheConfigUser = {}, parentLogger: Logg if(authProvider === 'valkey') { if(valkey === undefined) { logger.warn(`Auth Provider set to 'valkey' but not valkey connection string was not provided, falling back to file.`); - authConn = configDir; + authConn = getConfigDir(); authProvider = 'file'; } else { authConn = valkey; @@ -572,7 +572,7 @@ export const parseUserConfig = (config: CacheConfigUser = {}, parentLogger: Logg if(authProvider !== 'file') { logger.warn(`Unsupported provider given for auth: ${authProvider}`); } - authConn = configDir; + authConn = getConfigDir(); authProvider = 'file'; } diff --git a/src/backend/common/database/Database.ts b/src/backend/common/database/Database.ts index 5f3fd74b..22bb19da 100644 --- a/src/backend/common/database/Database.ts +++ b/src/backend/common/database/Database.ts @@ -1,4 +1,4 @@ -import { configDir } from '../index.ts'; +import { getDataDir } from '../index.ts'; import * as path from 'path'; import { childLogger, type Logger } from '@foxxmd/logging'; import { loggerNoop } from '../MaybeLogger.ts'; @@ -19,7 +19,7 @@ export const getDbPath = (name: string = 'ms', workingDirectory?: string): strin if (isMemoryDb(name)) { return MEMORY_DB_NAME; } - return path.resolve(workingDirectory ?? configDir, `${name}.db`); + return path.resolve(workingDirectory ?? getDataDir(), `${name}.db`); } export const getDbBackupPath = (dbPath: string, suffix?: string): string => { diff --git a/src/backend/common/index.ts b/src/backend/common/index.ts index 3124a3c0..c8f1f5ce 100644 --- a/src/backend/common/index.ts +++ b/src/backend/common/index.ts @@ -1,8 +1,28 @@ import * as path from 'path'; +import envPaths from 'env-paths'; -//const __filename = fileURLToPath(import.meta.url); -//const __dirname = path.dirname(__filename); +const osPaths = envPaths('multi-scrobbler', {suffix: ''}); + +export const getConfigDir = (): string => { + let configDirVal: string = process.env.CONFIG_DIR ?? osPaths.config; + // this shouldn't happen... + // but if it does we need to have some known path fallback so things don't explode + if(configDirVal === undefined) { + configDirVal = getPathFromCWD('./config'); // backwards compatibility + } + // resolve from relative directory, if one was used + return path.resolve(configDirVal); +} + +export const getDataDir = (): string => { + let dataDirVal: string = process.env.DATA_DIR ?? osPaths.data; + // this shouldn't happen... + // but if it does we need to have some known path fallback so things don't explode + if(dataDirVal === undefined) { + dataDirVal = getPathFromCWD('./config'); // defaulting to same directory for backwards compatibility + } + // resolve from relative directory, if one was used + return path.resolve(dataDirVal); +} -export const projectDir = process.cwd(); //path.resolve(__dirname, '../../../'); -export const configDir: string = process.env.CONFIG_DIR || path.resolve(projectDir, `./config`); export const getPathFromCWD = (...relativePaths: string[]) => path.resolve(process.cwd(), ...relativePaths); \ No newline at end of file diff --git a/src/backend/common/logging.ts b/src/backend/common/logging.ts index 0773beb8..123e1e3e 100644 --- a/src/backend/common/logging.ts +++ b/src/backend/common/logging.ts @@ -4,13 +4,10 @@ import type { Transform } from "node:stream"; import { PassThrough } from "node:stream"; import path from "path"; import process from "process"; -import { projectDir } from "./index.ts"; +import { getDataDir } from "./index.ts"; import { isDebugMode } from '../utils.ts'; -export let logPath = path.resolve(projectDir, `./logs`); -if (typeof process.env.CONFIG_DIR === 'string') { - logPath = path.resolve(process.env.CONFIG_DIR, './logs'); -} +const logPath = path.resolve(getDataDir(), `./logs`); export const initLogger = (): [Logger, Transform] => { const opts = parseLogOptions({file: false, console: 'trace'}) @@ -27,8 +24,8 @@ export const appLogger = async (config: LogOptions = {}): Promise<[Logger, PassT const { file } = config; const opts = parseLogOptions(isDebugMode() ? {...config, file: typeof file === 'object' ? {...file, level: 'trace'} : 'trace', console: 'trace', level: 'trace'} : config); const logger = await loggerAppRolling(opts, { - logBaseDir: typeof process.env.CONFIG_DIR === 'string' ? process.env.CONFIG_DIR : undefined, - logDefaultPath: './logs/scrobble.log', + logBaseDir: logPath, + logDefaultPath: './scrobble.log', destinations: [ buildDestinationJsonPrettyStream('trace', {destination: stream, object: true, colorize: true}) ] @@ -38,8 +35,8 @@ export const appLogger = async (config: LogOptions = {}): Promise<[Logger, PassT export const componentFileLogger = async (type: string, name: string, fileConfig: true | LogLevel | FileLogOptions, config: LogOptions = {}): Promise => { const opts = parseLogOptions(config, { - logBaseDir: typeof process.env.CONFIG_DIR === 'string' ? process.env.CONFIG_DIR : undefined, - logDefaultPath: './logs/scrobble.log' + logBaseDir: logPath, + logDefaultPath: './scrobble.log' }); const base = path.dirname(typeof opts.file.path === 'function' ? opts.file.path() : opts.file.path); diff --git a/src/backend/index.ts b/src/backend/index.ts index e8167a91..dfe66839 100644 --- a/src/backend/index.ts +++ b/src/backend/index.ts @@ -8,9 +8,8 @@ import isToday from 'dayjs/plugin/isToday.js'; import timezone from 'dayjs/plugin/timezone.js'; import week from 'dayjs/plugin/weekOfYear.js'; import utc from 'dayjs/plugin/utc.js'; -import * as path from "path"; import { SimpleIntervalJob, ToadScheduler } from "toad-scheduler"; -import { projectDir } from "./common/index.ts"; +import { getConfigDir, getDataDir } from "./common/index.ts"; import type {AIOConfig} from "./common/infrastructure/config/aioConfig.ts"; import { appLogger, initLogger as getInitLogger } from "./common/logging.ts"; import { getRoot } from "./ioc.ts"; @@ -87,10 +86,11 @@ process.on('SIGINT', async () => { }) -const configDir = process.env.CONFIG_DIR || path.resolve(projectDir, `./config`); +const configDir = getConfigDir() try { - initLogger.verbose(`Config Dir ENV: ${process.env.CONFIG_DIR} -> Resolved: ${configDir}`) + initLogger.verbose(`Config Dir ENV : ${process.env.CONFIG_DIR} -> Resolved: ${configDir}`); + initLogger.verbose(`Data Dir ENV : ${process.env.DATA_DIR} -> Resolved: ${getDataDir()}`); // try to read a configuration file let appConfigFail: Error | undefined = undefined; let config = {}; @@ -127,7 +127,7 @@ const configDir = process.env.CONFIG_DIR || path.resolve(projectDir, `./config`) const dbPath = getDbPath('ms'); logger.info(`Using database at ${db}`); - const [migratedDb, isNew] = await getMigratedDb(dbPath, {logger}); + const [migratedDb, _] = await getMigratedDb(dbPath, {logger}); db = migratedDb; const root = getRoot({ diff --git a/src/backend/ioc.ts b/src/backend/ioc.ts index 28a5ce2b..4353643a 100644 --- a/src/backend/ioc.ts +++ b/src/backend/ioc.ts @@ -1,8 +1,7 @@ import { type Logger, loggerDebug, type LogOptions } from "@foxxmd/logging"; import { EventEmitter } from "events"; import { createContainer } from "iti"; -import path from "path"; -import { projectDir } from "./common/index.ts"; +import { getConfigDir } from "./common/index.ts"; import { WildcardEmitter } from "./common/WildcardEmitter.ts"; import { generateBaseURL } from "./utils/NetworkUtils.ts"; @@ -64,7 +63,6 @@ const createRoot = (options: RootOptions = {logger: loggerDebug}) => { db, transformers = [] } = options || {}; - const configDir = process.env.CONFIG_DIR || path.resolve(projectDir, `./config`); let disableWeb = dw; if(disableWeb === undefined) { disableWeb = process.env.DISABLE_WEB === 'true'; @@ -131,7 +129,7 @@ const createRoot = (options: RootOptions = {logger: loggerDebug}) => { return createContainer().add({ version, - configDir: configDir, + configDir: getConfigDir(), isProd: process.env.NODE_ENV !== undefined && (process.env.NODE_ENV === 'production' || process.env.NODE_ENV === 'prod'), // @ts-ignore port: (Number.isInteger(portVal) ? portVal : Number.parseInt(portVal)) as number, diff --git a/src/backend/tests/tealfm/tealfm.test.ts b/src/backend/tests/tealfm/tealfm.test.ts index 1b9a262f..b75a331a 100644 --- a/src/backend/tests/tealfm/tealfm.test.ts +++ b/src/backend/tests/tealfm/tealfm.test.ts @@ -9,7 +9,7 @@ import { artistCreditsToNames } from '../../../core/StringUtils.ts'; import TealScrobbler from '../../scrobblers/TealfmScrobbler.ts'; import { EventEmitter } from "events"; import path from 'node:path'; -import { configDir } from '../../common/index.ts'; +import { getConfigDir } from '../../common/index.ts'; import { loggerDebug } from '@foxxmd/logging'; chai.use(asPromised); @@ -115,6 +115,6 @@ describe('#tealfmCar', function() { ); await tfm.buildDatabase(); - await tfm.parseScrobblesFromCar(path.resolve(configDir, 'tealfm-myteal-1778870858.car'), 100); + await tfm.parseScrobblesFromCar(path.resolve(getConfigDir(), 'tealfm-myteal-1778870858.car'), 100); }); }); \ No newline at end of file -- 2.51.2