mirror of
https://github.com/nicotsx/ironmount.git
synced 2025-12-10 12:10:51 +01:00
feat: local volume explore file system
This commit is contained in:
@@ -5,10 +5,26 @@ import { toMessage } from "../../../utils/errors";
|
||||
import { logger } from "../../../utils/logger";
|
||||
import type { VolumeBackend } from "../backend";
|
||||
|
||||
const mount = async (_config: BackendConfig, path: string) => {
|
||||
logger.info("Mounting directory volume...", path);
|
||||
await fs.mkdir(path, { recursive: true });
|
||||
return { status: BACKEND_STATUS.mounted };
|
||||
const mount = async (config: BackendConfig, _volumePath: string) => {
|
||||
if (config.backend !== "directory") {
|
||||
return { status: BACKEND_STATUS.error, error: "Invalid backend type" };
|
||||
}
|
||||
|
||||
logger.info("Mounting directory volume from:", config.path);
|
||||
|
||||
try {
|
||||
await fs.access(config.path);
|
||||
const stats = await fs.stat(config.path);
|
||||
|
||||
if (!stats.isDirectory()) {
|
||||
return { status: BACKEND_STATUS.error, error: "Path is not a directory" };
|
||||
}
|
||||
|
||||
return { status: BACKEND_STATUS.mounted };
|
||||
} catch (error) {
|
||||
logger.error("Failed to mount directory volume:", error);
|
||||
return { status: BACKEND_STATUS.error, error: toMessage(error) };
|
||||
}
|
||||
};
|
||||
|
||||
const unmount = async () => {
|
||||
@@ -16,12 +32,16 @@ const unmount = async () => {
|
||||
return { status: BACKEND_STATUS.unmounted };
|
||||
};
|
||||
|
||||
const checkHealth = async (path: string) => {
|
||||
const checkHealth = async (config: BackendConfig) => {
|
||||
if (config.backend !== "directory") {
|
||||
return { status: BACKEND_STATUS.error, error: "Invalid backend type" };
|
||||
}
|
||||
|
||||
try {
|
||||
await fs.access(path);
|
||||
await fs.access(config.path);
|
||||
|
||||
// Try to create a temporary file to ensure write access
|
||||
const tempFilePath = npath.join(path, `.healthcheck-${Date.now()}`);
|
||||
const tempFilePath = npath.join(config.path, `.healthcheck-${Date.now()}`);
|
||||
await fs.writeFile(tempFilePath, "healthcheck");
|
||||
await fs.unlink(tempFilePath);
|
||||
|
||||
@@ -32,8 +52,8 @@ const checkHealth = async (path: string) => {
|
||||
}
|
||||
};
|
||||
|
||||
export const makeDirectoryBackend = (config: BackendConfig, path: string): VolumeBackend => ({
|
||||
mount: () => mount(config, path),
|
||||
export const makeDirectoryBackend = (config: BackendConfig, volumePath: string): VolumeBackend => ({
|
||||
mount: () => mount(config, volumePath),
|
||||
unmount,
|
||||
checkHealth: () => checkHealth(path),
|
||||
checkHealth: () => checkHealth(config),
|
||||
});
|
||||
|
||||
@@ -22,7 +22,7 @@ const mount = async (config: BackendConfig, path: string) => {
|
||||
return { status: BACKEND_STATUS.error, error: "NFS mounting is only supported on Linux hosts." };
|
||||
}
|
||||
|
||||
const { status } = await checkHealth(path, config.readOnly);
|
||||
const { status } = await checkHealth(path, config.readOnly ?? false);
|
||||
if (status === "mounted") {
|
||||
return { status: BACKEND_STATUS.mounted };
|
||||
}
|
||||
@@ -117,5 +117,5 @@ const checkHealth = async (path: string, readOnly: boolean) => {
|
||||
export const makeNfsBackend = (config: BackendConfig, path: string): VolumeBackend => ({
|
||||
mount: () => mount(config, path),
|
||||
unmount: () => unmount(path),
|
||||
checkHealth: () => checkHealth(path, config.readOnly),
|
||||
checkHealth: () => checkHealth(path, config.readOnly ?? false),
|
||||
});
|
||||
|
||||
@@ -22,7 +22,7 @@ const mount = async (config: BackendConfig, path: string) => {
|
||||
return { status: BACKEND_STATUS.error, error: "SMB mounting is only supported on Linux hosts." };
|
||||
}
|
||||
|
||||
const { status } = await checkHealth(path, config.readOnly);
|
||||
const { status } = await checkHealth(path, config.readOnly ?? false);
|
||||
if (status === "mounted") {
|
||||
return { status: BACKEND_STATUS.mounted };
|
||||
}
|
||||
@@ -130,5 +130,5 @@ const checkHealth = async (path: string, readOnly: boolean) => {
|
||||
export const makeSmbBackend = (config: BackendConfig, path: string): VolumeBackend => ({
|
||||
mount: () => mount(config, path),
|
||||
unmount: () => unmount(path),
|
||||
checkHealth: () => checkHealth(path, config.readOnly),
|
||||
checkHealth: () => checkHealth(path, config.readOnly ?? false),
|
||||
});
|
||||
|
||||
@@ -26,7 +26,7 @@ const mount = async (config: BackendConfig, path: string) => {
|
||||
return { status: BACKEND_STATUS.error, error: "WebDAV mounting is only supported on Linux hosts." };
|
||||
}
|
||||
|
||||
const { status } = await checkHealth(path, config.readOnly);
|
||||
const { status } = await checkHealth(path, config.readOnly ?? false);
|
||||
if (status === "mounted") {
|
||||
return { status: BACKEND_STATUS.mounted };
|
||||
}
|
||||
@@ -164,5 +164,5 @@ const checkHealth = async (path: string, readOnly: boolean) => {
|
||||
export const makeWebdavBackend = (config: BackendConfig, path: string): VolumeBackend => ({
|
||||
mount: () => mount(config, path),
|
||||
unmount: () => unmount(path),
|
||||
checkHealth: () => checkHealth(path, config.readOnly),
|
||||
checkHealth: () => checkHealth(path, config.readOnly ?? false),
|
||||
});
|
||||
|
||||
@@ -278,13 +278,12 @@ export const restoreSnapshotDto = describeRoute({
|
||||
export const doctorStepSchema = type({
|
||||
step: "string",
|
||||
success: "boolean",
|
||||
output: "string?",
|
||||
error: "string?",
|
||||
output: "string | null",
|
||||
error: "string | null",
|
||||
});
|
||||
|
||||
export const doctorRepositoryResponse = type({
|
||||
success: "boolean",
|
||||
message: "string",
|
||||
steps: doctorStepSchema.array(),
|
||||
});
|
||||
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
@@ -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),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user