diff --git a/app/client/api-client/@tanstack/react-query.gen.ts b/app/client/api-client/@tanstack/react-query.gen.ts index 2120ef7b..f81bfbcb 100644 --- a/app/client/api-client/@tanstack/react-query.gen.ts +++ b/app/client/api-client/@tanstack/react-query.gen.ts @@ -497,7 +497,7 @@ export const listFilesInfiniteOptions = (options: Options) => DefaultError, InfiniteData, QueryKey>, - number | Pick>[0], "body" | "headers" | "path" | "query"> + string | Pick>[0], "body" | "headers" | "path" | "query"> >( // @ts-ignore { @@ -814,7 +814,7 @@ export const listSnapshotFilesInfiniteOptions = (options: Options, QueryKey>, - number | Pick>[0], "body" | "headers" | "path" | "query"> + string | Pick>[0], "body" | "headers" | "path" | "query"> >( // @ts-ignore { diff --git a/app/client/api-client/types.gen.ts b/app/client/api-client/types.gen.ts index 5629575b..74fbae0e 100644 --- a/app/client/api-client/types.gen.ts +++ b/app/client/api-client/types.gen.ts @@ -682,18 +682,9 @@ export type ListFilesData = { id: string; }; query?: { - /** - * Subdirectory path to list (relative to volume root) - */ + limit?: string; + offset?: string; path?: string; - /** - * Offset for pagination (default: 0) - */ - offset?: number; - /** - * Maximum number of files to return (default: 100, max: 1000) - */ - limit?: number; }; url: "/api/v1/volumes/{id}/files"; }; @@ -1736,15 +1727,9 @@ export type ListSnapshotFilesData = { snapshotId: string; }; query?: { + limit?: string; + offset?: string; path?: string; - /** - * Offset for pagination (default: 0) - */ - offset?: number; - /** - * Maximum number of files to return (default: 500, max: 1000) - */ - limit?: number; }; url: "/api/v1/repositories/{id}/snapshots/{snapshotId}/files"; }; diff --git a/app/server/modules/repositories/repositories.controller.ts b/app/server/modules/repositories/repositories.controller.ts index b697eb55..ba562d02 100644 --- a/app/server/modules/repositories/repositories.controller.ts +++ b/app/server/modules/repositories/repositories.controller.ts @@ -143,11 +143,10 @@ export const repositoriesController = new Hono() validator("query", listSnapshotFilesQuery), async (c) => { const { id, snapshotId } = c.req.param(); - const { path } = c.req.valid("query"); + const { path, ...query } = c.req.valid("query"); const decodedPath = path ? decodeURIComponent(path) : undefined; - const query = c.req.query(); const offset = Math.max(0, Number.parseInt(query.offset ?? "0", 10) || 0); const limit = Math.min(1000, Math.max(1, Number.parseInt(query.limit ?? "500", 10) || 500)); diff --git a/app/server/modules/repositories/repositories.dto.ts b/app/server/modules/repositories/repositories.dto.ts index d62a0844..485db506 100644 --- a/app/server/modules/repositories/repositories.dto.ts +++ b/app/server/modules/repositories/repositories.dto.ts @@ -262,43 +262,14 @@ export type ListSnapshotFilesDto = typeof listSnapshotFilesResponse.infer; export const listSnapshotFilesQuery = type({ path: "string?", + offset: "string.integer?", + limit: "string.integer?", }); export const listSnapshotFilesDto = describeRoute({ description: "List files and directories in a snapshot", tags: ["Repositories"], operationId: "listSnapshotFiles", - parameters: [ - { - in: "query", - name: "path", - required: false, - schema: { - type: "string", - }, - description: "Subdirectory path to list", - }, - { - in: "query", - name: "offset", - required: false, - schema: { - type: "integer", - default: 0, - }, - description: "Offset for pagination (default: 0)", - }, - { - in: "query", - name: "limit", - required: false, - schema: { - type: "integer", - default: 500, - }, - description: "Maximum number of files to return (default: 500, max: 1000)", - }, - ], responses: { 200: { description: "List of files and directories in the snapshot", diff --git a/app/server/modules/volumes/volume.controller.ts b/app/server/modules/volumes/volume.controller.ts index b6acb95f..7df2ed16 100644 --- a/app/server/modules/volumes/volume.controller.ts +++ b/app/server/modules/volumes/volume.controller.ts @@ -21,6 +21,7 @@ import { type ListFilesDto, browseFilesystemDto, type BrowseFilesystemDto, + listFilesQuery, } from "./volume.dto"; import { volumeService } from "./volume.service"; import { getVolumePath } from "./helpers"; @@ -104,15 +105,14 @@ export const volumeController = new Hono() return c.json({ error, status }, 200); }) - .get("/:id/files", listFilesDto, async (c) => { + .get("/:id/files", validator("query", listFilesQuery), listFilesDto, async (c) => { const { id } = c.req.param(); - const subPath = c.req.query("path"); - const offsetParam = c.req.query("offset"); - const offset = offsetParam ? parseInt(offsetParam, 10) : 0; - const limitParam = c.req.query("limit"); - const limit = limitParam ? parseInt(limitParam, 10) : undefined; + const { path, ...query } = c.req.valid("query"); - const result = await volumeService.listFiles(id, subPath, offset, limit); + const offset = Math.max(0, Number.parseInt(query.offset ?? "0", 10) || 0); + const limit = Math.min(1000, Math.max(1, Number.parseInt(query.limit ?? "500", 10) || 500)); + + const result = await volumeService.listFiles(id, path, offset, limit); const response = { files: result.files, diff --git a/app/server/modules/volumes/volume.dto.ts b/app/server/modules/volumes/volume.dto.ts index 6554f1b5..01214f95 100644 --- a/app/server/modules/volumes/volume.dto.ts +++ b/app/server/modules/volumes/volume.dto.ts @@ -283,41 +283,16 @@ export const listFilesResponse = type({ }); export type ListFilesDto = typeof listFilesResponse.infer; +export const listFilesQuery = type({ + path: "string?", + offset: "string.integer?", + limit: "string.integer?", +}); + export const listFilesDto = describeRoute({ description: "List files in a volume directory", operationId: "listFiles", tags: ["Volumes"], - parameters: [ - { - in: "query", - name: "path", - required: false, - schema: { - type: "string", - }, - description: "Subdirectory path to list (relative to volume root)", - }, - { - in: "query", - name: "offset", - required: false, - schema: { - type: "integer", - default: 0, - }, - description: "Offset for pagination (default: 0)", - }, - { - in: "query", - name: "limit", - required: false, - schema: { - type: "integer", - default: 100, - }, - description: "Maximum number of files to return (default: 100, max: 1000)", - }, - ], responses: { 200: { description: "List of files in the volume", diff --git a/app/server/modules/volumes/volume.service.ts b/app/server/modules/volumes/volume.service.ts index fb561b11..824ac4f5 100644 --- a/app/server/modules/volumes/volume.service.ts +++ b/app/server/modules/volumes/volume.service.ts @@ -18,6 +18,7 @@ import { serverEvents } from "../../core/events"; import { volumeConfigSchema, type BackendConfig } from "~/schemas/volumes"; import { type } from "arktype"; import { getOrganizationId } from "~/server/core/request-context"; +import { isNodeJSErrnoException } from "~/server/utils/fs"; async function encryptSensitiveFields(config: BackendConfig): Promise { switch (config.backend) { @@ -373,7 +374,7 @@ const listFiles = async ( hasMore: startOffset + entries.length < total, }; } catch (error) { - if ((error as NodeJS.ErrnoException).code === "ENOENT") { + if (isNodeJSErrnoException(error) && error.code === "ENOENT") { throw new NotFoundError("Directory not found"); } throw new InternalServerError(`Failed to list files: ${toMessage(error)}`);