From 0c0a3b8581e9a4195a771ca438d5a68d15c462cc Mon Sep 17 00:00:00 2001 From: Nicolas Meienberger Date: Tue, 2 Sep 2025 22:52:44 +0200 Subject: [PATCH] feat(server): test mount endpoint --- .dockerignore | 2 + Dockerfile.dev | 1 + .../api-client/@tanstack/react-query.gen.ts | 44 ++++++++++++++++- apps/client/app/api-client/sdk.gen.ts | 18 +++++++ apps/client/app/api-client/types.gen.ts | 36 +++++++++++++- .../src/modules/volumes/volume.controller.ts | 8 ++++ apps/server/src/modules/volumes/volume.dto.ts | 29 +++++++++++ .../src/modules/volumes/volume.service.ts | 48 ++++++++++++++++++- mutagen.yml | 2 +- 9 files changed, 184 insertions(+), 4 deletions(-) diff --git a/.dockerignore b/.dockerignore index ccaa0be..8267df7 100644 --- a/.dockerignore +++ b/.dockerignore @@ -12,3 +12,5 @@ !apps/**/drizzle/** !apps/**/app/** !apps/**/public/** + +!packages/**/src/** diff --git a/Dockerfile.dev b/Dockerfile.dev index b7db1bc..4b5d2e4 100644 --- a/Dockerfile.dev +++ b/Dockerfile.dev @@ -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 diff --git a/apps/client/app/api-client/@tanstack/react-query.gen.ts b/apps/client/app/api-client/@tanstack/react-query.gen.ts index de34603..a0e5d8b 100644 --- a/apps/client/app/api-client/@tanstack/react-query.gen.ts +++ b/apps/client/app/api-client/@tanstack/react-query.gen.ts @@ -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) => + createQueryKey("testConnection", options); + +/** + * Test connection to backend + */ +export const testConnectionOptions = (options?: Options) => { + 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>, +): UseMutationOptions> => { + const mutationOptions: UseMutationOptions> = { + mutationFn: async (localOptions) => { + const { data } = await testConnection({ + ...options, + ...localOptions, + throwOnError: true, + }); + return data; + }, + }; + return mutationOptions; +}; + /** * Delete a volume */ diff --git a/apps/client/app/api-client/sdk.gen.ts b/apps/client/app/api-client/sdk.gen.ts index 8538f4e..7016902 100644 --- a/apps/client/app/api-client/sdk.gen.ts +++ b/apps/client/app/api-client/sdk.gen.ts @@ -6,6 +6,8 @@ import type { ListVolumesResponses, CreateVolumeData, CreateVolumeResponses, + TestConnectionData, + TestConnectionResponses, DeleteVolumeData, DeleteVolumeResponses, } from "./types.gen"; @@ -54,6 +56,22 @@ export const createVolume = ( }); }; +/** + * Test connection to backend + */ +export const testConnection = ( + options?: Options, +) => { + return (options?.client ?? _heyApiClient).post({ + url: "/api/v1/volumes/test-connection", + ...options, + headers: { + "Content-Type": "application/json", + ...options?.headers, + }, + }); +}; + /** * Delete a volume */ diff --git a/apps/client/app/api-client/types.gen.ts b/apps/client/app/api-client/types.gen.ts index e7a9b0d..bd54ca3 100644 --- a/apps/client/app/api-client/types.gen.ts +++ b/apps/client/app/api-client/types.gen.ts @@ -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: { diff --git a/apps/server/src/modules/volumes/volume.controller.ts b/apps/server/src/modules/volumes/volume.controller.ts index f0988eb..129402f 100644 --- a/apps/server/src/modules/volumes/volume.controller.ts +++ b/apps/server/src/modules/volumes/volume.controller.ts @@ -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); diff --git a/apps/server/src/modules/volumes/volume.dto.ts b/apps/server/src/modules/volumes/volume.dto.ts index db145cf..93967bc 100644 --- a/apps/server/src/modules/volumes/volume.dto.ts +++ b/apps/server/src/modules/volumes/volume.dto.ts @@ -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), + }, + }, + }, + }, +}); diff --git a/apps/server/src/modules/volumes/volume.service.ts b/apps/server/src/modules/volumes/volume.service.ts index 5918d9b..e704be8 100644 --- a/apps/server/src/modules/volumes/volume.service.ts +++ b/apps/server/src/modules/volumes/volume.service.ts @@ -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, }; diff --git a/mutagen.yml b/mutagen.yml index f4031b8..149ee16 100644 --- a/mutagen.yml +++ b/mutagen.yml @@ -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"