feat(server): test mount endpoint

This commit is contained in:
Nicolas Meienberger
2025-09-02 22:52:44 +02:00
parent de0ae08008
commit 0c0a3b8581
9 changed files with 184 additions and 4 deletions

View File

@@ -12,3 +12,5 @@
!apps/**/drizzle/**
!apps/**/app/**
!apps/**/public/**
!packages/**/src/**

View File

@@ -3,6 +3,7 @@ FROM oven/bun:1.2.20-alpine AS base
WORKDIR /app
COPY ./package.json ./bun.lock ./
COPY ./packages/schemas/package.json ./packages/schemas/package.json
COPY ./apps/client/package.json ./apps/client/package.json
COPY ./apps/server/package.json ./apps/server/package.json

View File

@@ -1,11 +1,13 @@
// This file is auto-generated by @hey-api/openapi-ts
import { type Options, listVolumes, createVolume, deleteVolume } from "../sdk.gen";
import { type Options, listVolumes, createVolume, testConnection, deleteVolume } from "../sdk.gen";
import { queryOptions, type UseMutationOptions, type DefaultError } from "@tanstack/react-query";
import type {
ListVolumesData,
CreateVolumeData,
CreateVolumeResponse,
TestConnectionData,
TestConnectionResponse,
DeleteVolumeData,
DeleteVolumeResponse,
} from "../types.gen";
@@ -109,6 +111,46 @@ export const createVolumeMutation = (
return mutationOptions;
};
export const testConnectionQueryKey = (options?: Options<TestConnectionData>) =>
createQueryKey("testConnection", options);
/**
* Test connection to backend
*/
export const testConnectionOptions = (options?: Options<TestConnectionData>) => {
return queryOptions({
queryFn: async ({ queryKey, signal }) => {
const { data } = await testConnection({
...options,
...queryKey[0],
signal,
throwOnError: true,
});
return data;
},
queryKey: testConnectionQueryKey(options),
});
};
/**
* Test connection to backend
*/
export const testConnectionMutation = (
options?: Partial<Options<TestConnectionData>>,
): UseMutationOptions<TestConnectionResponse, DefaultError, Options<TestConnectionData>> => {
const mutationOptions: UseMutationOptions<TestConnectionResponse, DefaultError, Options<TestConnectionData>> = {
mutationFn: async (localOptions) => {
const { data } = await testConnection({
...options,
...localOptions,
throwOnError: true,
});
return data;
},
};
return mutationOptions;
};
/**
* Delete a volume
*/

View File

@@ -6,6 +6,8 @@ import type {
ListVolumesResponses,
CreateVolumeData,
CreateVolumeResponses,
TestConnectionData,
TestConnectionResponses,
DeleteVolumeData,
DeleteVolumeResponses,
} from "./types.gen";
@@ -54,6 +56,22 @@ export const createVolume = <ThrowOnError extends boolean = false>(
});
};
/**
* Test connection to backend
*/
export const testConnection = <ThrowOnError extends boolean = false>(
options?: Options<TestConnectionData, ThrowOnError>,
) => {
return (options?.client ?? _heyApiClient).post<TestConnectionResponses, unknown, ThrowOnError>({
url: "/api/v1/volumes/test-connection",
...options,
headers: {
"Content-Type": "application/json",
...options?.headers,
},
});
};
/**
* Delete a volume
*/

View File

@@ -33,7 +33,7 @@ export type CreateVolumeData = {
exportPath: string;
port: number;
server: string;
version: string;
version: "3" | "4" | "4.1";
}
| {
backend: "smb";
@@ -61,6 +61,40 @@ export type CreateVolumeResponses = {
export type CreateVolumeResponse = CreateVolumeResponses[keyof CreateVolumeResponses];
export type TestConnectionData = {
body?: {
config:
| {
backend: "directory";
}
| {
backend: "nfs";
exportPath: string;
port: number;
server: string;
version: "3" | "4" | "4.1";
}
| {
backend: "smb";
};
};
path?: never;
query?: never;
url: "/api/v1/volumes/test-connection";
};
export type TestConnectionResponses = {
/**
* Connection test result
*/
200: {
message: string;
success: boolean;
};
};
export type TestConnectionResponse = TestConnectionResponses[keyof TestConnectionResponses];
export type DeleteVolumeData = {
body?: never;
path: {

View File

@@ -7,6 +7,8 @@ import {
deleteVolumeDto,
type ListVolumesResponseDto,
listVolumesDto,
testConnectionBody,
testConnectionDto,
} from "./volume.dto";
import { volumeService } from "./volume.service";
@@ -35,6 +37,12 @@ export const volumeController = new Hono()
return c.json({ message: "Volume created", volume: res.volume });
})
.post("/test-connection", testConnectionDto, validator("json", testConnectionBody), async (c) => {
const body = c.req.valid("json");
const result = await volumeService.testConnection(body.config);
return c.json(result, 200);
})
.delete("/:name", deleteVolumeDto, async (c) => {
const { name } = c.req.param();
const res = await volumeService.deleteVolume(name);

View File

@@ -89,3 +89,32 @@ export const deleteVolumeDto = describeRoute({
},
},
});
/**
* Test connection
*/
export const testConnectionBody = type({
config: volumeConfigSchema,
});
export const testConnectionResponse = type({
success: "boolean",
message: "string",
});
export const testConnectionDto = describeRoute({
description: "Test connection to backend",
operationId: "testConnection",
validateResponse: true,
tags: ["Volumes"],
responses: {
200: {
description: "Connection test result",
content: {
"application/json": {
schema: resolver(testConnectionResponse),
},
},
},
},
});

View File

@@ -1,3 +1,5 @@
import * as fs from "node:fs/promises";
import * as os from "node:os";
import * as path from "node:path";
import type { BackendConfig } from "@ironmount/schemas";
import { eq } from "drizzle-orm";
@@ -33,7 +35,7 @@ const createVolume = async (name: string, backendConfig: BackendConfig) => {
name: slug,
config: backendConfig,
path: path.join(volumePathHost, slug),
type: "nfs",
type: backendConfig.backend,
})
.returning();
@@ -84,9 +86,53 @@ const mountVolume = async (name: string) => {
}
};
const testConnection = async (backendConfig: BackendConfig) => {
let tempDir: string | null = null;
try {
tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "ironmount-test-"));
const mockVolume = {
id: 0,
name: "test-connection",
path: tempDir,
config: backendConfig,
createdAt: new Date(),
updatedAt: new Date(),
type: backendConfig.backend,
};
const backend = createVolumeBackend(mockVolume);
await backend.mount();
await backend.unmount();
return {
success: true,
message: "Connection successful",
};
} catch (error) {
return {
success: false,
message: error instanceof Error ? error.message : "Connection failed",
};
} finally {
if (tempDir) {
try {
await fs.access(tempDir);
await fs.rm(tempDir, { recursive: true, force: true });
} catch (cleanupError) {
// Ignore cleanup errors if directory doesn't exist or can't be removed
console.warn("Failed to cleanup temp directory:", cleanupError);
}
}
}
};
export const volumeService = {
listVolumes,
createVolume,
mountVolume,
deleteVolume,
testConnection,
};

View File

@@ -9,7 +9,7 @@ sync:
- "tmp"
- "logs"
- "mutagen.yml.lock"
- "data/ironmount.db"
- "data"
ironmount:
alpha: "."
beta: "nicolas@192.168.2.42:/home/nicolas/ironmount"