diff --git a/apps/server/src/index.ts b/apps/server/src/index.ts
index b98e299..5a0199f 100644
--- a/apps/server/src/index.ts
+++ b/apps/server/src/index.ts
@@ -71,7 +71,7 @@ const socketPath = "/run/docker/plugins/ironmount.sock";
fetch: app.fetch,
});
- await startup();
+ startup();
logger.info(`Server is running at http://localhost:8080 and unix socket at ${socketPath}`);
})();
diff --git a/apps/server/src/modules/backends/backend.ts b/apps/server/src/modules/backends/backend.ts
index 9283d4c..4442ea5 100644
--- a/apps/server/src/modules/backends/backend.ts
+++ b/apps/server/src/modules/backends/backend.ts
@@ -1,8 +1,9 @@
import type { BackendStatus } from "@ironmount/schemas";
+import { VOLUME_MOUNT_BASE } from "../../core/constants";
import type { Volume } from "../../db/schema";
import { makeDirectoryBackend } from "./directory/directory-backend";
import { makeNfsBackend } from "./nfs/nfs-backend";
-import { VOLUME_MOUNT_BASE } from "../../core/constants";
+import { makeSmbBackend } from "./smb/smb-backend";
type OperationResult = {
error?: string;
@@ -22,11 +23,11 @@ export const createVolumeBackend = (volume: Volume): VolumeBackend => {
case "nfs": {
return makeNfsBackend(volume.config, path);
}
+ case "smb": {
+ return makeSmbBackend(volume.config, path);
+ }
case "directory": {
return makeDirectoryBackend(volume.config, path);
}
- default: {
- throw new Error(`Backend ${volume.config.backend} not implemented`);
- }
}
};
diff --git a/apps/server/src/modules/backends/nfs/nfs-backend.ts b/apps/server/src/modules/backends/nfs/nfs-backend.ts
index 3d18669..ebfd66e 100644
--- a/apps/server/src/modules/backends/nfs/nfs-backend.ts
+++ b/apps/server/src/modules/backends/nfs/nfs-backend.ts
@@ -1,17 +1,13 @@
-import { execFile as execFileCb } from "node:child_process";
import * as fs from "node:fs/promises";
import * as os from "node:os";
-import * as npath from "node:path";
import { BACKEND_STATUS, type BackendConfig } from "@ironmount/schemas";
import type { VolumeBackend } from "../backend";
import { logger } from "../../../utils/logger";
-import { promisify } from "node:util";
import { withTimeout } from "../../../utils/timeout";
import { OPERATION_TIMEOUT } from "../../../core/constants";
import { toMessage } from "../../../utils/errors";
import { getMountForPath } from "../../../utils/mountinfo";
-
-const execFile = promisify(execFileCb);
+import { createTestFile, executeMount, executeUnmount } from "../utils/backend-utils";
const mount = async (config: BackendConfig, path: string) => {
logger.debug(`Mounting volume ${path}...`);
@@ -44,14 +40,7 @@ const mount = async (config: BackendConfig, path: string) => {
logger.debug(`Mounting volume ${path}...`);
logger.info(`Executing mount: mount ${args.join(" ")}`);
- const { stderr } = await execFile("mount", args, {
- timeout: OPERATION_TIMEOUT,
- maxBuffer: 1024 * 1024,
- });
-
- if (stderr?.trim()) {
- logger.warn(stderr.trim());
- }
+ await executeMount(args);
logger.info(`NFS volume at ${path} mounted successfully.`);
return { status: BACKEND_STATUS.mounted };
@@ -59,11 +48,9 @@ const mount = async (config: BackendConfig, path: string) => {
try {
return await withTimeout(run(), OPERATION_TIMEOUT, "NFS mount");
- } catch (err: any) {
- const msg = err.stderr?.toString().trim() || err.message;
-
- logger.error("Error mounting NFS volume", { error: msg });
- return { status: BACKEND_STATUS.error, error: msg };
+ } catch (err) {
+ logger.error("Error mounting NFS volume", { error: toMessage(err) });
+ return { status: BACKEND_STATUS.error, error: toMessage(err) };
}
};
@@ -81,14 +68,7 @@ const unmount = async (path: string) => {
return { status: BACKEND_STATUS.unmounted };
}
- const { stderr } = await execFile("umount", ["-l", "-f", path], {
- timeout: OPERATION_TIMEOUT,
- maxBuffer: 1024 * 1024,
- });
-
- if (stderr?.trim()) {
- logger.warn(stderr.trim());
- }
+ await executeUnmount(path);
await fs.rmdir(path);
@@ -101,7 +81,6 @@ const unmount = async (path: string) => {
} catch (err: any) {
const msg = err.stderr?.toString().trim() || err.message;
logger.error("Error unmounting NFS volume", { path, error: msg });
-
return { status: BACKEND_STATUS.error, error: msg };
}
};
@@ -117,10 +96,7 @@ const checkHealth = async (path: string) => {
throw new Error(`Path ${path} is not mounted as NFS.`);
}
- const testFilePath = npath.join(path, `.healthcheck-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`);
-
- await fs.writeFile(testFilePath, "healthcheck");
- await fs.unlink(testFilePath);
+ await createTestFile(path);
logger.debug(`NFS volume at ${path} is healthy and mounted.`);
return { status: BACKEND_STATUS.mounted };
diff --git a/apps/server/src/modules/backends/smb/smb-backend.ts b/apps/server/src/modules/backends/smb/smb-backend.ts
new file mode 100644
index 0000000..7dbc7ce
--- /dev/null
+++ b/apps/server/src/modules/backends/smb/smb-backend.ts
@@ -0,0 +1,128 @@
+import * as fs from "node:fs/promises";
+import * as os from "node:os";
+import { BACKEND_STATUS, type BackendConfig } from "@ironmount/schemas";
+import type { VolumeBackend } from "../backend";
+import { logger } from "../../../utils/logger";
+import { withTimeout } from "../../../utils/timeout";
+import { OPERATION_TIMEOUT } from "../../../core/constants";
+import { toMessage } from "../../../utils/errors";
+import { getMountForPath } from "../../../utils/mountinfo";
+import { createTestFile, executeMount, executeUnmount } from "../utils/backend-utils";
+
+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);
+ 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}`);
+ }
+
+ 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) => {
+ 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.`);
+ }
+
+ 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),
+});
diff --git a/apps/server/src/modules/backends/utils/backend-utils.ts b/apps/server/src/modules/backends/utils/backend-utils.ts
new file mode 100644
index 0000000..b96d643
--- /dev/null
+++ b/apps/server/src/modules/backends/utils/backend-utils.ts
@@ -0,0 +1,37 @@
+import * as npath from "node:path";
+import * as fs from "node:fs/promises";
+import { execFile as execFileCb } from "node:child_process";
+import { promisify } from "node:util";
+import { OPERATION_TIMEOUT } from "../../../core/constants";
+import { logger } from "../../../utils/logger";
+
+const execFile = promisify(execFileCb);
+
+export const executeMount = async (args: string[]): Promise => {
+ const { stderr } = await execFile("mount", args, {
+ timeout: OPERATION_TIMEOUT,
+ maxBuffer: 1024 * 1024,
+ });
+
+ if (stderr?.trim()) {
+ logger.warn(stderr.trim());
+ }
+};
+
+export const executeUnmount = async (path: string): Promise => {
+ const { stderr } = await execFile("umount", ["-l", "-f", path], {
+ timeout: OPERATION_TIMEOUT,
+ maxBuffer: 1024 * 1024,
+ });
+
+ if (stderr?.trim()) {
+ logger.warn(stderr.trim());
+ }
+};
+
+export const createTestFile = async (path: string): Promise => {
+ const testFilePath = npath.join(path, `.healthcheck-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`);
+
+ await fs.writeFile(testFilePath, "healthcheck");
+ await fs.unlink(testFilePath);
+};
diff --git a/packages/schemas/src/index.ts b/packages/schemas/src/index.ts
index 39ee174..12142aa 100644
--- a/packages/schemas/src/index.ts
+++ b/packages/schemas/src/index.ts
@@ -18,6 +18,13 @@ export const nfsConfigSchema = type({
export const smbConfigSchema = type({
backend: "'smb'",
+ server: "string",
+ share: "string",
+ username: "string",
+ password: "string",
+ vers: type("'1.0' | '2.0' | '2.1' | '3.0'").default("3.0"),
+ domain: "string?",
+ port: type("string.integer.parse").or(type("number")).to("1 <= number <= 65535").default(445),
});
export const directoryConfigSchema = type({