Compare commits

...

6 Commits

4 changed files with 102 additions and 41 deletions

View File

@@ -5,7 +5,7 @@ import { Dropdown as BootstrapDropdown } from "bootstrap";
import clsx from "clsx";
import { type ComponentChildren, RefObject } from "preact";
import { createPortal } from "preact/compat";
import { useContext, useEffect, useMemo, useRef, useState } from "preact/hooks";
import { useCallback, useContext, useEffect, useMemo, useRef, useState } from "preact/hooks";
import { CommandNames } from "../../components/app_context";
import NoteContext from "../../components/note_context";
@@ -338,15 +338,19 @@ interface AttributesProps extends StatusBarContext {
function AttributesButton({ note, attributesShown, setAttributesShown }: AttributesProps) {
const [ count, setCount ] = useState(note.attributes.length);
const refreshCount = useCallback((note: FNote) => {
return note.getAttributes().filter(a => !a.isAutoLink).length;
}, []);
// React to note changes.
useEffect(() => {
setCount(note.getAttributes().filter(a => !a.isAutoLink).length);
}, [ note ]);
setCount(refreshCount(note));
}, [ note, refreshCount ]);
// React to changes in count.
useTriliumEvent("entitiesReloaded", (({loadResults}) => {
if (loadResults.getAttributeRows().some(attr => attributes.isAffecting(attr, note))) {
setCount(note.attributes.length);
setCount(refreshCount(note));
}
}));

View File

