refactor: unify backend and frontend servers (#3)

* refactor: unify backend and frontend servers

* refactor: correct paths for openapi & drizzle

* refactor: move api-client to client

* fix: drizzle paths

* chore: fix linting issues

* fix: form reset issue
This commit is contained in:
Nico
2025-11-13 20:11:46 +01:00
committed by GitHub
parent 8d7e50508d
commit 95a0d44b45
240 changed files with 5171 additions and 5875 deletions

View File

@@ -0,0 +1,73 @@
import * as fs from "node:fs/promises";
import Docker from "dockerode";
import { logger } from "../utils/logger";
export type SystemCapabilities = {
docker: boolean;
rclone: boolean;
};
let capabilitiesPromise: Promise<SystemCapabilities> | null = null;
/**
* Returns the current system capabilities.
* On first call, detects all capabilities and caches the promise.
* Subsequent calls return the same cached promise, ensuring detection only happens once.
*/
export async function getCapabilities(): Promise<SystemCapabilities> {
if (capabilitiesPromise === null) {
// Start detection and cache the promise
capabilitiesPromise = detectCapabilities();
}
return capabilitiesPromise;
}
/**
* Detects which optional capabilities are available in the current environment
*/
async function detectCapabilities(): Promise<SystemCapabilities> {
return {
docker: await detectDocker(),
rclone: await detectRclone(),
};
}
/**
* Checks if Docker is available by:
* 1. Checking if /var/run/docker.sock exists and is accessible
* 2. Attempting to ping the Docker daemon
*/
async function detectDocker(): Promise<boolean> {
try {
await fs.access("/var/run/docker.sock");
const docker = new Docker();
await docker.ping();
logger.info("Docker capability: enabled");
return true;
} catch (_) {
logger.warn(
"Docker capability: disabled. " +
"To enable: mount /var/run/docker.sock and /run/docker/plugins in docker-compose.yml",
);
return false;
}
}
/**
* Checks if rclone is available by:
* 1. Checking if /root/.config/rclone directory exists and is accessible
*/
async function detectRclone(): Promise<boolean> {
try {
await fs.access("/root/.config/rclone");
logger.info("rclone capability: enabled");
return true;
} catch (_) {
logger.warn("rclone capability: disabled. " + "To enable: mount /root/.config/rclone in docker-compose.yml");
return false;
}
}

23
app/server/core/config.ts Normal file
View File

@@ -0,0 +1,23 @@
import { type } from "arktype";
import "dotenv/config";
const envSchema = type({
NODE_ENV: type.enumerated("development", "production", "test").default("development"),
SESSION_SECRET: "string?",
}).pipe((s) => ({
__prod__: s.NODE_ENV === "production",
environment: s.NODE_ENV,
sessionSecret: s.SESSION_SECRET || "change-me-in-production-please",
}));
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);

View File

@@ -0,0 +1,6 @@
export const OPERATION_TIMEOUT = 5000;
export const VOLUME_MOUNT_BASE = "/var/lib/ironmount/volumes";
export const REPOSITORY_BASE = "/var/lib/ironmount/repositories";
export const DATABASE_URL = "/var/lib/ironmount/data/ironmount.db";
export const RESTIC_PASS_FILE = "/var/lib/ironmount/data/restic.pass";
export const SOCKET_PATH = "/run/docker/plugins/ironmount.sock";

37
app/server/core/events.ts Normal file
View File

@@ -0,0 +1,37 @@
import { EventEmitter } from "node:events";
import type { TypedEmitter } from "tiny-typed-emitter";
/**
* Event payloads for the SSE system
*/
interface ServerEvents {
"backup:started": (data: { scheduleId: number; volumeName: string; repositoryName: string }) => void;
"backup:progress": (data: {
scheduleId: number;
volumeName: string;
repositoryName: string;
seconds_elapsed: number;
percent_done: number;
total_files: number;
files_done: number;
total_bytes: number;
bytes_done: number;
current_files: string[];
}) => void;
"backup:completed": (data: {
scheduleId: number;
volumeName: string;
repositoryName: string;
status: "success" | "error" | "stopped";
}) => void;
"volume:mounted": (data: { volumeName: string }) => void;
"volume:unmounted": (data: { volumeName: string }) => void;
"volume:updated": (data: { volumeName: string }) => void;
"volume:status_changed": (data: { volumeName: string; status: string }) => void;
}
/**
* Global event emitter for server-side events
* Use this to emit events that should be broadcasted to connected clients via SSE
*/
export const serverEvents = new EventEmitter() as TypedEmitter<ServerEvents>;

View File

@@ -0,0 +1,44 @@
import cron, { type ScheduledTask } from "node-cron";
import { logger } from "../utils/logger";
export abstract class Job {
abstract run(): Promise<unknown>;
}
type JobConstructor = new () => Job;
class SchedulerClass {
private tasks: ScheduledTask[] = [];
async start() {
logger.info("Scheduler started");
}
build(JobClass: JobConstructor) {
const job = new JobClass();
return {
schedule: (cronExpression: string) => {
const task = cron.schedule(cronExpression, async () => {
try {
await job.run();
} catch (error) {
logger.error(`Job ${JobClass.name} failed:`, error);
}
});
this.tasks.push(task);
logger.info(`Scheduled job ${JobClass.name} with cron: ${cronExpression}`);
},
};
}
async stop() {
for (const task of this.tasks) {
task.stop();
}
this.tasks = [];
logger.info("Scheduler stopped");
}
}
export const Scheduler = new SchedulerClass();