feat: local volume explore file system

This commit is contained in:
Nicolas Meienberger
2025-11-08 11:00:18 +01:00
parent 4aeebea5b2
commit 5b4b571581
15 changed files with 409 additions and 24 deletions

View File

@@ -21,6 +21,8 @@ import {
type ListContainersDto,
type UpdateVolumeDto,
type ListFilesDto,
browseFilesystemDto,
type BrowseFilesystemDto,
} from "./volume.dto";
import { volumeService } from "./volume.service";
import { getVolumePath } from "./helpers";
@@ -121,4 +123,15 @@ export const volumeController = new Hono()
c.header("Cache-Control", "public, max-age=10, stale-while-revalidate=60");
return c.json<ListFilesDto>(response, 200);
})
.get("/filesystem/browse", browseFilesystemDto, async (c) => {
const path = c.req.query("path") || "/";
const result = await volumeService.browseFilesystem(path);
const response = {
directories: result.directories,
path: result.path,
};
return c.json<BrowseFilesystemDto>(response, 200);
});

View File

@@ -335,3 +335,39 @@ export const listFilesDto = describeRoute({
},
},
});
/**
* Browse filesystem directories
*/
export const browseFilesystemResponse = type({
directories: fileEntrySchema.array(),
path: "string",
});
export type BrowseFilesystemDto = typeof browseFilesystemResponse.infer;
export const browseFilesystemDto = describeRoute({
description: "Browse directories on the host filesystem",
operationId: "browseFilesystem",
tags: ["Volumes"],
parameters: [
{
in: "query",
name: "path",
required: false,
schema: {
type: "string",
},
description: "Directory path to browse (absolute path, defaults to /)",
},
],
responses: {
200: {
description: "List of directories in the specified path",
content: {
"application/json": {
schema: resolver(browseFilesystemResponse),
},
},
},
},
});

View File

@@ -276,7 +276,9 @@ const listFiles = async (name: string, subPath?: string) => {
throw new InternalServerError("Volume is not mounted");
}
const volumePath = getVolumePath(volume.name);
// For directory volumes, use the configured path directly
const volumePath =
volume.config.backend === "directory" ? volume.config.path : getVolumePath(volume.name);
const requestedPath = subPath ? path.join(volumePath, subPath) : volumePath;
@@ -328,6 +330,48 @@ const listFiles = async (name: string, subPath?: string) => {
}
};
const browseFilesystem = async (browsePath: string) => {
const normalizedPath = path.normalize(browsePath);
try {
const entries = await fs.readdir(normalizedPath, { withFileTypes: true });
const directories = await Promise.all(
entries
.filter((entry) => entry.isDirectory())
.map(async (entry) => {
const fullPath = path.join(normalizedPath, entry.name);
try {
const stats = await fs.stat(fullPath);
return {
name: entry.name,
path: fullPath,
type: "directory" as const,
size: undefined,
modifiedAt: stats.mtimeMs,
};
} catch {
return {
name: entry.name,
path: fullPath,
type: "directory" as const,
size: undefined,
modifiedAt: undefined,
};
}
}),
);
return {
directories: directories.sort((a, b) => a.name.localeCompare(b.name)),
path: normalizedPath,
};
} catch (error) {
throw new InternalServerError(`Failed to browse filesystem: ${toMessage(error)}`);
}
};
export const volumeService = {
listVolumes,
createVolume,
@@ -340,4 +384,5 @@ export const volumeService = {
checkHealth,
getContainersUsingVolume,
listFiles,
browseFilesystem,
};