Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor(data_dir): simplify logic and make code robust and testable #880

Merged
merged 12 commits into from
Jan 14, 2025
Merged
138 changes: 138 additions & 0 deletions spec-es6/data_dir.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
import { describe, it, execute, expect } from "./mini_test.ts";

import { getPlatformAppDataDir, getDataDirs} from "../src/services/data_dir.ts"



describe("data_dir.ts unit tests", () => {

describe("#getPlatformAppDataDir()", () => {

type TestCaseGetPlatformAppDataDir = [
description: string,
fnValue: Parameters<typeof getPlatformAppDataDir>,
expectedValueFn: (val: ReturnType<typeof getPlatformAppDataDir>) => boolean
]
const testCases: TestCaseGetPlatformAppDataDir[] = [

[
"w/ unsupported OS it should return 'null'",
["aix", undefined],
(val) => val === null
],

[
"w/ win32 and no APPDATA set it should return 'null'",
["win32", undefined],
(val) => val === null
],

[
"w/ win32 and set APPDATA it should return set 'APPDATA'",
["win32", "AppData"],
(val) => val === "AppData"
],

[
"w/ linux it should return '/.local/share'",
["linux", undefined],
(val) => val !== null && val.endsWith("/.local/share")
],

[
"w/ linux and wrongly set APPDATA it should ignore APPDATA and return /.local/share",
["linux", "FakeAppData"],
(val) => val !== null && val.endsWith("/.local/share")
],

[
"w/ darwin it should return /Library/Application Support",
["darwin", undefined],
(val) => val !== null && val.endsWith("/Library/Application Support")
],
];

testCases.forEach(testCase => {
const [testDescription, value, isExpected] = testCase;
return it(testDescription, () => {
const actual = getPlatformAppDataDir(...value);
const result = isExpected(actual);
expect(result).toBeTruthy()

})
})


})

describe("#getTriliumDataDir", () => {
// TODO
})

describe("#getDataDirs()", () => {

const envKeys: Omit<keyof ReturnType<typeof getDataDirs>, "TRILIUM_DATA_DIR">[] = [
"DOCUMENT_PATH",
"BACKUP_DIR",
"LOG_DIR",
"ANONYMIZED_DB_DIR",
"CONFIG_INI_PATH",
];

const setMockedEnv = (prefix: string | null) => {
envKeys.forEach(key => {
if (prefix) {
process.env[`TRILIUM_${key}`] = `${prefix}_${key}`
} else {
delete process.env[`TRILIUM_${key}`]
}
})
};

it("w/ process.env values present, it should return an object using values from process.env", () => {

// set mocked values
const mockValuePrefix = "MOCK";
setMockedEnv(mockValuePrefix);

// get result
const result = getDataDirs(`${mockValuePrefix}_TRILIUM_DATA_DIR`);

for (const key in result) {
expect(result[key]).toEqual(`${mockValuePrefix}_${key}`)
}
})

it("w/ NO process.env values present, it should return an object using supplied TRILIUM_DATA_DIR as base", () => {

// make sure values are undefined
setMockedEnv(null);

const mockDataDir = "/home/test/MOCK_TRILIUM_DATA_DIR"
const result = getDataDirs(mockDataDir);

for (const key in result) {
expect(result[key].startsWith(mockDataDir)).toBeTruthy()
}
})

it("should ignore attempts to change a property on the returned object", () => {

// make sure values are undefined
setMockedEnv(null);

const mockDataDir = "/home/test/MOCK_TRILIUM_DATA_DIR"
const result = getDataDirs(mockDataDir);

//@ts-expect-error - attempt to change value of readonly property
result.BACKUP_DIR = "attempt to change";

for (const key in result) {
expect(result[key].startsWith(mockDataDir)).toBeTruthy()
}
})
})

});

execute()
110 changes: 59 additions & 51 deletions src/services/data_dir.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,75 +2,83 @@

/*
* This file resolves trilium data path in this order of priority:
* - if TRILIUM_DATA_DIR environment variable exists, then its value is used as the path
* - if "trilium-data" dir exists directly in the home dir, then it is used
* - based on OS convention, if the "app data directory" exists, we'll use or create "trilium-data" directory there
* - as a fallback if the previous step fails, we'll use home dir
* - case A) if TRILIUM_DATA_DIR environment variable exists, then its value is used as the path
* - case B) if "trilium-data" dir exists directly in the home dir, then it is used
* - case C) based on OS convention, if the "app data directory" exists, we'll use or create "trilium-data" directory there
* - case D) as a fallback if the previous step fails, we'll use home dir
*/

import os from "os";
import fs from "fs";
import path from "path";
import { join as pathJoin } from "path";

