mirror of
https://github.com/nicotsx/ironmount.git
synced 2025-12-10 12:10:51 +01:00
refactor: unify backend and frontend servers (#3)
* refactor: unify backend and frontend servers * refactor: correct paths for openapi & drizzle * refactor: move api-client to client * fix: drizzle paths * chore: fix linting issues * fix: form reset issue
This commit is contained in:
37
app/server/modules/backends/backend.ts
Normal file
37
app/server/modules/backends/backend.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
import type { BackendStatus } from "~/schemas/volumes";
|
||||
import type { Volume } from "../../db/schema";
|
||||
import { getVolumePath } from "../volumes/helpers";
|
||||
import { makeDirectoryBackend } from "./directory/directory-backend";
|
||||
import { makeNfsBackend } from "./nfs/nfs-backend";
|
||||
import { makeSmbBackend } from "./smb/smb-backend";
|
||||
import { makeWebdavBackend } from "./webdav/webdav-backend";
|
||||
|
||||
type OperationResult = {
|
||||
error?: string;
|
||||
status: BackendStatus;
|
||||
};
|
||||
|
||||
export type VolumeBackend = {
|
||||
mount: () => Promise<OperationResult>;
|
||||
unmount: () => Promise<OperationResult>;
|
||||
checkHealth: () => Promise<OperationResult>;
|
||||
};
|
||||
|
||||
export const createVolumeBackend = (volume: Volume): VolumeBackend => {
|
||||
const path = getVolumePath(volume);
|
||||
|
||||
switch (volume.config.backend) {
|
||||
case "nfs": {
|
||||
return makeNfsBackend(volume.config, path);
|
||||
}
|
||||
case "smb": {
|
||||
return makeSmbBackend(volume.config, path);
|
||||
}
|
||||
case "directory": {
|
||||
return makeDirectoryBackend(volume.config, path);
|
||||
}
|
||||
case "webdav": {
|
||||
return makeWebdavBackend(volume.config, path);
|
||||
}
|
||||
}
|
||||
};
|
||||
59
app/server/modules/backends/directory/directory-backend.ts
Normal file
59
app/server/modules/backends/directory/directory-backend.ts
Normal file
@@ -0,0 +1,59 @@
|
||||
import * as fs from "node:fs/promises";
|
||||
import * as npath from "node:path";
|
||||
import { toMessage } from "../../../utils/errors";
|
||||
import { logger } from "../../../utils/logger";
|
||||
import type { VolumeBackend } from "../backend";
|
||||
import { BACKEND_STATUS, type BackendConfig } from "~/schemas/volumes";
|
||||
|
||||
const mount = async (config: BackendConfig, _volumePath: string) => {
|
||||
if (config.backend !== "directory") {
|
||||
return { status: BACKEND_STATUS.error, error: "Invalid backend type" };
|
||||
}
|
||||
|
||||
logger.info("Mounting directory volume from:", config.path);
|
||||
|
||||
try {
|
||||
await fs.access(config.path);
|
||||
const stats = await fs.stat(config.path);
|
||||
|
||||
if (!stats.isDirectory()) {
|
||||
return { status: BACKEND_STATUS.error, error: "Path is not a directory" };
|
||||
}
|
||||
|
||||
return { status: BACKEND_STATUS.mounted };
|
||||
} catch (error) {
|
||||
logger.error("Failed to mount directory volume:", error);
|
||||
return { status: BACKEND_STATUS.error, error: toMessage(error) };
|
||||
}
|
||||
};
|
||||
|
||||
const unmount = async () => {
|
||||
logger.info("Cannot unmount directory volume.");
|
||||
return { status: BACKEND_STATUS.unmounted };
|
||||
};
|
||||
|
||||
const checkHealth = async (config: BackendConfig) => {
|
||||
if (config.backend !== "directory") {
|
||||
return { status: BACKEND_STATUS.error, error: "Invalid backend type" };
|
||||
}
|
||||
|
||||
try {
|
||||
await fs.access(config.path);
|
||||
|
||||
// Try to create a temporary file to ensure write access
|
||||
const tempFilePath = npath.join(config.path, `.healthcheck-${Date.now()}`);
|
||||
await fs.writeFile(tempFilePath, "healthcheck");
|
||||
await fs.unlink(tempFilePath);
|
||||
|
||||
return { status: BACKEND_STATUS.mounted };
|
||||
} catch (error) {
|
||||
logger.error("Directory health check failed:", error);
|
||||
return { status: BACKEND_STATUS.error, error: toMessage(error) };
|
||||
}
|
||||
};
|
||||
|
||||
export const makeDirectoryBackend = (config: BackendConfig, volumePath: string): VolumeBackend => ({
|
||||
mount: () => mount(config, volumePath),
|
||||
unmount,
|
||||
checkHealth: () => checkHealth(config),
|
||||
});
|
||||
121
app/server/modules/backends/nfs/nfs-backend.ts
Normal file
121
app/server/modules/backends/nfs/nfs-backend.ts
Normal file
@@ -0,0 +1,121 @@
|
||||
import * as fs from "node:fs/promises";
|
||||
import * as os from "node:os";
|
||||
import { OPERATION_TIMEOUT } from "../../../core/constants";
|
||||
import { toMessage } from "../../../utils/errors";
|
||||
import { logger } from "../../../utils/logger";
|
||||
import { getMountForPath } from "../../../utils/mountinfo";
|
||||
import { withTimeout } from "../../../utils/timeout";
|
||||
import type { VolumeBackend } from "../backend";
|
||||
import { createTestFile, executeMount, executeUnmount } from "../utils/backend-utils";
|
||||
import { BACKEND_STATUS, type BackendConfig } from "~/schemas/volumes";
|
||||
|
||||
const mount = async (config: BackendConfig, path: string) => {
|
||||
logger.debug(`Mounting volume ${path}...`);
|
||||
|
||||
if (config.backend !== "nfs") {
|
||||
logger.error("Provided config is not for NFS backend");
|
||||
return { status: BACKEND_STATUS.error, error: "Provided config is not for NFS backend" };
|
||||
}
|
||||
|
||||
if (os.platform() !== "linux") {
|
||||
logger.error("NFS mounting is only supported on Linux hosts.");
|
||||
return { status: BACKEND_STATUS.error, error: "NFS mounting is only supported on Linux hosts." };
|
||||
}
|
||||
|
||||
const { status } = await checkHealth(path, config.readOnly ?? false);
|
||||
if (status === "mounted") {
|
||||
return { status: BACKEND_STATUS.mounted };
|
||||
}
|
||||
|
||||
logger.debug(`Trying to unmount any existing mounts at ${path} before mounting...`);
|
||||
await unmount(path);
|
||||
|
||||
const run = async () => {
|
||||
await fs.mkdir(path, { recursive: true });
|
||||
|
||||
const source = `${config.server}:${config.exportPath}`;
|
||||
const options = [`vers=${config.version}`, `port=${config.port}`];
|
||||
if (config.readOnly) {
|
||||
options.push("ro");
|
||||
}
|
||||
const args = ["-t", "nfs", "-o", options.join(","), source, path];
|
||||
|
||||
logger.debug(`Mounting volume ${path}...`);
|
||||
logger.info(`Executing mount: mount ${args.join(" ")}`);
|
||||
|
||||
await executeMount(args);
|
||||
|
||||
logger.info(`NFS volume at ${path} mounted successfully.`);
|
||||
return { status: BACKEND_STATUS.mounted };
|
||||
};
|
||||
|
||||
try {
|
||||
return await withTimeout(run(), OPERATION_TIMEOUT, "NFS mount");
|
||||
} catch (err) {
|
||||
logger.error("Error mounting NFS volume", { error: toMessage(err) });
|
||||
return { status: BACKEND_STATUS.error, error: toMessage(err) };
|
||||
}
|
||||
};
|
||||
|
||||
const unmount = async (path: string) => {
|
||||
if (os.platform() !== "linux") {
|
||||
logger.error("NFS unmounting is only supported on Linux hosts.");
|
||||
return { status: BACKEND_STATUS.error, error: "NFS unmounting is only supported on Linux hosts." };
|
||||
}
|
||||
|
||||
const run = async () => {
|
||||
try {
|
||||
await fs.access(path);
|
||||
} catch {
|
||||
logger.warn(`Path ${path} does not exist. Skipping unmount.`);
|
||||
return { status: BACKEND_STATUS.unmounted };
|
||||
}
|
||||
|
||||
await executeUnmount(path);
|
||||
|
||||
await fs.rmdir(path);
|
||||
|
||||
logger.info(`NFS volume at ${path} unmounted successfully.`);
|
||||
return { status: BACKEND_STATUS.unmounted };
|
||||
};
|
||||
|
||||
try {
|
||||
return await withTimeout(run(), OPERATION_TIMEOUT, "NFS unmount");
|
||||
} catch (err) {
|
||||
logger.error("Error unmounting NFS volume", { path, error: toMessage(err) });
|
||||
return { status: BACKEND_STATUS.error, error: toMessage(err) };
|
||||
}
|
||||
};
|
||||
|
||||
const checkHealth = async (path: string, readOnly: boolean) => {
|
||||
const run = async () => {
|
||||
logger.debug(`Checking health of NFS volume at ${path}...`);
|
||||
await fs.access(path);
|
||||
|
||||
const mount = await getMountForPath(path);
|
||||
|
||||
if (!mount || !mount.fstype.startsWith("nfs")) {
|
||||
throw new Error(`Path ${path} is not mounted as NFS.`);
|
||||
}
|
||||
|
||||
if (!readOnly) {
|
||||
await createTestFile(path);
|
||||
}
|
||||
|
||||
logger.debug(`NFS volume at ${path} is healthy and mounted.`);
|
||||
return { status: BACKEND_STATUS.mounted };
|
||||
};
|
||||
|
||||
try {
|
||||
return await withTimeout(run(), OPERATION_TIMEOUT, "NFS health check");
|
||||
} catch (error) {
|
||||
logger.error("NFS volume health check failed:", toMessage(error));
|
||||
return { status: BACKEND_STATUS.error, error: toMessage(error) };
|
||||
}
|
||||
};
|
||||
|
||||
export const makeNfsBackend = (config: BackendConfig, path: string): VolumeBackend => ({
|
||||
mount: () => mount(config, path),
|
||||
unmount: () => unmount(path),
|
||||
checkHealth: () => checkHealth(path, config.readOnly ?? false),
|
||||
});
|
||||
134
app/server/modules/backends/smb/smb-backend.ts
Normal file
134
app/server/modules/backends/smb/smb-backend.ts
Normal file
@@ -0,0 +1,134 @@
|
||||
import * as fs from "node:fs/promises";
|
||||
import * as os from "node:os";
|
||||
import { OPERATION_TIMEOUT } from "../../../core/constants";
|
||||
import { toMessage } from "../../../utils/errors";
|
||||
import { logger } from "../../../utils/logger";
|
||||
import { getMountForPath } from "../../../utils/mountinfo";
|
||||
import { withTimeout } from "../../../utils/timeout";
|
||||
import type { VolumeBackend } from "../backend";
|
||||
import { createTestFile, executeMount, executeUnmount } from "../utils/backend-utils";
|
||||
import { BACKEND_STATUS, type BackendConfig } from "~/schemas/volumes";
|
||||
|
||||
const mount = async (config: BackendConfig, path: string) => {
|
||||
logger.debug(`Mounting SMB volume ${path}...`);
|
||||
|
||||
if (config.backend !== "smb") {
|
||||
logger.error("Provided config is not for SMB backend");
|
||||
return { status: BACKEND_STATUS.error, error: "Provided config is not for SMB backend" };
|
||||
}
|
||||
|
||||
if (os.platform() !== "linux") {
|
||||
logger.error("SMB mounting is only supported on Linux hosts.");
|
||||
return { status: BACKEND_STATUS.error, error: "SMB mounting is only supported on Linux hosts." };
|
||||
}
|
||||
|
||||
const { status } = await checkHealth(path, config.readOnly ?? false);
|
||||
if (status === "mounted") {
|
||||
return { status: BACKEND_STATUS.mounted };
|
||||
}
|
||||
|
||||
logger.debug(`Trying to unmount any existing mounts at ${path} before mounting...`);
|
||||
await unmount(path);
|
||||
|
||||
const run = async () => {
|
||||
await fs.mkdir(path, { recursive: true });
|
||||
|
||||
const source = `//${config.server}/${config.share}`;
|
||||
const options = [
|
||||
`user=${config.username}`,
|
||||
`pass=${config.password}`,
|
||||
`vers=${config.vers}`,
|
||||
`port=${config.port}`,
|
||||
"uid=1000",
|
||||
"gid=1000",
|
||||
];
|
||||
|
||||
if (config.domain) {
|
||||
options.push(`domain=${config.domain}`);
|
||||
}
|
||||
|
||||
if (config.readOnly) {
|
||||
options.push("ro");
|
||||
}
|
||||
|
||||
const args = ["-t", "cifs", "-o", options.join(","), source, path];
|
||||
|
||||
logger.debug(`Mounting SMB volume ${path}...`);
|
||||
logger.info(`Executing mount: mount ${args.join(" ")}`);
|
||||
|
||||
await executeMount(args);
|
||||
|
||||
logger.info(`SMB volume at ${path} mounted successfully.`);
|
||||
return { status: BACKEND_STATUS.mounted };
|
||||
};
|
||||
|
||||
try {
|
||||
return await withTimeout(run(), OPERATION_TIMEOUT, "SMB mount");
|
||||
} catch (error) {
|
||||
logger.error("Error mounting SMB volume", { error: toMessage(error) });
|
||||
return { status: BACKEND_STATUS.error, error: toMessage(error) };
|
||||
}
|
||||
};
|
||||
|
||||
const unmount = async (path: string) => {
|
||||
if (os.platform() !== "linux") {
|
||||
logger.error("SMB unmounting is only supported on Linux hosts.");
|
||||
return { status: BACKEND_STATUS.error, error: "SMB unmounting is only supported on Linux hosts." };
|
||||
}
|
||||
|
||||
const run = async () => {
|
||||
try {
|
||||
await fs.access(path);
|
||||
} catch {
|
||||
logger.warn(`Path ${path} does not exist. Skipping unmount.`);
|
||||
return { status: BACKEND_STATUS.unmounted };
|
||||
}
|
||||
|
||||
await executeUnmount(path);
|
||||
|
||||
await fs.rmdir(path);
|
||||
|
||||
logger.info(`SMB volume at ${path} unmounted successfully.`);
|
||||
return { status: BACKEND_STATUS.unmounted };
|
||||
};
|
||||
|
||||
try {
|
||||
return await withTimeout(run(), OPERATION_TIMEOUT, "SMB unmount");
|
||||
} catch (error) {
|
||||
logger.error("Error unmounting SMB volume", { path, error: toMessage(error) });
|
||||
return { status: BACKEND_STATUS.error, error: toMessage(error) };
|
||||
}
|
||||
};
|
||||
|
||||
const checkHealth = async (path: string, readOnly: boolean) => {
|
||||
const run = async () => {
|
||||
logger.debug(`Checking health of SMB volume at ${path}...`);
|
||||
await fs.access(path);
|
||||
|
||||
const mount = await getMountForPath(path);
|
||||
|
||||
if (!mount || mount.fstype !== "cifs") {
|
||||
throw new Error(`Path ${path} is not mounted as CIFS/SMB.`);
|
||||
}
|
||||
|
||||
if (!readOnly) {
|
||||
await createTestFile(path);
|
||||
}
|
||||
|
||||
logger.debug(`SMB volume at ${path} is healthy and mounted.`);
|
||||
return { status: BACKEND_STATUS.mounted };
|
||||
};
|
||||
|
||||
try {
|
||||
return await withTimeout(run(), OPERATION_TIMEOUT, "SMB health check");
|
||||
} catch (error) {
|
||||
logger.error("SMB volume health check failed:", toMessage(error));
|
||||
return { status: BACKEND_STATUS.error, error: toMessage(error) };
|
||||
}
|
||||
};
|
||||
|
||||
export const makeSmbBackend = (config: BackendConfig, path: string): VolumeBackend => ({
|
||||
mount: () => mount(config, path),
|
||||
unmount: () => unmount(path),
|
||||
checkHealth: () => checkHealth(path, config.readOnly ?? false),
|
||||
});
|
||||
47
app/server/modules/backends/utils/backend-utils.ts
Normal file
47
app/server/modules/backends/utils/backend-utils.ts
Normal file
@@ -0,0 +1,47 @@
|
||||
import * as fs from "node:fs/promises";
|
||||
import * as npath from "node:path";
|
||||
import { toMessage } from "../../../utils/errors";
|
||||
import { logger } from "../../../utils/logger";
|
||||
import { $ } from "bun";
|
||||
|
||||
export const executeMount = async (args: string[]): Promise<void> => {
|
||||
let stderr: string | undefined;
|
||||
|
||||
const result = await $`mount ${args}`.nothrow();
|
||||
stderr = result.stderr.toString();
|
||||
|
||||
if (stderr?.trim()) {
|
||||
logger.warn(stderr.trim());
|
||||
}
|
||||
};
|
||||
|
||||
export const executeUnmount = async (path: string): Promise<void> => {
|
||||
let stderr: string | undefined;
|
||||
|
||||
const result = await $`umount -l -f ${path}`.nothrow();
|
||||
stderr = result.stderr.toString();
|
||||
|
||||
if (stderr?.trim()) {
|
||||
logger.warn(stderr.trim());
|
||||
}
|
||||
};
|
||||
|
||||
export const createTestFile = async (path: string): Promise<void> => {
|
||||
const testFilePath = npath.join(path, `.healthcheck-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`);
|
||||
|
||||
await fs.writeFile(testFilePath, "healthcheck");
|
||||
|
||||
const files = await fs.readdir(path);
|
||||
await Promise.all(
|
||||
files.map(async (file) => {
|
||||
if (file.startsWith(".healthcheck-")) {
|
||||
const filePath = npath.join(path, file);
|
||||
try {
|
||||
await fs.unlink(filePath);
|
||||
} catch (err) {
|
||||
logger.warn(`Failed to stat or unlink file ${filePath}: ${toMessage(err)}`);
|
||||
}
|
||||
}
|
||||
}),
|
||||
);
|
||||
};
|
||||
168
app/server/modules/backends/webdav/webdav-backend.ts
Normal file
168
app/server/modules/backends/webdav/webdav-backend.ts
Normal file
@@ -0,0 +1,168 @@
|
||||
import { execFile as execFileCb } from "node:child_process";
|
||||
import * as fs from "node:fs/promises";
|
||||
import * as os from "node:os";
|
||||
import { promisify } from "node:util";
|
||||
import { OPERATION_TIMEOUT } from "../../../core/constants";
|
||||
import { toMessage } from "../../../utils/errors";
|
||||
import { logger } from "../../../utils/logger";
|
||||
import { getMountForPath } from "../../../utils/mountinfo";
|
||||
import { withTimeout } from "../../../utils/timeout";
|
||||
import type { VolumeBackend } from "../backend";
|
||||
import { createTestFile, executeMount, executeUnmount } from "../utils/backend-utils";
|
||||
import { BACKEND_STATUS, type BackendConfig } from "~/schemas/volumes";
|
||||
|
||||
const execFile = promisify(execFileCb);
|
||||
|
||||
const mount = async (config: BackendConfig, path: string) => {
|
||||
logger.debug(`Mounting WebDAV volume ${path}...`);
|
||||
|
||||
if (config.backend !== "webdav") {
|
||||
logger.error("Provided config is not for WebDAV backend");
|
||||
return { status: BACKEND_STATUS.error, error: "Provided config is not for WebDAV backend" };
|
||||
}
|
||||
|
||||
if (os.platform() !== "linux") {
|
||||
logger.error("WebDAV mounting is only supported on Linux hosts.");
|
||||
return { status: BACKEND_STATUS.error, error: "WebDAV mounting is only supported on Linux hosts." };
|
||||
}
|
||||
|
||||
const { status } = await checkHealth(path, config.readOnly ?? false);
|
||||
if (status === "mounted") {
|
||||
return { status: BACKEND_STATUS.mounted };
|
||||
}
|
||||
|
||||
logger.debug(`Trying to unmount any existing mounts at ${path} before mounting...`);
|
||||
await unmount(path);
|
||||
|
||||
const run = async () => {
|
||||
await fs.mkdir(path, { recursive: true }).catch((err) => {
|
||||
logger.warn(`Failed to create directory ${path}: ${err.message}`);
|
||||
});
|
||||
|
||||
const protocol = config.ssl ? "https" : "http";
|
||||
const defaultPort = config.ssl ? 443 : 80;
|
||||
const port = config.port !== defaultPort ? `:${config.port}` : "";
|
||||
const source = `${protocol}://${config.server}${port}${config.path}`;
|
||||
|
||||
const options = config.readOnly
|
||||
? ["uid=1000", "gid=1000", "file_mode=0444", "dir_mode=0555", "ro"]
|
||||
: ["uid=1000", "gid=1000", "file_mode=0664", "dir_mode=0775"];
|
||||
|
||||
if (config.username && config.password) {
|
||||
const secretsFile = "/etc/davfs2/secrets";
|
||||
const secretsContent = `${source} ${config.username} ${config.password}\n`;
|
||||
await fs.appendFile(secretsFile, secretsContent, { mode: 0o600 });
|
||||
}
|
||||
|
||||
logger.debug(`Mounting WebDAV volume ${path}...`);
|
||||
|
||||
const args = ["-t", "davfs", source, path];
|
||||
await executeMount(args);
|
||||
|
||||
const { stderr } = await execFile("mount", ["-t", "davfs", "-o", options.join(","), source, path], {
|
||||
timeout: OPERATION_TIMEOUT,
|
||||
maxBuffer: 1024 * 1024,
|
||||
});
|
||||
|
||||
if (stderr?.trim()) {
|
||||
logger.warn(stderr.trim());
|
||||
}
|
||||
|
||||
logger.info(`WebDAV volume at ${path} mounted successfully.`);
|
||||
return { status: BACKEND_STATUS.mounted };
|
||||
};
|
||||
|
||||
try {
|
||||
return await withTimeout(run(), OPERATION_TIMEOUT, "WebDAV mount");
|
||||
} catch (error) {
|
||||
const errorMsg = toMessage(error);
|
||||
|
||||
if (errorMsg.includes("already mounted")) {
|
||||
return { status: BACKEND_STATUS.mounted };
|
||||
}
|
||||
|
||||
logger.error("Error mounting WebDAV volume", { error: errorMsg });
|
||||
|
||||
if (errorMsg.includes("option") && errorMsg.includes("requires argument")) {
|
||||
return {
|
||||
status: BACKEND_STATUS.error,
|
||||
error: "Invalid mount options. Please check your WebDAV server configuration.",
|
||||
};
|
||||
} else if (errorMsg.includes("connection refused") || errorMsg.includes("Connection refused")) {
|
||||
return {
|
||||
status: BACKEND_STATUS.error,
|
||||
error: "Cannot connect to WebDAV server. Please check the server address and port.",
|
||||
};
|
||||
} else if (errorMsg.includes("unauthorized") || errorMsg.includes("Unauthorized")) {
|
||||
return {
|
||||
status: BACKEND_STATUS.error,
|
||||
error: "Authentication failed. Please check your username and password.",
|
||||
};
|
||||
}
|
||||
|
||||
return { status: BACKEND_STATUS.error, error: errorMsg };
|
||||
}
|
||||
};
|
||||
|
||||
const unmount = async (path: string) => {
|
||||
if (os.platform() !== "linux") {
|
||||
logger.error("WebDAV unmounting is only supported on Linux hosts.");
|
||||
return { status: BACKEND_STATUS.error, error: "WebDAV unmounting is only supported on Linux hosts." };
|
||||
}
|
||||
|
||||
const run = async () => {
|
||||
try {
|
||||
await fs.access(path);
|
||||
} catch (e) {
|
||||
logger.warn(`Path ${path} does not exist. Skipping unmount.`, e);
|
||||
return { status: BACKEND_STATUS.unmounted };
|
||||
}
|
||||
|
||||
await executeUnmount(path);
|
||||
|
||||
await fs.rmdir(path);
|
||||
|
||||
logger.info(`WebDAV volume at ${path} unmounted successfully.`);
|
||||
return { status: BACKEND_STATUS.unmounted };
|
||||
};
|
||||
|
||||
try {
|
||||
return await withTimeout(run(), OPERATION_TIMEOUT, "WebDAV unmount");
|
||||
} catch (error) {
|
||||
logger.error("Error unmounting WebDAV volume", { path, error: toMessage(error) });
|
||||
return { status: BACKEND_STATUS.error, error: toMessage(error) };
|
||||
}
|
||||
};
|
||||
|
||||
const checkHealth = async (path: string, readOnly: boolean) => {
|
||||
const run = async () => {
|
||||
logger.debug(`Checking health of WebDAV volume at ${path}...`);
|
||||
await fs.access(path);
|
||||
|
||||
const mount = await getMountForPath(path);
|
||||
|
||||
if (!mount || mount.fstype !== "fuse") {
|
||||
throw new Error(`Path ${path} is not mounted as WebDAV.`);
|
||||
}
|
||||
|
||||
if (!readOnly) {
|
||||
await createTestFile(path);
|
||||
}
|
||||
|
||||
logger.debug(`WebDAV volume at ${path} is healthy and mounted.`);
|
||||
return { status: BACKEND_STATUS.mounted };
|
||||
};
|
||||
|
||||
try {
|
||||
return await withTimeout(run(), OPERATION_TIMEOUT, "WebDAV health check");
|
||||
} catch (error) {
|
||||
logger.error("WebDAV volume health check failed:", toMessage(error));
|
||||
return { status: BACKEND_STATUS.error, error: toMessage(error) };
|
||||
}
|
||||
};
|
||||
|
||||
export const makeWebdavBackend = (config: BackendConfig, path: string): VolumeBackend => ({
|
||||
mount: () => mount(config, path),
|
||||
unmount: () => unmount(path),
|
||||
checkHealth: () => checkHealth(path, config.readOnly ?? false),
|
||||
});
|
||||
Reference in New Issue
Block a user