@@ -10,6 +10,7 @@ describe("data_dir.ts unit tests", async () => {
const mockFn = {
existsSyncMock: vi.fn(),
mkdirSyncMock: vi.fn(),
statSyncMock: vi.fn(),
osHomedirMock: vi.fn(),
osPlatformMock: vi.fn(),
pathJoinMock: vi.fn()
@@ -21,7 +22,8 @@ describe("data_dir.ts unit tests", async () => {
return {
default: {
existsSync: mockFn.existsSyncMock,
mkdirSync: mockFn.mkdirSyncMock
mkdirSync: mockFn.mkdirSyncMock,
statSync: mockFn.statSyncMock
}
};
});
@@ -109,34 +111,36 @@ describe("data_dir.ts unit tests", async () => {
*/
describe("case A", () => {
it("when folder exists it should return the path, without attempting to create the folder", async () => {
it("when folder exists it should return the path, handling EEXIST gracefully", async () => {
const mockTriliumDataPath = "/home/mock/trilium-data-ENV-A1";
process.env.TRILIUM_DATA_DIR = mockTriliumDataPath;
// set fs.existsSync to true, i.e. the folder does exist
mockFn.existsSyncMock.mockImplementation(() => true);
// mkdirSync throws EEXIST when folder already exists (EAFP pattern)
const eexistError = new Error("EEXIST: file already exists") as NodeJS.ErrnoException;
eexistError.code = "EEXIST";
mockFn.mkdirSyncMock.mockImplementation(() => { throw eexistError; });
// statSync confirms it's a directory
mockFn.statSyncMock.mockImplementation(() => ({ isDirectory: () => true }));
const result = getTriliumDataDir("trilium-data");
// createDirIfNotExisting should call existsync 1 time and mkdirSync 0 times -> as it does not need to create the folder
// and return value should be TRILIUM_DATA_DIR value from process.env
expect(mockFn.existsSyncMock).toHaveBeenCalledTimes(1);
expect(mockFn.mkdirSyncMock).toHaveBeenCalledTimes(0);
// createDirIfNotExisting tries mkdirSync first (EAFP), then statSync to verify it's a directory
expect(mockFn.mkdirSyncMock).toHaveBeenCalledTimes(1);
expect(mockFn.statSyncMock).toHaveBeenCalledTimes(1);
expect(result).toEqual(process.env.TRILIUM_DATA_DIR);
});
it("when folder does not exist it should attempt to create the folder and return the path", async () => {
it("when folder does not exist it should create the folder and return the path", async () => {
const mockTriliumDataPath = "/home/mock/trilium-data-ENV-A2";
process.env.TRILIUM_DATA_DIR = mockTriliumDataPath;
// set fs.existsSync mock to return false, i.e. the folder does not exist
mockFn.existsSyncMock.mockImplementation(() => false);
// mkdirSync succeeds when folder doesn't exist
mockFn.mkdirSyncMock.mockImplementation(() => undefined);
const result = getTriliumDataDir("trilium-data");
// createDirIfNotExisting should call existsync 1 time and mkdirSync 1 times -> as it has to create the folder
// and return value should be TRILIUM_DATA_DIR value from process.env
expect(mockFn.existsSyncMock).toHaveBeenCalledTimes(1);
// createDirIfNotExisting calls mkdirSync which succeeds
expect(mockFn.mkdirSyncMock).toHaveBeenCalledTimes(1);
expect(result).toEqual(process.env.TRILIUM_DATA_DIR);
});
@@ -171,19 +175,19 @@ describe("data_dir.ts unit tests", async () => {
// use Generator to precisely control order of fs.existSync return values
const existsSyncMockGen = (function* () {
// 1) fs.existSync -> case B
// 1) fs.existSync -> case B -> checking if folder exists in home dir
yield false;
// 2) fs.existSync -> case C -> checking if default OS PlatformAppDataDir exists
yield true;
// 3) fs.existSync -> case C -> checking if Trilium Data folder exists
yield false;
})();
mockFn.existsSyncMock.mockImplementation(() => existsSyncMockGen.next().value);
// mkdirSync succeeds (folder doesn't exist)
mockFn.mkdirSyncMock.mockImplementation(() => undefined);
const result = getTriliumDataDir(dataDirName);
expect(mockFn.existsSyncMock).toHaveBeenCalledTimes(3);
expect(mockFn.existsSyncMock).toHaveBeenCalledTimes(2);
expect(mockFn.mkdirSyncMock).toHaveBeenCalledTimes(1);
expect(result).toEqual(mockPlatformDataPath);
});
@@ -198,21 +202,26 @@ describe("data_dir.ts unit tests", async () => {
// use Generator to precisely control order of fs.existSync return values
const existsSyncMockGen = (function* () {
// 1) fs.existSync -> case B
// 1) fs.existSync -> case B -> checking if folder exists in home dir
yield false;
// 2) fs.existSync -> case C -> checking if default OS PlatformAppDataDir exists
yield true;
// 3) fs.existSync -> case C -> checking if Trilium Data folder exists
yield true;
})();
mockFn.existsSyncMock.mockImplementation(() => existsSyncMockGen.next().value);
// mkdirSync throws EEXIST (folder already exists), statSync confirms it's a directory
const eexistError = new Error("EEXIST: file already exists") as NodeJS.ErrnoException;
eexistError.code = "EEXIST";
mockFn.mkdirSyncMock.mockImplementation(() => { throw eexistError; });
mockFn.statSyncMock.mockImplementation(() => ({ isDirectory: () => true }));
const result = getTriliumDataDir(dataDirName);
expect(result).toEqual(mockPlatformDataPath);
expect(mockFn.existsSyncMock).toHaveBeenCalledTimes(3);
expect(mockFn.mkdirSyncMock).toHaveBeenCalledTimes(0);
expect(mockFn.existsSyncMock).toHaveBeenCalledTimes(2);
expect(mockFn.mkdirSyncMock).toHaveBeenCalledTimes(1);
expect(mockFn.statSyncMock).toHaveBeenCalledTimes(1);
});
it("w/ Platform 'win32' and set process.env.APPDATA behaviour", async () => {
@@ -227,20 +236,20 @@ describe("data_dir.ts unit tests", async () => {
// use Generator to precisely control order of fs.existSync return values
const existsSyncMockGen = (function* () {
// 1) fs.existSync -> case B
// 1) fs.existSync -> case B -> checking if folder exists in home dir
yield false;
// 2) fs.existSync -> case C -> checking if default OS PlatformAppDataDir exists
yield true;
// 3) fs.existSync -> case C -> checking if Trilium Data folder exists
yield false;
})();
mockFn.existsSyncMock.mockImplementation(() => existsSyncMockGen.next().value);
// mkdirSync succeeds (folder doesn't exist)
mockFn.mkdirSyncMock.mockImplementation(() => undefined);
const result = getTriliumDataDir(dataDirName);
expect(result).toEqual(mockPlatformDataPath);
expect(mockFn.existsSyncMock).toHaveBeenCalledTimes(3);
expect(mockFn.existsSyncMock).toHaveBeenCalledTimes(2);
expect(mockFn.mkdirSyncMock).toHaveBeenCalledTimes(1);
});
});
@@ -253,19 +262,15 @@ describe("data_dir.ts unit tests", async () => {
setMockPlatform("aix", homedir, mockPlatformDataPath);
const existsSyncMockGen = (function* () {
// first fs.existSync -> case B -> checking if folder exists in home folder
yield false;
// second fs.existSync -> case D -> triggered by createDirIfNotExisting
yield false;
})();
mockFn.existsSyncMock.mockImplementation(() => existsSyncMockGen.next().value);
// fs.existSync -> case B -> checking if folder exists in home folder
mockFn.existsSyncMock.mockImplementation(() => false);
// mkdirSync succeeds (folder doesn't exist)
mockFn.mkdirSyncMock.mockImplementation(() => undefined);
const result = getTriliumDataDir(dataDirName);
expect(result).toEqual(mockPlatformDataPath);
expect(mockFn.existsSyncMock).toHaveBeenCalledTimes(2);
expect(mockFn.existsSyncMock).toHaveBeenCalledTimes(1);
expect(mockFn.mkdirSyncMock).toHaveBeenCalledTimes(1);
});
});

View File

@@ -75,9 +75,56 @@ export function getPlatformAppDataDir(platform: ReturnType<typeof os.platform>,
}
}
function outputPermissionDiagnostics(targetPath: fs.PathLike) {
const pathStr = targetPath.toString();
const parentDir = pathJoin(pathStr, "..");
console.error("\n========== PERMISSION ERROR DIAGNOSTICS ==========");
console.error(`Failed to create directory: ${pathStr}`);
// Output current process UID:GID (Unix only)
if (typeof process.getuid === "function" && typeof process.getgid === "function") {
console.error(`Process running as UID:GID = ${process.getuid()}:${process.getgid()}`);
}
// Try to get parent directory stats
try {
const stats = fs.statSync(parentDir);
console.error(`Parent directory: ${parentDir}`);
console.error(` Owner UID:GID = ${stats.uid}:${stats.gid}`);
console.error(` Permissions = ${(stats.mode & 0o777).toString(8)} (octal)`);
} catch {
console.error(`Parent directory ${parentDir} is not accessible`);
}
console.error("\nTo fix this issue:");
console.error(" - Ensure the data directory is owned by the user running Trilium");
console.error(" - Or set USER_UID and USER_GID environment variables to match the directory owner");
console.error(" - Example: docker run -e USER_UID=$(id -u) -e USER_GID=$(id -g) ...");
console.error("====================================================\n");
}
function createDirIfNotExisting(path: fs.PathLike, permissionMode: fs.Mode = FOLDER_PERMISSIONS) {
if (!fs.existsSync(path)) {
try {
fs.mkdirSync(path, permissionMode);
} catch (err: unknown) {
if (err && typeof err === "object" && "code" in err) {
const code = (err as { code: string }).code;
if (code === "EACCES") {
outputPermissionDiagnostics(path);
} else if (code === "EEXIST") {
// Directory already exists - verify it's actually a directory
try {
if (fs.statSync(path).isDirectory()) {
return;
}
} catch {
// If we can't stat it, fall through to re-throw original error
}
}
}
throw err;
}
}

View File

@@ -50,6 +50,11 @@ const LOCALE_MAPPINGS: Record<DISPLAYABLE_LOCALE_IDS, LocaleMapping | null> = {
coreTranslation: () => import("ckeditor5/translations/ja.js"),
premiumFeaturesTranslation: () => import("ckeditor5-premium-features/translations/ja.js"),
},
pl: {
languageCode: "pl",
coreTranslation: () => import("ckeditor5/translations/pl.js"),
premiumFeaturesTranslation: () => import("ckeditor5-premium-features/translations/pl.js"),
},
pt: {
languageCode: "pt",
coreTranslation: () => import("ckeditor5/translations/pt.js"),