mirror of
https://github.com/nicotsx/ironmount.git
synced 2025-12-10 12:10:51 +01:00
chore: re-order backend file structure
This commit is contained in:
@@ -12,7 +12,9 @@
|
||||
"dotenv": "^17.2.1",
|
||||
"drizzle-orm": "^0.44.4",
|
||||
"hono": "^4.9.2",
|
||||
"hono-openapi": "^0.4.8"
|
||||
"hono-openapi": "^0.4.8",
|
||||
"http-errors-enhanced": "^3.0.2",
|
||||
"slugify": "^1.6.6"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/bun": "^1.2.20",
|
||||
|
||||
@@ -1,31 +0,0 @@
|
||||
import { type } from "arktype";
|
||||
import { Hono } from "hono";
|
||||
import {
|
||||
listVolumesDescriptor,
|
||||
listVolumesResponse,
|
||||
} from "../descriptors/volume.descriptors";
|
||||
|
||||
export const volumeController = new Hono()
|
||||
.get("/", listVolumesDescriptor, (c) => {
|
||||
const res = listVolumesResponse({
|
||||
volumes: [],
|
||||
});
|
||||
|
||||
if (res instanceof type.errors) {
|
||||
return c.json({ error: "Invalid response format" }, 500);
|
||||
}
|
||||
|
||||
return c.json(res, 200);
|
||||
})
|
||||
.post("/", (c) => {
|
||||
return c.json({ message: "Create a new volume" }, 201);
|
||||
})
|
||||
.get("/:name", (c) => {
|
||||
return c.json({ message: `Details of volume ${c.req.param("name")}` });
|
||||
})
|
||||
.put("/:name", (c) => {
|
||||
return c.json({ message: `Update volume ${c.req.param("name")}` });
|
||||
})
|
||||
.delete("/:name", (c) => {
|
||||
return c.json({ message: `Delete volume ${c.req.param("name")}` });
|
||||
});
|
||||
@@ -5,11 +5,13 @@ const envSchema = type({
|
||||
NODE_ENV: type
|
||||
.enumerated("development", "production", "test")
|
||||
.default("development"),
|
||||
DB_FILE_NAME: "string",
|
||||
VOLUME_ROOT: "string",
|
||||
}).pipe((s) => ({
|
||||
__prod__: s.NODE_ENV === "production",
|
||||
environment: s.NODE_ENV,
|
||||
dbFileName: s.DB_FILE_NAME,
|
||||
dbFileName: "/data/ironmount.db",
|
||||
volumeRootHost: s.VOLUME_ROOT,
|
||||
volumeRootContainer: "/mnt/volumes",
|
||||
}));
|
||||
|
||||
const parseConfig = (env: unknown) => {
|
||||
|
||||
@@ -2,12 +2,19 @@ import { type } from "arktype";
|
||||
import { sql } from "drizzle-orm";
|
||||
import { int, sqliteTable, text } from "drizzle-orm/sqlite-core";
|
||||
|
||||
const BACKEND_TYPES = {
|
||||
nfs: "nfs",
|
||||
smb: "smb",
|
||||
directory: "directory",
|
||||
};
|
||||
export type BackendType = keyof typeof BACKEND_TYPES;
|
||||
|
||||
const nfsConfigSchema = type({
|
||||
backend: "'nfs'",
|
||||
server: "string",
|
||||
exportPath: "string",
|
||||
port: "number",
|
||||
version: type.enumerated(["3", "4"]),
|
||||
version: "string", // Shold be an enum: "3" | "4" | "4.1"
|
||||
});
|
||||
|
||||
const smbConfigSchema = type({
|
||||
@@ -18,10 +25,12 @@ const directoryConfigSchema = type({
|
||||
backend: "'directory'",
|
||||
});
|
||||
|
||||
const configSchema = nfsConfigSchema
|
||||
export const volumeConfigSchema = nfsConfigSchema
|
||||
.or(smbConfigSchema)
|
||||
.or(directoryConfigSchema);
|
||||
|
||||
export type BackendConfig = typeof volumeConfigSchema.infer;
|
||||
|
||||
export const volumesTable = sqliteTable("volumes_table", {
|
||||
id: int().primaryKey({ autoIncrement: true }),
|
||||
name: text().notNull().unique(),
|
||||
@@ -30,6 +39,6 @@ export const volumesTable = sqliteTable("volumes_table", {
|
||||
createdAt: int("created_at").notNull().default(sql`(current_timestamp)`),
|
||||
updatedAt: int("updated_at").notNull().default(sql`(current_timestamp)`),
|
||||
config: text("config", { mode: "json" })
|
||||
.$type<typeof configSchema.inferOut>()
|
||||
.$type<typeof volumeConfigSchema.inferOut>()
|
||||
.notNull(),
|
||||
});
|
||||
|
||||
@@ -1,23 +0,0 @@
|
||||
import { Scalar } from "@scalar/hono-api-reference";
|
||||
import type { Hono } from "hono";
|
||||
import { openAPISpecs } from "hono-openapi";
|
||||
|
||||
export const generalDescriptor = (app: Hono) =>
|
||||
openAPISpecs(app, {
|
||||
documentation: {
|
||||
info: {
|
||||
title: "Ironmount API",
|
||||
version: "1.0.0",
|
||||
description: "API for managing Docker volumes",
|
||||
},
|
||||
servers: [
|
||||
{ url: "http://localhost:3000", description: "Development Server" },
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
export const scalarDescriptor = Scalar({
|
||||
title: "Ironmount API Docs",
|
||||
pageTitle: "Ironmount API Docs",
|
||||
url: "/api/v1/openapi.json",
|
||||
});
|
||||
@@ -1,26 +0,0 @@
|
||||
import { type } from "arktype";
|
||||
import { describeRoute } from "hono-openapi";
|
||||
import { resolver } from "hono-openapi/arktype";
|
||||
|
||||
export const listVolumesResponse = type({
|
||||
volumes: type({
|
||||
name: "string",
|
||||
mountpoint: "string",
|
||||
createdAt: "string",
|
||||
}).array(),
|
||||
});
|
||||
|
||||
export const listVolumesDescriptor = describeRoute({
|
||||
description: "List all volumes",
|
||||
tags: ["Volumes"],
|
||||
responses: {
|
||||
200: {
|
||||
description: "A list of volumes",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: resolver(listVolumesResponse),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
@@ -1,13 +1,31 @@
|
||||
import * as fs from "node:fs/promises";
|
||||
import { Scalar } from "@scalar/hono-api-reference";
|
||||
import { Hono } from "hono";
|
||||
import { logger } from "hono/logger";
|
||||
import { driverController } from "./controllers/driver.controller";
|
||||
import { volumeController } from "./controllers/volume.controller";
|
||||
import { openAPISpecs } from "hono-openapi";
|
||||
import { runDbMigrations } from "./db/db";
|
||||
import {
|
||||
generalDescriptor,
|
||||
scalarDescriptor,
|
||||
} from "./descriptors/general.descriptors";
|
||||
import { driverController } from "./modules/driver/driver.controller";
|
||||
import { volumeController } from "./modules/volumes/volume.controller";
|
||||
|
||||
export const generalDescriptor = (app: Hono) =>
|
||||
openAPISpecs(app, {
|
||||
documentation: {
|
||||
info: {
|
||||
title: "Ironmount API",
|
||||
version: "1.0.0",
|
||||
description: "API for managing Docker volumes",
|
||||
},
|
||||
servers: [
|
||||
{ url: "http://localhost:3000", description: "Development Server" },
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
export const scalarDescriptor = Scalar({
|
||||
title: "Ironmount API Docs",
|
||||
pageTitle: "Ironmount API Docs",
|
||||
url: "/api/v1/openapi.json",
|
||||
});
|
||||
|
||||
const driver = new Hono().use(logger()).route("/", driverController);
|
||||
const app = new Hono()
|
||||
|
||||
50
apps/server/src/modules/volumes/volume.controller.ts
Normal file
50
apps/server/src/modules/volumes/volume.controller.ts
Normal file
@@ -0,0 +1,50 @@
|
||||
import { Hono } from "hono";
|
||||
import { validator } from "hono-openapi/arktype";
|
||||
import { handleServiceError } from "../../utils/errors";
|
||||
import {
|
||||
createVolumeBody,
|
||||
createVolumeDto,
|
||||
type ListVolumesResponseDto,
|
||||
listVolumesDto,
|
||||
} from "./volume.dto";
|
||||
import { volumeService } from "./volume.service";
|
||||
|
||||
export const volumeController = new Hono()
|
||||
.get("/", listVolumesDto, async (c) => {
|
||||
const volumes = await volumeService.listVolumes();
|
||||
|
||||
const response = {
|
||||
volumes: volumes.map((volume) => ({
|
||||
name: volume.name,
|
||||
mountpoint: volume.path,
|
||||
createdAt: volume.createdAt,
|
||||
})),
|
||||
} satisfies ListVolumesResponseDto;
|
||||
|
||||
return c.json(response, 200);
|
||||
})
|
||||
.post(
|
||||
"/",
|
||||
createVolumeDto,
|
||||
validator("json", createVolumeBody),
|
||||
async (c) => {
|
||||
const body = c.req.valid("json");
|
||||
const res = await volumeService.createVolume(body.name, body.config);
|
||||
|
||||
if (res.error) {
|
||||
const { message, status } = handleServiceError(res.error);
|
||||
return c.json(message, status);
|
||||
}
|
||||
|
||||
return c.json({ message: "Volume created", volume: res.volume });
|
||||
},
|
||||
)
|
||||
.get("/:name", (c) => {
|
||||
return c.json({ message: `Details of volume ${c.req.param("name")}` });
|
||||
})
|
||||
.put("/:name", (c) => {
|
||||
return c.json({ message: `Update volume ${c.req.param("name")}` });
|
||||
})
|
||||
.delete("/:name", (c) => {
|
||||
return c.json({ message: `Delete volume ${c.req.param("name")}` });
|
||||
});
|
||||
60
apps/server/src/modules/volumes/volume.dto.ts
Normal file
60
apps/server/src/modules/volumes/volume.dto.ts
Normal file
@@ -0,0 +1,60 @@
|
||||
import { type } from "arktype";
|
||||
import { describeRoute } from "hono-openapi";
|
||||
import { resolver } from "hono-openapi/arktype";
|
||||
import { volumeConfigSchema } from "../../db/schema";
|
||||
|
||||
/**
|
||||
* List all volumes
|
||||
*/
|
||||
export const listVolumesResponse = type({
|
||||
volumes: type({
|
||||
name: "string",
|
||||
mountpoint: "string",
|
||||
createdAt: "number",
|
||||
}).array(),
|
||||
});
|
||||
export type ListVolumesResponseDto = typeof listVolumesResponse.infer;
|
||||
|
||||
export const listVolumesDto = describeRoute({
|
||||
description: "List all volumes",
|
||||
tags: ["Volumes"],
|
||||
responses: {
|
||||
200: {
|
||||
description: "A list of volumes",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: resolver(listVolumesResponse),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* Create a new volume
|
||||
*/
|
||||
export const createVolumeBody = type({
|
||||
name: "string",
|
||||
config: volumeConfigSchema,
|
||||
});
|
||||
|
||||
export const createVolumeResponse = type({
|
||||
name: "string",
|
||||
mountpoint: "string",
|
||||
createdAt: "number",
|
||||
});
|
||||
|
||||
export const createVolumeDto = describeRoute({
|
||||
description: "Create a new volume",
|
||||
tags: ["Volumes"],
|
||||
responses: {
|
||||
201: {
|
||||
description: "Volume created successfully",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: resolver(createVolumeResponse),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
44
apps/server/src/modules/volumes/volume.service.ts
Normal file
44
apps/server/src/modules/volumes/volume.service.ts
Normal file
@@ -0,0 +1,44 @@
|
||||
import * as path from "node:path";
|
||||
import { eq } from "drizzle-orm";
|
||||
import { ConflictError } from "http-errors-enhanced";
|
||||
import slugify from "slugify";
|
||||
import { config } from "../../core/config";
|
||||
import { db } from "../../db/db";
|
||||
import { type BackendConfig, volumesTable } from "../../db/schema";
|
||||
|
||||
const listVolumes = async () => {
|
||||
const volumes = await db.query.volumesTable.findMany({});
|
||||
|
||||
return volumes;
|
||||
};
|
||||
|
||||
const createVolume = async (name: string, backendConfig: BackendConfig) => {
|
||||
const slug = slugify(name, { lower: true, strict: true });
|
||||
|
||||
const existing = await db.query.volumesTable.findFirst({
|
||||
where: eq(volumesTable.name, slug),
|
||||
});
|
||||
|
||||
if (existing) {
|
||||
return { error: new ConflictError("Volume already exists") };
|
||||
}
|
||||
|
||||
const volumePathHost = path.join(config.volumeRootHost);
|
||||
|
||||
const val = await db
|
||||
.insert(volumesTable)
|
||||
.values({
|
||||
name: slug,
|
||||
config: backendConfig,
|
||||
path: path.join(volumePathHost, slug),
|
||||
type: "nfs",
|
||||
})
|
||||
.returning();
|
||||
|
||||
return { volume: val[0], status: 201 };
|
||||
};
|
||||
|
||||
export const volumeService = {
|
||||
listVolumes,
|
||||
createVolume,
|
||||
};
|
||||
8
apps/server/src/utils/errors.ts
Normal file
8
apps/server/src/utils/errors.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
import { ConflictError } from "http-errors-enhanced";
|
||||
|
||||
export const handleServiceError = (error: unknown) => {
|
||||
if (error instanceof ConflictError) {
|
||||
return { message: error.message, status: 409 as const };
|
||||
}
|
||||
return { message: "Internal Server Error", status: 500 as const };
|
||||
};
|
||||
Reference in New Issue
Block a user