function getAppDataDir() {
let appDataDir = os.homedir(); // fallback if OS is not recognized
const DIR_NAME = "trilium-data";
const FOLDER_PERMISSIONS = 0o700;

if (os.platform() === "win32" && process.env.APPDATA) {
appDataDir = process.env.APPDATA;
} else if (os.platform() === "linux") {
appDataDir = `${os.homedir()}/.local/share`;
} else if (os.platform() === "darwin") {
appDataDir = `${os.homedir()}/Library/Application Support`;
export function getTriliumDataDir(dataDirName: string) {
// case A
if (process.env.TRILIUM_DATA_DIR) {
createDirIfNotExisting(process.env.TRILIUM_DATA_DIR);
return process.env.TRILIUM_DATA_DIR;
}

if (!fs.existsSync(appDataDir)) {
// expected app data path doesn't exist, let's use fallback
appDataDir = os.homedir();
// case B
const homePath = pathJoin(os.homedir(), dataDirName);
if (fs.existsSync(homePath)) {
return homePath;
}

return appDataDir;
// case C
const platformAppDataDir = getPlatformAppDataDir(os.platform(), process.env.APPDATA);
if (platformAppDataDir && fs.existsSync(platformAppDataDir)) {
const appDataDirPath = pathJoin(platformAppDataDir, dataDirName);
createDirIfNotExisting(appDataDirPath);
return appDataDirPath;
}

// case D
createDirIfNotExisting(homePath);
return homePath;
}

const DIR_NAME = "trilium-data";
export function getDataDirs(TRILIUM_DATA_DIR: string) {
const dataDirs = {
TRILIUM_DATA_DIR: TRILIUM_DATA_DIR,
DOCUMENT_PATH: process.env.TRILIUM_DOCUMENT_PATH || pathJoin(TRILIUM_DATA_DIR, "document.db"),
BACKUP_DIR: process.env.TRILIUM_BACKUP_DIR || pathJoin(TRILIUM_DATA_DIR, "backup"),
LOG_DIR: process.env.TRILIUM_LOG_DIR || pathJoin(TRILIUM_DATA_DIR, "log"),
ANONYMIZED_DB_DIR: process.env.TRILIUM_ANONYMIZED_DB_DIR || pathJoin(TRILIUM_DATA_DIR, "anonymized-db"),
CONFIG_INI_PATH: process.env.TRILIUM_CONFIG_INI_PATH || pathJoin(TRILIUM_DATA_DIR, "config.ini")
} as const;

Object.freeze(dataDirs);
return dataDirs;
}

function getTriliumDataDir() {
if (process.env.TRILIUM_DATA_DIR) {
if (!fs.existsSync(process.env.TRILIUM_DATA_DIR)) {
fs.mkdirSync(process.env.TRILIUM_DATA_DIR, 0o700);
}
export function getPlatformAppDataDir(platform: ReturnType<typeof os.platform>, ENV_APPDATA_DIR: string | undefined = process.env.APPDATA) {
switch (true) {
case platform === "win32" && !!ENV_APPDATA_DIR:
return ENV_APPDATA_DIR;

return process.env.TRILIUM_DATA_DIR;
}
case platform === "linux":
return `${os.homedir()}/.local/share`;

const homePath = os.homedir() + path.sep + DIR_NAME;
case platform === "darwin":
return `${os.homedir()}/Library/Application Support`;

if (fs.existsSync(homePath)) {
return homePath;
default:
// if OS is not recognized
return null;
}
}

const appDataPath = getAppDataDir() + path.sep + DIR_NAME;

if (!fs.existsSync(appDataPath)) {
fs.mkdirSync(appDataPath, 0o700);
function createDirIfNotExisting(path: fs.PathLike, permissionMode: fs.Mode = FOLDER_PERMISSIONS) {
if (!fs.existsSync(path)) {
fs.mkdirSync(path, permissionMode);
}

return appDataPath;
}

const TRILIUM_DATA_DIR = getTriliumDataDir();
const DIR_SEP = TRILIUM_DATA_DIR + path.sep;

const DOCUMENT_PATH = process.env.TRILIUM_DOCUMENT_PATH || `${DIR_SEP}document.db`;
const BACKUP_DIR = process.env.TRILIUM_BACKUP_DIR || `${DIR_SEP}backup`;
const LOG_DIR = process.env.TRILIUM_LOG_DIR || `${DIR_SEP}log`;
const ANONYMIZED_DB_DIR = process.env.TRILIUM_ANONYMIZED_DB_DIR || `${DIR_SEP}anonymized-db`;
const CONFIG_INI_PATH = process.env.TRILIUM_CONFIG_INI_PATH || `${DIR_SEP}config.ini`;

export default {
TRILIUM_DATA_DIR,
DOCUMENT_PATH,
BACKUP_DIR,
LOG_DIR,
ANONYMIZED_DB_DIR,
CONFIG_INI_PATH
};
const TRILIUM_DATA_DIR = getTriliumDataDir(DIR_NAME);
const dataDirs = getDataDirs(TRILIUM_DATA_DIR);

export default dataDirs;
Loading