From 14dadc85e7088a76fc8e53554fc3c73f5b5a0d52 Mon Sep 17 00:00:00 2001 From: Renan Bernordi Date: Sun, 16 Nov 2025 17:47:23 -0300 Subject: [PATCH] new abstract method for volumepath --- app/server/jobs/cleanup-dangling.ts | 12 +++-- app/server/modules/backends/backend.ts | 24 ++++++---- .../backends/directory/directory-backend.ts | 6 ++- .../backends/mariadb/mariadb-backend.ts | 10 ++++- .../modules/backends/mysql/mysql-backend.ts | 10 ++++- .../modules/backends/nfs/nfs-backend.ts | 6 ++- .../backends/postgres/postgres-backend.ts | 14 +++++- .../modules/backends/smb/smb-backend.ts | 6 ++- .../modules/backends/webdav/webdav-backend.ts | 6 ++- app/server/modules/backups/backups.service.ts | 13 ++++-- .../modules/driver/driver.controller.ts | 26 +++++++---- app/server/modules/volumes/helpers.ts | 45 ------------------- .../modules/volumes/volume.controller.ts | 11 +++-- app/server/modules/volumes/volume.service.ts | 7 +-- 14 files changed, 112 insertions(+), 84 deletions(-) delete mode 100644 app/server/modules/volumes/helpers.ts diff --git a/app/server/jobs/cleanup-dangling.ts b/app/server/jobs/cleanup-dangling.ts index a755e59..e8f4ece 100644 --- a/app/server/jobs/cleanup-dangling.ts +++ b/app/server/jobs/cleanup-dangling.ts @@ -3,7 +3,7 @@ import path from "node:path"; import fs from "node:fs/promises"; import { volumeService } from "../modules/volumes/volume.service"; import { readMountInfo } from "../utils/mountinfo"; -import { getVolumePath } from "../modules/volumes/helpers"; +import { createVolumeBackend } from "../modules/backends/backend"; import { logger } from "../utils/logger"; import { executeUnmount } from "../modules/backends/utils/backend-utils"; import { toMessage } from "../utils/errors"; @@ -16,7 +16,10 @@ export class CleanupDanglingMountsJob extends Job { for (const mount of allSystemMounts) { if (mount.mountPoint.includes("ironmount") && mount.mountPoint.endsWith("_data")) { - const matchingVolume = allVolumes.find((v) => getVolumePath(v) === mount.mountPoint); + const matchingVolume = allVolumes.find((v) => { + const backend = createVolumeBackend(v); + return backend.getVolumePath() === mount.mountPoint; + }); if (!matchingVolume) { logger.info(`Found dangling mount at ${mount.mountPoint}, attempting to unmount...`); await executeUnmount(mount.mountPoint).catch((err) => { @@ -36,7 +39,10 @@ export class CleanupDanglingMountsJob extends Job { for (const dir of allIronmountDirs) { const volumePath = `${VOLUME_MOUNT_BASE}/${dir}/_data`; - const matchingVolume = allVolumes.find((v) => getVolumePath(v) === volumePath); + const matchingVolume = allVolumes.find((v) => { + const backend = createVolumeBackend(v); + return backend.getVolumePath() === volumePath; + }); if (!matchingVolume) { const fullPath = path.join(VOLUME_MOUNT_BASE, dir); logger.info(`Found dangling mount directory at ${fullPath}, attempting to remove...`); diff --git a/app/server/modules/backends/backend.ts b/app/server/modules/backends/backend.ts index 88b140a..25267b9 100644 --- a/app/server/modules/backends/backend.ts +++ b/app/server/modules/backends/backend.ts @@ -1,6 +1,6 @@ import type { BackendStatus } from "~/schemas/volumes"; import type { Volume } from "../../db/schema"; -import { getVolumePath } from "../volumes/helpers"; +import { VOLUME_MOUNT_BASE } from "../../core/constants"; import { makeDirectoryBackend } from "./directory/directory-backend"; import { makeNfsBackend } from "./nfs/nfs-backend"; import { makeSmbBackend } from "./smb/smb-backend"; @@ -18,32 +18,38 @@ export type VolumeBackend = { mount: () => Promise; unmount: () => Promise; checkHealth: () => Promise; + getVolumePath: () => string; + isDatabaseBackend: () => boolean; + getDumpPath: () => string | null; + getDumpFilePath: (timestamp: number) => string | null; }; export const createVolumeBackend = (volume: Volume): VolumeBackend => { - const path = getVolumePath(volume); + const path = volume.config.backend === "directory" + ? volume.config.path + : `${VOLUME_MOUNT_BASE}/${volume.name}/_data`; switch (volume.config.backend) { case "nfs": { - return makeNfsBackend(volume.config, path); + return makeNfsBackend(volume.config, volume.name, path); } case "smb": { - return makeSmbBackend(volume.config, path); + return makeSmbBackend(volume.config, volume.name, path); } case "directory": { - return makeDirectoryBackend(volume.config, path); + return makeDirectoryBackend(volume.config, volume.name, path); } case "webdav": { - return makeWebdavBackend(volume.config, path); + return makeWebdavBackend(volume.config, volume.name, path); } case "mariadb": { - return makeMariaDBBackend(volume.config, path); + return makeMariaDBBackend(volume.config, volume.name, path); } case "mysql": { - return makeMySQLBackend(volume.config, path); + return makeMySQLBackend(volume.config, volume.name, path); } case "postgres": { - return makePostgresBackend(volume.config, path); + return makePostgresBackend(volume.config, volume.name, path); } default: { throw new Error(`Unsupported backend type: ${(volume.config as any).backend}`); diff --git a/app/server/modules/backends/directory/directory-backend.ts b/app/server/modules/backends/directory/directory-backend.ts index ce3eae8..db71526 100644 --- a/app/server/modules/backends/directory/directory-backend.ts +++ b/app/server/modules/backends/directory/directory-backend.ts @@ -52,8 +52,12 @@ const checkHealth = async (config: BackendConfig) => { } }; -export const makeDirectoryBackend = (config: BackendConfig, volumePath: string): VolumeBackend => ({ +export const makeDirectoryBackend = (config: BackendConfig, volumeName: string, volumePath: string): VolumeBackend => ({ mount: () => mount(config, volumePath), unmount, checkHealth: () => checkHealth(config), + getVolumePath: () => config.backend === "directory" ? config.path : volumePath, + isDatabaseBackend: () => false, + getDumpPath: () => null, + getDumpFilePath: () => null, }); diff --git a/app/server/modules/backends/mariadb/mariadb-backend.ts b/app/server/modules/backends/mariadb/mariadb-backend.ts index 5546872..ef26699 100644 --- a/app/server/modules/backends/mariadb/mariadb-backend.ts +++ b/app/server/modules/backends/mariadb/mariadb-backend.ts @@ -4,6 +4,7 @@ import { logger } from "../../../utils/logger"; import { testMariaDBConnection } from "../../../utils/database-dump"; import type { VolumeBackend } from "../backend"; import { BACKEND_STATUS, type BackendConfig } from "~/schemas/volumes"; +import { VOLUME_MOUNT_BASE } from "../../../core/constants"; const mount = async (config: BackendConfig, volumePath: string) => { if (config.backend !== "mariadb") { @@ -50,8 +51,15 @@ const checkHealth = async (config: BackendConfig) => { } }; -export const makeMariaDBBackend = (config: BackendConfig, volumePath: string): VolumeBackend => ({ +export const makeMariaDBBackend = (config: BackendConfig, volumeName: string, volumePath: string): VolumeBackend => ({ mount: () => mount(config, volumePath), unmount: () => unmount(volumePath), checkHealth: () => checkHealth(config), + getVolumePath: () => volumePath, + isDatabaseBackend: () => true, + getDumpPath: () => `${VOLUME_MOUNT_BASE}/${volumeName}/dumps`, + getDumpFilePath: (timestamp: number) => { + const dumpDir = `${VOLUME_MOUNT_BASE}/${volumeName}/dumps`; + return `${dumpDir}/${volumeName}-${timestamp}.sql`; + }, }); \ No newline at end of file diff --git a/app/server/modules/backends/mysql/mysql-backend.ts b/app/server/modules/backends/mysql/mysql-backend.ts index e5b24b9..724ac62 100644 --- a/app/server/modules/backends/mysql/mysql-backend.ts +++ b/app/server/modules/backends/mysql/mysql-backend.ts @@ -4,6 +4,7 @@ import { logger } from "../../../utils/logger"; import { testMySQLConnection } from "../../../utils/database-dump"; import type { VolumeBackend } from "../backend"; import { BACKEND_STATUS, type BackendConfig } from "~/schemas/volumes"; +import { VOLUME_MOUNT_BASE } from "../../../core/constants"; const mount = async (config: BackendConfig, volumePath: string) => { if (config.backend !== "mysql") { @@ -50,8 +51,15 @@ const checkHealth = async (config: BackendConfig) => { } }; -export const makeMySQLBackend = (config: BackendConfig, volumePath: string): VolumeBackend => ({ +export const makeMySQLBackend = (config: BackendConfig, volumeName: string, volumePath: string): VolumeBackend => ({ mount: () => mount(config, volumePath), unmount: () => unmount(volumePath), checkHealth: () => checkHealth(config), + getVolumePath: () => volumePath, + isDatabaseBackend: () => true, + getDumpPath: () => `${VOLUME_MOUNT_BASE}/${volumeName}/dumps`, + getDumpFilePath: (timestamp: number) => { + const dumpDir = `${VOLUME_MOUNT_BASE}/${volumeName}/dumps`; + return `${dumpDir}/${volumeName}-${timestamp}.sql`; + }, }); \ No newline at end of file diff --git a/app/server/modules/backends/nfs/nfs-backend.ts b/app/server/modules/backends/nfs/nfs-backend.ts index 248c662..8f8a60e 100644 --- a/app/server/modules/backends/nfs/nfs-backend.ts +++ b/app/server/modules/backends/nfs/nfs-backend.ts @@ -114,8 +114,12 @@ const checkHealth = async (path: string, readOnly: boolean) => { } }; -export const makeNfsBackend = (config: BackendConfig, path: string): VolumeBackend => ({ +export const makeNfsBackend = (config: BackendConfig, volumeName: string, path: string): VolumeBackend => ({ mount: () => mount(config, path), unmount: () => unmount(path), checkHealth: () => checkHealth(path, config.readOnly ?? false), + getVolumePath: () => path, + isDatabaseBackend: () => false, + getDumpPath: () => null, + getDumpFilePath: () => null, }); diff --git a/app/server/modules/backends/postgres/postgres-backend.ts b/app/server/modules/backends/postgres/postgres-backend.ts index 84f556f..8e1e19d 100644 --- a/app/server/modules/backends/postgres/postgres-backend.ts +++ b/app/server/modules/backends/postgres/postgres-backend.ts @@ -4,6 +4,7 @@ import { logger } from "../../../utils/logger"; import { testPostgresConnection } from "../../../utils/database-dump"; import type { VolumeBackend } from "../backend"; import { BACKEND_STATUS, type BackendConfig } from "~/schemas/volumes"; +import { VOLUME_MOUNT_BASE } from "../../../core/constants"; const mount = async (config: BackendConfig, volumePath: string) => { if (config.backend !== "postgres") { @@ -50,8 +51,19 @@ const checkHealth = async (config: BackendConfig) => { } }; -export const makePostgresBackend = (config: BackendConfig, volumePath: string): VolumeBackend => ({ +export const makePostgresBackend = (config: BackendConfig, volumeName: string, volumePath: string): VolumeBackend => ({ mount: () => mount(config, volumePath), unmount: () => unmount(volumePath), checkHealth: () => checkHealth(config), + getVolumePath: () => volumePath, + isDatabaseBackend: () => true, + getDumpPath: () => `${VOLUME_MOUNT_BASE}/${volumeName}/dumps`, + getDumpFilePath: (timestamp: number) => { + const dumpDir = `${VOLUME_MOUNT_BASE}/${volumeName}/dumps`; + const extension = config.backend === "postgres" && + (config as Extract).dumpFormat !== "plain" + ? "dump" + : "sql"; + return `${dumpDir}/${volumeName}-${timestamp}.${extension}`; + }, }); \ No newline at end of file diff --git a/app/server/modules/backends/smb/smb-backend.ts b/app/server/modules/backends/smb/smb-backend.ts index 6da1b95..68f84d4 100644 --- a/app/server/modules/backends/smb/smb-backend.ts +++ b/app/server/modules/backends/smb/smb-backend.ts @@ -127,8 +127,12 @@ const checkHealth = async (path: string, readOnly: boolean) => { } }; -export const makeSmbBackend = (config: BackendConfig, path: string): VolumeBackend => ({ +export const makeSmbBackend = (config: BackendConfig, volumeName: string, path: string): VolumeBackend => ({ mount: () => mount(config, path), unmount: () => unmount(path), checkHealth: () => checkHealth(path, config.readOnly ?? false), + getVolumePath: () => path, + isDatabaseBackend: () => false, + getDumpPath: () => null, + getDumpFilePath: () => null, }); diff --git a/app/server/modules/backends/webdav/webdav-backend.ts b/app/server/modules/backends/webdav/webdav-backend.ts index 03d34ae..0f42cba 100644 --- a/app/server/modules/backends/webdav/webdav-backend.ts +++ b/app/server/modules/backends/webdav/webdav-backend.ts @@ -161,8 +161,12 @@ const checkHealth = async (path: string, readOnly: boolean) => { } }; -export const makeWebdavBackend = (config: BackendConfig, path: string): VolumeBackend => ({ +export const makeWebdavBackend = (config: BackendConfig, volumeName: string, path: string): VolumeBackend => ({ mount: () => mount(config, path), unmount: () => unmount(path), checkHealth: () => checkHealth(path, config.readOnly ?? false), + getVolumePath: () => path, + isDatabaseBackend: () => false, + getDumpPath: () => null, + getDumpFilePath: () => null, }); diff --git a/app/server/modules/backups/backups.service.ts b/app/server/modules/backups/backups.service.ts index e061f57..1515d34 100644 --- a/app/server/modules/backups/backups.service.ts +++ b/app/server/modules/backups/backups.service.ts @@ -7,7 +7,7 @@ import { db } from "../../db/db"; import { backupSchedulesTable, repositoriesTable, volumesTable } from "../../db/schema"; import { restic } from "../../utils/restic"; import { logger } from "../../utils/logger"; -import { getVolumePath, isDatabaseVolume, getDumpFilePath } from "../volumes/helpers"; +import { createVolumeBackend } from "../backends/backend"; import type { CreateBackupScheduleBody, UpdateBackupScheduleBody } from "./backups.dto"; import { toMessage } from "../../utils/errors"; import { serverEvents } from "../../core/events"; @@ -208,15 +208,20 @@ const executeBackup = async (scheduleId: number, manual = false) => { runningBackups.set(scheduleId, abortController); try { + const backend = createVolumeBackend(volume); let backupPath: string; let dumpFilePath: string | null = null; - const isDatabase = isDatabaseVolume(volume); + const isDatabase = backend.isDatabaseBackend(); if (isDatabase) { logger.info(`Creating database dump for volume ${volume.name}`); const timestamp = Date.now(); - dumpFilePath = getDumpFilePath(volume, timestamp); + dumpFilePath = backend.getDumpFilePath(timestamp); + + if (!dumpFilePath) { + throw new Error("Failed to get dump file path for database volume"); + } try { await executeDatabaseDump(volume.config as DatabaseConfig, dumpFilePath); @@ -228,7 +233,7 @@ const executeBackup = async (scheduleId: number, manual = false) => { backupPath = dumpFilePath; } else { - backupPath = getVolumePath(volume); + backupPath = backend.getVolumePath(); } const backupOptions: { diff --git a/app/server/modules/driver/driver.controller.ts b/app/server/modules/driver/driver.controller.ts index c3822aa..0b40b74 100644 --- a/app/server/modules/driver/driver.controller.ts +++ b/app/server/modules/driver/driver.controller.ts @@ -1,6 +1,7 @@ import { Hono } from "hono"; import { volumeService } from "../volumes/volume.service"; -import { getVolumePath } from "../volumes/helpers"; +import { createVolumeBackend } from "../backends/backend"; +import { VOLUME_MOUNT_BASE } from "../../core/constants"; export const driverController = new Hono() .post("/VolumeDriver.Capabilities", (c) => { @@ -31,9 +32,11 @@ export const driverController = new Hono() } const volumeName = body.Name.replace(/^im-/, ""); + const { volume } = await volumeService.getVolume(volumeName); + const backend = createVolumeBackend(volume); return c.json({ - Mountpoint: getVolumePath(volumeName), + Mountpoint: backend.getVolumePath(), }); }) .post("/VolumeDriver.Unmount", (c) => { @@ -49,9 +52,10 @@ export const driverController = new Hono() } const { volume } = await volumeService.getVolume(body.Name.replace(/^im-/, "")); + const backend = createVolumeBackend(volume); return c.json({ - Mountpoint: getVolumePath(volume), + Mountpoint: backend.getVolumePath(), }); }) .post("/VolumeDriver.Get", async (c) => { @@ -62,11 +66,12 @@ export const driverController = new Hono() } const { volume } = await volumeService.getVolume(body.Name.replace(/^im-/, "")); + const backend = createVolumeBackend(volume); return c.json({ Volume: { Name: `im-${volume.name}`, - Mountpoint: getVolumePath(volume), + Mountpoint: backend.getVolumePath(), Status: {}, }, Err: "", @@ -75,11 +80,14 @@ export const driverController = new Hono() .post("/VolumeDriver.List", async (c) => { const volumes = await volumeService.listVolumes(); - const res = volumes.map((volume) => ({ - Name: `im-${volume.name}`, - Mountpoint: getVolumePath(volume), - Status: {}, - })); + const res = volumes.map((volume) => { + const backend = createVolumeBackend(volume); + return { + Name: `im-${volume.name}`, + Mountpoint: backend.getVolumePath(), + Status: {}, + }; + }); return c.json({ Volumes: res, diff --git a/app/server/modules/volumes/helpers.ts b/app/server/modules/volumes/helpers.ts deleted file mode 100644 index 38f2eba..0000000 --- a/app/server/modules/volumes/helpers.ts +++ /dev/null @@ -1,45 +0,0 @@ -import { VOLUME_MOUNT_BASE } from "../../core/constants"; -import type { Volume } from "../../db/schema"; -import type { BackendConfig } from "~/schemas/volumes"; - -export const getVolumePath = (volume: Volume) => { - if (volume.config.backend === "directory") { - return volume.config.path; - } - - return `${VOLUME_MOUNT_BASE}/${volume.name}/_data`; -}; - -/** - * Check if a volume is a database volume - */ -export const isDatabaseVolume = (volume: Volume): boolean => { - return ["mariadb", "mysql", "postgres", "sqlite"].includes(volume.config.backend); -}; - -/** - * Check if a backend config is a database backend - */ -export const isDatabaseBackend = (config: BackendConfig): boolean => { - return ["mariadb", "mysql", "postgres", "sqlite"].includes(config.backend); -}; - -/** - * Get the dump directory path for a database volume - */ -export const getDumpPath = (volume: Volume): string => { - return `${VOLUME_MOUNT_BASE}/${volume.name}/dumps`; -}; - -/** - * Get the dump file path for a database volume backup - */ -export const getDumpFilePath = (volume: Volume, timestamp: number): string => { - const dumpDir = getDumpPath(volume); - const extension = volume.config.backend === "postgres" && - volume.config.backend === "postgres" && - (volume.config as Extract).dumpFormat !== "plain" - ? "dump" - : "sql"; - return `${dumpDir}/${volume.name}-${timestamp}.${extension}`; -}; diff --git a/app/server/modules/volumes/volume.controller.ts b/app/server/modules/volumes/volume.controller.ts index 94c0818..1882d16 100644 --- a/app/server/modules/volumes/volume.controller.ts +++ b/app/server/modules/volumes/volume.controller.ts @@ -25,7 +25,7 @@ import { type BrowseFilesystemDto, } from "./volume.dto"; import { volumeService } from "./volume.service"; -import { getVolumePath } from "./helpers"; +import { createVolumeBackend } from "../backends/backend"; export const volumeController = new Hono() .get("/", listVolumesDto, async (c) => { @@ -37,9 +37,10 @@ export const volumeController = new Hono() const body = c.req.valid("json"); const res = await volumeService.createVolume(body.name, body.config); + const backend = createVolumeBackend(res.volume); const response = { ...res.volume, - path: getVolumePath(res.volume), + path: backend.getVolumePath(), }; return c.json(response, 201); @@ -60,10 +61,11 @@ export const volumeController = new Hono() const { name } = c.req.param(); const res = await volumeService.getVolume(name); + const backend = createVolumeBackend(res.volume); const response = { volume: { ...res.volume, - path: getVolumePath(res.volume), + path: backend.getVolumePath(), }, statfs: { total: res.statfs.total ?? 0, @@ -85,9 +87,10 @@ export const volumeController = new Hono() const body = c.req.valid("json"); const res = await volumeService.updateVolume(name, body); + const backend = createVolumeBackend(res.volume); const response = { ...res.volume, - path: getVolumePath(res.volume), + path: backend.getVolumePath(), }; return c.json(response, 200); diff --git a/app/server/modules/volumes/volume.service.ts b/app/server/modules/volumes/volume.service.ts index 2e0d6fe..2d9efb5 100644 --- a/app/server/modules/volumes/volume.service.ts +++ b/app/server/modules/volumes/volume.service.ts @@ -13,7 +13,6 @@ import { getStatFs, type StatFs } from "../../utils/mountinfo"; import { withTimeout } from "../../utils/timeout"; import { createVolumeBackend } from "../backends/backend"; import type { UpdateVolumeBody } from "./volume.dto"; -import { getVolumePath } from "./helpers"; import { logger } from "../../utils/logger"; import { serverEvents } from "../../core/events"; import type { BackendConfig } from "~/schemas/volumes"; @@ -129,7 +128,8 @@ const getVolume = async (name: string) => { let statfs: Partial = {}; if (volume.status === "mounted") { - statfs = await withTimeout(getStatFs(getVolumePath(volume)), 1000, "getStatFs").catch((error) => { + const backend = createVolumeBackend(volume); + statfs = await withTimeout(getStatFs(backend.getVolumePath()), 1000, "getStatFs").catch((error) => { logger.warn(`Failed to get statfs for volume ${name}: ${toMessage(error)}`); return {}; }); @@ -296,7 +296,8 @@ const listFiles = async (name: string, subPath?: string) => { } // For directory volumes, use the configured path directly - const volumePath = getVolumePath(volume); + const backend = createVolumeBackend(volume); + const volumePath = backend.getVolumePath(); const requestedPath = subPath ? path.join(volumePath, subPath) : volumePath;