mirror of
https://github.com/nicotsx/ironmount.git
synced 2025-12-10 12:10:51 +01:00
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:
73
app/server/core/capabilities.ts
Normal file
73
app/server/core/capabilities.ts
Normal 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
23
app/server/core/config.ts
Normal 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);
|
||||
6
app/server/core/constants.ts
Normal file
6
app/server/core/constants.ts
Normal 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
37
app/server/core/events.ts
Normal 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>;
|
||||
44
app/server/core/scheduler.ts
Normal file
44
app/server/core/scheduler.ts
Normal 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();
|
||||
Reference in New Issue
Block a user