From dafac5cbe7a62f7c969b950acc9a84422b80d9bf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thi=C3=AAn=20To=C3=A1n?= Date: Sun, 10 May 2026 10:51:04 +0000 Subject: [PATCH] fix: handle permission denied when creating DATA_DIR - Add permission denied fallback in DATA_DIR initialization - Falls back to default user directory (~/.9router) if configured DATA_DIR is not writable - Apply same fallback logic to mitm/paths.js for consistency - Logs informative warning when fallback occurs - Added unit test to verify fallback behavior Fixes the error: EACCES: permission denied, mkdir '/var/lib/9router/db' --- src/lib/dataDir.js | 25 +++++++++++++++++++++++-- src/mitm/paths.js | 26 +++++++++++++++++++++++--- tests/unit/dataDir.test.js | 27 +++++++++++++++++++++++++++ 3 files changed, 73 insertions(+), 5 deletions(-) create mode 100644 tests/unit/dataDir.test.js diff --git a/src/lib/dataDir.js b/src/lib/dataDir.js index 946a70853c0..61123d0de83 100644 --- a/src/lib/dataDir.js +++ b/src/lib/dataDir.js @@ -1,14 +1,35 @@ +import fs from "node:fs"; import path from "path"; import os from "os"; const APP_NAME = "9router"; -export function getDataDir() { - if (process.env.DATA_DIR) return process.env.DATA_DIR; +function getDefaultDataDir() { if (process.platform === "win32") { return path.join(process.env.APPDATA || path.join(os.homedir(), "AppData", "Roaming"), APP_NAME); } return path.join(os.homedir(), `.${APP_NAME}`); } +export function getDataDir() { + const configured = process.env.DATA_DIR; + if (configured) { + try { + fs.mkdirSync(configured, { recursive: true }); + return configured; + } catch (error) { + if (error?.code === "EACCES" || error?.code === "EPERM") { + console.warn( + `[DATA_DIR] Cannot use configured DATA_DIR='${configured}' because it is not writable. Falling back to default user directory.`, + ); + } else { + console.warn( + `[DATA_DIR] Unable to initialize configured DATA_DIR='${configured}': ${error?.message}. Falling back to default user directory.`, + ); + } + } + } + return getDefaultDataDir(); +} + export const DATA_DIR = getDataDir(); diff --git a/src/mitm/paths.js b/src/mitm/paths.js index 3c667f411d8..b141ae9974b 100644 --- a/src/mitm/paths.js +++ b/src/mitm/paths.js @@ -1,15 +1,35 @@ +const fs = require("fs"); const path = require("path"); const os = require("os"); -// Single source of truth for data directory — matches localDb.js logic -function getDataDir() { - if (process.env.DATA_DIR) return process.env.DATA_DIR; +function getDefaultDataDir() { if (process.platform === "win32") { return path.join(process.env.APPDATA || path.join(os.homedir(), "AppData", "Roaming"), "9router"); } return path.join(os.homedir(), ".9router"); } +function getDataDir() { + const configured = process.env.DATA_DIR; + if (configured) { + try { + fs.mkdirSync(configured, { recursive: true }); + return configured; + } catch (error) { + if (error?.code === "EACCES" || error?.code === "EPERM") { + console.warn( + `[DATA_DIR] Cannot use configured DATA_DIR='${configured}' because it is not writable. Falling back to default user directory.`, + ); + } else { + console.warn( + `[DATA_DIR] Unable to initialize configured DATA_DIR='${configured}': ${error?.message}. Falling back to default user directory.`, + ); + } + } + } + return getDefaultDataDir(); +} + const DATA_DIR = getDataDir(); const MITM_DIR = path.join(DATA_DIR, "mitm"); diff --git a/tests/unit/dataDir.test.js b/tests/unit/dataDir.test.js new file mode 100644 index 00000000000..a3cac0a8e74 --- /dev/null +++ b/tests/unit/dataDir.test.js @@ -0,0 +1,27 @@ +import os from "node:os"; +import path from "node:path"; +import fs from "node:fs"; +import { describe, it, expect, afterEach, vi } from "vitest"; + +const originalDataDir = process.env.DATA_DIR; + +afterEach(() => { + vi.resetModules(); + vi.restoreAllMocks(); + if (originalDataDir === undefined) delete process.env.DATA_DIR; + else process.env.DATA_DIR = originalDataDir; +}); + +describe("DATA_DIR fallback", () => { + it("falls back to the default user directory when configured DATA_DIR is not writable", async () => { + process.env.DATA_DIR = path.join(os.tmpdir(), "9router-unwritable"); + vi.spyOn(fs, "mkdirSync").mockImplementation(() => { + const error = new Error("permission denied"); + error.code = "EACCES"; + throw error; + }); + + const { DATA_DIR } = await import("@/lib/dataDir.js"); + expect(DATA_DIR).toBe(path.join(os.homedir(), ".9router")); + }); +});