From 2d10da72f6b3b6088dbfefc7aa2e2253e84275dc Mon Sep 17 00:00:00 2001 From: Nicolas Meienberger Date: Sun, 15 Mar 2026 12:03:09 +0100 Subject: [PATCH] refactor(volume-backend): assert mounted helper --- .../modules/backends/nfs/nfs-backend.ts | 18 +-------- .../modules/backends/rclone/rclone-backend.ts | 18 +-------- .../modules/backends/smb/smb-backend.ts | 18 +-------- .../utils/__tests__/backend-utils.test.ts | 40 +++++++++++++++++++ .../modules/backends/utils/backend-utils.ts | 19 +++++++++ .../modules/backends/webdav/webdav-backend.ts | 18 +-------- 6 files changed, 67 insertions(+), 64 deletions(-) create mode 100644 app/server/modules/backends/utils/__tests__/backend-utils.test.ts diff --git a/app/server/modules/backends/nfs/nfs-backend.ts b/app/server/modules/backends/nfs/nfs-backend.ts index 98ba3956..2f0fafdc 100644 --- a/app/server/modules/backends/nfs/nfs-backend.ts +++ b/app/server/modules/backends/nfs/nfs-backend.ts @@ -7,7 +7,7 @@ import { logger } from "@zerobyte/core/node"; import { getMountForPath } from "../../../utils/mountinfo"; import { withTimeout } from "../../../utils/timeout"; import type { VolumeBackend } from "../backend"; -import { executeMount, executeUnmount } from "../utils/backend-utils"; +import { assertMounted, executeMount, executeUnmount } from "../utils/backend-utils"; const mount = async (config: BackendConfig, path: string) => { logger.debug(`Mounting volume ${path}...`); @@ -111,21 +111,7 @@ const unmount = async (path: string) => { const checkHealth = async (path: string) => { const run = async () => { - try { - await fs.access(path); - } catch { - throw new Error("Volume is not mounted"); - } - - const mount = await getMountForPath(path); - - if (!mount || mount.mountPoint !== path) { - throw new Error("Volume is not mounted"); - } - - if (!mount.fstype.startsWith("nfs")) { - throw new Error(`Path ${path} is not mounted as NFS (found ${mount.fstype}).`); - } + await assertMounted(path, (fstype) => fstype.startsWith("nfs")); logger.debug(`NFS volume at ${path} is healthy and mounted.`); return { status: BACKEND_STATUS.mounted }; diff --git a/app/server/modules/backends/rclone/rclone-backend.ts b/app/server/modules/backends/rclone/rclone-backend.ts index 73735d51..06b946e0 100644 --- a/app/server/modules/backends/rclone/rclone-backend.ts +++ b/app/server/modules/backends/rclone/rclone-backend.ts @@ -6,7 +6,7 @@ import { logger } from "@zerobyte/core/node"; import { getMountForPath } from "../../../utils/mountinfo"; import { withTimeout } from "../../../utils/timeout"; import type { VolumeBackend } from "../backend"; -import { executeUnmount } from "../utils/backend-utils"; +import { assertMounted, executeUnmount } from "../utils/backend-utils"; import { BACKEND_STATUS, type BackendConfig } from "~/schemas/volumes"; import { safeExec } from "@zerobyte/core/node"; import { config as zbConfig } from "~/server/core/config"; @@ -102,21 +102,7 @@ const unmount = async (path: string) => { const checkHealth = async (path: string) => { const run = async () => { - try { - await fs.access(path); - } catch { - throw new Error("Volume is not mounted"); - } - - const mount = await getMountForPath(path); - - if (!mount || mount.mountPoint !== path) { - throw new Error("Volume is not mounted"); - } - - if (!mount.fstype.includes("rclone")) { - throw new Error(`Path ${path} is not mounted as rclone (found ${mount.fstype}).`); - } + await assertMounted(path, (fstype) => fstype.includes("rclone")); logger.debug(`Rclone volume at ${path} is healthy and mounted.`); return { status: BACKEND_STATUS.mounted }; diff --git a/app/server/modules/backends/smb/smb-backend.ts b/app/server/modules/backends/smb/smb-backend.ts index 05e5ae60..472a6111 100644 --- a/app/server/modules/backends/smb/smb-backend.ts +++ b/app/server/modules/backends/smb/smb-backend.ts @@ -7,7 +7,7 @@ import { logger } from "@zerobyte/core/node"; import { getMountForPath } from "../../../utils/mountinfo"; import { withTimeout } from "../../../utils/timeout"; import type { VolumeBackend } from "../backend"; -import { executeMount, executeUnmount } from "../utils/backend-utils"; +import { assertMounted, executeMount, executeUnmount } from "../utils/backend-utils"; import { BACKEND_STATUS, type BackendConfig } from "~/schemas/volumes"; const mount = async (config: BackendConfig, path: string) => { @@ -117,21 +117,7 @@ const unmount = async (path: string) => { const checkHealth = async (path: string) => { const run = async () => { - try { - await fs.access(path); - } catch { - throw new Error("Volume is not mounted"); - } - - const mount = await getMountForPath(path); - - if (!mount || mount.mountPoint !== path) { - throw new Error("Volume is not mounted"); - } - - if (mount.fstype !== "cifs") { - throw new Error(`Path ${path} is not mounted as CIFS/SMB (found ${mount.fstype}).`); - } + await assertMounted(path, (fstype) => fstype === "cifs"); logger.debug(`SMB volume at ${path} is healthy and mounted.`); return { status: BACKEND_STATUS.mounted }; diff --git a/app/server/modules/backends/utils/__tests__/backend-utils.test.ts b/app/server/modules/backends/utils/__tests__/backend-utils.test.ts new file mode 100644 index 00000000..d8da94d8 --- /dev/null +++ b/app/server/modules/backends/utils/__tests__/backend-utils.test.ts @@ -0,0 +1,40 @@ +import * as fs from "node:fs/promises"; +import { afterEach, describe, expect, mock, spyOn, test } from "bun:test"; +import * as mountinfo from "../../../../utils/mountinfo"; +import { assertMounted } from "../backend-utils"; + +afterEach(() => { + mock.restore(); +}); + +describe("assertMountedFilesystem", () => { + test("throws when the path is not accessible", async () => { + spyOn(fs, "access").mockRejectedValueOnce(new Error("missing")); + + await expect(assertMounted("/tmp/volume", (fstype) => fstype.startsWith("nfs"))).rejects.toThrow( + "Volume is not mounted", + ); + }); + + test("throws when the mount filesystem does not match", async () => { + spyOn(fs, "access").mockResolvedValueOnce(undefined); + spyOn(mountinfo, "getMountForPath").mockResolvedValueOnce({ + mountPoint: "/tmp/volume", + fstype: "cifs", + }); + + await expect(assertMounted("/tmp/volume", (fstype) => fstype.startsWith("nfs"))).rejects.toThrow( + "Path /tmp/volume is not mounted as correct fstype (found cifs).", + ); + }); + + test("accepts a matching mounted filesystem", async () => { + spyOn(fs, "access").mockResolvedValueOnce(undefined); + spyOn(mountinfo, "getMountForPath").mockResolvedValueOnce({ + mountPoint: "/tmp/volume", + fstype: "nfs4", + }); + + await expect(assertMounted("/tmp/volume", (fstype) => fstype.startsWith("nfs"))).resolves.toBeUndefined(); + }); +}); diff --git a/app/server/modules/backends/utils/backend-utils.ts b/app/server/modules/backends/utils/backend-utils.ts index d0f85cc1..751a055b 100644 --- a/app/server/modules/backends/utils/backend-utils.ts +++ b/app/server/modules/backends/utils/backend-utils.ts @@ -3,6 +3,7 @@ import * as npath from "node:path"; import { toMessage } from "../../../utils/errors"; import { logger } from "@zerobyte/core/node"; import { safeExec } from "@zerobyte/core/node"; +import { getMountForPath } from "../../../utils/mountinfo"; export const executeMount = async (args: string[]): Promise => { const shouldBeVerbose = process.env.LOG_LEVEL === "debug" || process.env.NODE_ENV !== "production"; @@ -44,6 +45,24 @@ export const executeUnmount = async (path: string): Promise => { } }; +export const assertMounted = async (path: string, isExpectedFilesystem: (fstype: string) => boolean) => { + try { + await fs.access(path); + } catch { + throw new Error("Volume is not mounted"); + } + + const mount = await getMountForPath(path); + + if (!mount || mount.mountPoint !== path) { + throw new Error("Volume is not mounted"); + } + + if (!isExpectedFilesystem(mount.fstype)) { + throw new Error(`Path ${path} is not mounted as correct fstype (found ${mount.fstype}).`); + } +}; + export const createTestFile = async (path: string): Promise => { const testFilePath = npath.join(path, `.healthcheck-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`); diff --git a/app/server/modules/backends/webdav/webdav-backend.ts b/app/server/modules/backends/webdav/webdav-backend.ts index 15f8c4c9..7885be84 100644 --- a/app/server/modules/backends/webdav/webdav-backend.ts +++ b/app/server/modules/backends/webdav/webdav-backend.ts @@ -7,7 +7,7 @@ import { logger } from "@zerobyte/core/node"; import { getMountForPath } from "../../../utils/mountinfo"; import { withTimeout } from "../../../utils/timeout"; import type { VolumeBackend } from "../backend"; -import { executeMount, executeUnmount } from "../utils/backend-utils"; +import { assertMounted, executeMount, executeUnmount } from "../utils/backend-utils"; import { BACKEND_STATUS, type BackendConfig } from "~/schemas/volumes"; const mount = async (config: BackendConfig, path: string) => { @@ -135,21 +135,7 @@ const unmount = async (path: string) => { const checkHealth = async (path: string) => { const run = async () => { - try { - await fs.access(path); - } catch { - throw new Error("Volume is not mounted"); - } - - const mount = await getMountForPath(path); - - if (!mount || mount.mountPoint !== path) { - throw new Error("Volume is not mounted"); - } - - if (mount.fstype !== "fuse" && mount.fstype !== "davfs") { - throw new Error(`Path ${path} is not mounted as WebDAV (found ${mount.fstype}).`); - } + await assertMounted(path, (fstype) => fstype === "fuse" || fstype === "davfs"); logger.debug(`WebDAV volume at ${path} is healthy and mounted.`); return { status: BACKEND_STATUS.mounted };