new abstract method for volumepath

This commit is contained in:
Renan Bernordi
2025-11-16 17:47:23 -03:00
parent ff16c6914d
commit 14dadc85e7
14 changed files with 112 additions and 84 deletions

View File

@@ -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...`);

View File

@@ -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<OperationResult>;
unmount: () => Promise<OperationResult>;
checkHealth: () => Promise<OperationResult>;
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}`);

View File

@@ -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,
});

View File

@@ -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`;
},
});

View File

@@ -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`;
},
});

View File

@@ -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,
});

View File

@@ -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<BackendConfig, { backend: "postgres" }>).dumpFormat !== "plain"
? "dump"
: "sql";
return `${dumpDir}/${volumeName}-${timestamp}.${extension}`;
},
});

View File

@@ -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,
});

View File

@@ -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,
});

View File

@@ -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: {

View File

@@ -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,

View File

@@ -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<BackendConfig, { backend: "postgres" }>).dumpFormat !== "plain"
? "dump"
: "sql";
return `${dumpDir}/${volume.name}-${timestamp}.${extension}`;
};

View File

@@ -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<CreateVolumeDto>(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<UpdateVolumeDto>(response, 200);

View File

@@ -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<StatFs> = {};
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;