mirror of
https://github.com/nicotsx/ironmount.git
synced 2025-12-10 12:10:51 +01:00
refactor: switch from go to bun
This commit is contained in:
11
apps/server/drizzle.config.ts
Normal file
11
apps/server/drizzle.config.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import "dotenv/config";
|
||||
import { defineConfig } from "drizzle-kit";
|
||||
|
||||
export default defineConfig({
|
||||
out: "./drizzle",
|
||||
schema: "./src/db/schema.ts",
|
||||
dialect: "sqlite",
|
||||
dbCredentials: {
|
||||
url: "ironmount.db",
|
||||
},
|
||||
});
|
||||
7
apps/server/drizzle/0000_known_madelyne_pryor.sql
Normal file
7
apps/server/drizzle/0000_known_madelyne_pryor.sql
Normal file
@@ -0,0 +1,7 @@
|
||||
CREATE TABLE `volumes_table` (
|
||||
`id` integer PRIMARY KEY AUTOINCREMENT NOT NULL,
|
||||
`name` text NOT NULL,
|
||||
`type` text NOT NULL
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE UNIQUE INDEX `volumes_table_name_unique` ON `volumes_table` (`name`);
|
||||
4
apps/server/drizzle/0001_far_frank_castle.sql
Normal file
4
apps/server/drizzle/0001_far_frank_castle.sql
Normal file
@@ -0,0 +1,4 @@
|
||||
ALTER TABLE `volumes_table` ADD `path` text NOT NULL;--> statement-breakpoint
|
||||
ALTER TABLE `volumes_table` ADD `created_at` integer DEFAULT (current_timestamp) NOT NULL;--> statement-breakpoint
|
||||
ALTER TABLE `volumes_table` ADD `updated_at` integer DEFAULT (current_timestamp) NOT NULL;--> statement-breakpoint
|
||||
ALTER TABLE `volumes_table` ADD `config` text NOT NULL;
|
||||
57
apps/server/drizzle/meta/0000_snapshot.json
Normal file
57
apps/server/drizzle/meta/0000_snapshot.json
Normal file
@@ -0,0 +1,57 @@
|
||||
{
|
||||
"version": "6",
|
||||
"dialect": "sqlite",
|
||||
"id": "f4a793d2-279b-4688-bbe3-2b69673fa7fa",
|
||||
"prevId": "00000000-0000-0000-0000-000000000000",
|
||||
"tables": {
|
||||
"volumes_table": {
|
||||
"name": "volumes_table",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "integer",
|
||||
"primaryKey": true,
|
||||
"notNull": true,
|
||||
"autoincrement": true
|
||||
},
|
||||
"name": {
|
||||
"name": "name",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"type": {
|
||||
"name": "type",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
}
|
||||
},
|
||||
"indexes": {
|
||||
"volumes_table_name_unique": {
|
||||
"name": "volumes_table_name_unique",
|
||||
"columns": [
|
||||
"name"
|
||||
],
|
||||
"isUnique": true
|
||||
}
|
||||
},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"checkConstraints": {}
|
||||
}
|
||||
},
|
||||
"views": {},
|
||||
"enums": {},
|
||||
"_meta": {
|
||||
"schemas": {},
|
||||
"tables": {},
|
||||
"columns": {}
|
||||
},
|
||||
"internal": {
|
||||
"indexes": {}
|
||||
}
|
||||
}
|
||||
87
apps/server/drizzle/meta/0001_snapshot.json
Normal file
87
apps/server/drizzle/meta/0001_snapshot.json
Normal file
@@ -0,0 +1,87 @@
|
||||
{
|
||||
"version": "6",
|
||||
"dialect": "sqlite",
|
||||
"id": "004e25a0-ecda-4b1a-aeab-46c8f78d5275",
|
||||
"prevId": "f4a793d2-279b-4688-bbe3-2b69673fa7fa",
|
||||
"tables": {
|
||||
"volumes_table": {
|
||||
"name": "volumes_table",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "integer",
|
||||
"primaryKey": true,
|
||||
"notNull": true,
|
||||
"autoincrement": true
|
||||
},
|
||||
"name": {
|
||||
"name": "name",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"path": {
|
||||
"name": "path",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"type": {
|
||||
"name": "type",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": "(current_timestamp)"
|
||||
},
|
||||
"updated_at": {
|
||||
"name": "updated_at",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": "(current_timestamp)"
|
||||
},
|
||||
"config": {
|
||||
"name": "config",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
}
|
||||
},
|
||||
"indexes": {
|
||||
"volumes_table_name_unique": {
|
||||
"name": "volumes_table_name_unique",
|
||||
"columns": [
|
||||
"name"
|
||||
],
|
||||
"isUnique": true
|
||||
}
|
||||
},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"checkConstraints": {}
|
||||
}
|
||||
},
|
||||
"views": {},
|
||||
"enums": {},
|
||||
"_meta": {
|
||||
"schemas": {},
|
||||
"tables": {},
|
||||
"columns": {}
|
||||
},
|
||||
"internal": {
|
||||
"indexes": {}
|
||||
}
|
||||
}
|
||||
20
apps/server/drizzle/meta/_journal.json
Normal file
20
apps/server/drizzle/meta/_journal.json
Normal file
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"version": "7",
|
||||
"dialect": "sqlite",
|
||||
"entries": [
|
||||
{
|
||||
"idx": 0,
|
||||
"version": "6",
|
||||
"when": 1755765658194,
|
||||
"tag": "0000_known_madelyne_pryor",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 1,
|
||||
"version": "6",
|
||||
"when": 1755775437391,
|
||||
"tag": "0001_far_frank_castle",
|
||||
"breakpoints": true
|
||||
}
|
||||
]
|
||||
}
|
||||
21
apps/server/package.json
Normal file
21
apps/server/package.json
Normal file
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"name": "@ironmount/server",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "bun run --hot src/index.ts",
|
||||
"gen:migrations": "drizzle-kit generate"
|
||||
},
|
||||
"dependencies": {
|
||||
"@hono/arktype-validator": "^2.0.1",
|
||||
"@scalar/hono-api-reference": "^0.9.13",
|
||||
"arktype": "^2.1.20",
|
||||
"dotenv": "^17.2.1",
|
||||
"drizzle-orm": "^0.44.4",
|
||||
"hono": "^4.9.2",
|
||||
"hono-openapi": "^0.4.8"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/bun": "^1.2.20",
|
||||
"drizzle-kit": "^0.31.4"
|
||||
}
|
||||
}
|
||||
58
apps/server/src/controllers/driver.controller.ts
Normal file
58
apps/server/src/controllers/driver.controller.ts
Normal file
@@ -0,0 +1,58 @@
|
||||
import { Hono } from "hono";
|
||||
|
||||
export const driverController = new Hono()
|
||||
.post("/VolumeDriver.Capabilities", (c) => {
|
||||
return c.json({
|
||||
Capabilities: {
|
||||
Scope: "global",
|
||||
},
|
||||
});
|
||||
})
|
||||
.post("/Plugin.Activate", (c) => {
|
||||
return c.json({
|
||||
Implements: ["VolumeDriver"],
|
||||
});
|
||||
})
|
||||
.post("/VolumeDriver.Create", (c) => {
|
||||
return c.json({
|
||||
Err: "",
|
||||
});
|
||||
})
|
||||
.post("/VolumeDriver.Remove", (c) => {
|
||||
return c.json({
|
||||
Err: "",
|
||||
});
|
||||
})
|
||||
.post("/VolumeDriver.Mount", (c) => {
|
||||
return c.json({
|
||||
Mountpoint: `/mnt/something`,
|
||||
});
|
||||
})
|
||||
.post("/VolumeDriver.Unmount", (c) => {
|
||||
return c.json({
|
||||
Err: "",
|
||||
});
|
||||
})
|
||||
.post("/VolumeDriver.Path", (c) => {
|
||||
return c.json({
|
||||
Mountpoint: `/mnt/something`,
|
||||
});
|
||||
})
|
||||
.post("/VolumeDriver.Get", (c) => {
|
||||
return c.json({
|
||||
Name: "my-volume",
|
||||
Mountpoint: `/mnt/something`,
|
||||
Status: {},
|
||||
});
|
||||
})
|
||||
.post("/VolumeDriver.List", (c) => {
|
||||
return c.json({
|
||||
Volumes: [
|
||||
{
|
||||
Name: "my-volume",
|
||||
Mountpoint: `/mnt/something`,
|
||||
Status: {},
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
31
apps/server/src/controllers/volume.controller.ts
Normal file
31
apps/server/src/controllers/volume.controller.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
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")}` });
|
||||
});
|
||||
25
apps/server/src/core/config.ts
Normal file
25
apps/server/src/core/config.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import { type } from "arktype";
|
||||
import "dotenv/config";
|
||||
|
||||
const envSchema = type({
|
||||
NODE_ENV: type
|
||||
.enumerated("development", "production", "test")
|
||||
.default("development"),
|
||||
DB_FILE_NAME: "string",
|
||||
}).pipe((s) => ({
|
||||
__prod__: s.NODE_ENV === "production",
|
||||
environment: s.NODE_ENV,
|
||||
dbFileName: s.DB_FILE_NAME,
|
||||
}));
|
||||
|
||||
const parseConfig = (env: unknown) => {
|
||||
const result = envSchema(env);
|
||||
|
||||
if (result instanceof type.errors) {
|
||||
throw new Error(`Invalid environment variables: ${result.toString()}`);
|
||||
}
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
export const config = parseConfig(process.env);
|
||||
14
apps/server/src/db/db.ts
Normal file
14
apps/server/src/db/db.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import "dotenv/config";
|
||||
import { Database } from "bun:sqlite";
|
||||
import { drizzle } from "drizzle-orm/bun-sqlite";
|
||||
import { migrate } from "drizzle-orm/bun-sqlite/migrator";
|
||||
import { config } from "../core/config";
|
||||
import * as schema from "./schema";
|
||||
|
||||
const sqlite = new Database(config.dbFileName);
|
||||
|
||||
export const db = drizzle({ client: sqlite, schema });
|
||||
|
||||
export const runDbMigrations = () => {
|
||||
migrate(db, { migrationsFolder: "./drizzle" });
|
||||
};
|
||||
35
apps/server/src/db/schema.ts
Normal file
35
apps/server/src/db/schema.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
import { type } from "arktype";
|
||||
import { sql } from "drizzle-orm";
|
||||
import { int, sqliteTable, text } from "drizzle-orm/sqlite-core";
|
||||
|
||||
const nfsConfigSchema = type({
|
||||
backend: "'nfs'",
|
||||
server: "string",
|
||||
exportPath: "string",
|
||||
port: "number",
|
||||
version: type.enumerated(["3", "4"]),
|
||||
});
|
||||
|
||||
const smbConfigSchema = type({
|
||||
backend: "'smb'",
|
||||
});
|
||||
|
||||
const directoryConfigSchema = type({
|
||||
backend: "'directory'",
|
||||
});
|
||||
|
||||
const configSchema = nfsConfigSchema
|
||||
.or(smbConfigSchema)
|
||||
.or(directoryConfigSchema);
|
||||
|
||||
export const volumesTable = sqliteTable("volumes_table", {
|
||||
id: int().primaryKey({ autoIncrement: true }),
|
||||
name: text().notNull().unique(),
|
||||
path: text().notNull(),
|
||||
type: text().notNull(),
|
||||
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>()
|
||||
.notNull(),
|
||||
});
|
||||
23
apps/server/src/descriptors/general.descriptors.ts
Normal file
23
apps/server/src/descriptors/general.descriptors.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
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",
|
||||
});
|
||||
26
apps/server/src/descriptors/volume.descriptors.ts
Normal file
26
apps/server/src/descriptors/volume.descriptors.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
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),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
48
apps/server/src/index.ts
Normal file
48
apps/server/src/index.ts
Normal file
@@ -0,0 +1,48 @@
|
||||
import * as fs from "node:fs/promises";
|
||||
import { Hono } from "hono";
|
||||
import { logger } from "hono/logger";
|
||||
import { driverController } from "./controllers/driver.controller";
|
||||
import { volumeController } from "./controllers/volume.controller";
|
||||
import { runDbMigrations } from "./db/db";
|
||||
import {
|
||||
generalDescriptor,
|
||||
scalarDescriptor,
|
||||
} from "./descriptors/general.descriptors";
|
||||
|
||||
const driver = new Hono().use(logger()).route("/", driverController);
|
||||
const app = new Hono()
|
||||
.use(logger())
|
||||
.get("healthcheck", (c) => c.json({ status: "ok" }))
|
||||
.basePath("/api/v1")
|
||||
.route("/volumes", volumeController);
|
||||
|
||||
app.get("/openapi.json", generalDescriptor(app));
|
||||
app.get("/docs", scalarDescriptor);
|
||||
|
||||
app.get("/", (c) => {
|
||||
return c.json({ message: "Welcome to the Ironmount API" });
|
||||
});
|
||||
|
||||
const socketPath = "/run/docker/plugins/ironmount.sock";
|
||||
|
||||
(async () => {
|
||||
await fs.mkdir("/run/docker/plugins", { recursive: true });
|
||||
|
||||
runDbMigrations();
|
||||
|
||||
Bun.serve({
|
||||
unix: socketPath,
|
||||
fetch: driver.fetch,
|
||||
});
|
||||
|
||||
Bun.serve({
|
||||
port: 8080,
|
||||
fetch: app.fetch,
|
||||
});
|
||||
|
||||
console.log(
|
||||
`Server is running at http://localhost:8080 and unix socket at ${socketPath}`,
|
||||
);
|
||||
})();
|
||||
|
||||
export type AppType = typeof app;
|
||||
7
apps/server/tsconfig.json
Normal file
7
apps/server/tsconfig.json
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"strict": true,
|
||||
"jsx": "react-jsx",
|
||||
"jsxImportSource": "hono/jsx"
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user