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

Draft
wants to merge 11 commits into
base: develop
Choose a base branch
from
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()
126 changes: 71 additions & 55 deletions src/services/data_dir.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,77 +2,93 @@

/*
* 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;

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;
}

// case B
const homePath = pathJoin(os.homedir(), dataDirName);
if (fs.existsSync(homePath)) {
return homePath;
}

// 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;
}

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 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;
}

if (!fs.existsSync(appDataDir)) {
// expected app data path doesn't exist, let's use fallback
appDataDir = os.homedir();
}
export function getPlatformAppDataDir(platform: ReturnType<typeof os.platform>, ENV_APPDATA_DIR: string | undefined = process.env.APPDATA) {

return appDataDir;
}
switch(true) {
case platform === 'win32' && !!ENV_APPDATA_DIR:
return ENV_APPDATA_DIR;

const DIR_NAME = 'trilium-data';
case platform === 'linux':
return `${os.homedir()}/.local/share`;

function getTriliumDataDir() {
if (process.env.TRILIUM_DATA_DIR) {
if (!fs.existsSync(process.env.TRILIUM_DATA_DIR)) {
fs.mkdirSync(process.env.TRILIUM_DATA_DIR, 0o700);
}
case platform === 'darwin':
return `${os.homedir()}/Library/Application Support`;

return process.env.TRILIUM_DATA_DIR;
default:
// if OS is not recognized
return null;
}

const homePath = os.homedir() + path.sep + DIR_NAME;
}

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

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

if (!fs.existsSync(appDataPath)) {
fs.mkdirSync(appDataPath, 0o700);
}

return appDataPath;
}
const TRILIUM_DATA_DIR = getTriliumDataDir(DIR_NAME);
const dataDirs = getDataDirs(TRILIUM_DATA_DIR);

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
};
export default dataDirs;