refactor(volume-backend): assert mounted helper

This commit is contained in:
Nicolas Meienberger
2026-03-15 12:03:09 +01:00
parent 959cb21d83
commit 2d10da72f6
6 changed files with 67 additions and 64 deletions

View File

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

View File

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

View File

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

View File

@@ -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();
});
});

View File

@@ -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<void> => {
const shouldBeVerbose = process.env.LOG_LEVEL === "debug" || process.env.NODE_ENV !== "production";
@@ -44,6 +45,24 @@ export const executeUnmount = async (path: string): Promise<void> => {
}
};
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<void> => {
const testFilePath = npath.join(path, `.healthcheck-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`);

View File

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