refactor: use async local storage for cleaner context sharing
This commit is contained in:
parent
2361d2e062
commit
343c00cb33
20 changed files with 257 additions and 195 deletions
|
|
@ -87,13 +87,7 @@ export default function OnboardingPage() {
|
||||||
} else if (error) {
|
} else if (error) {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
const errorMessage = error.message ?? "Unknown error";
|
const errorMessage = error.message ?? "Unknown error";
|
||||||
if (errorMessage.includes("User registrations are currently disabled")) {
|
toast.error("Failed to create admin user", { description: errorMessage });
|
||||||
toast.error("User registrations are currently disabled", {
|
|
||||||
description: "Please contact an administrator for access.",
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
toast.error("Failed to create admin user", { description: errorMessage });
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
24
app/server/core/request-context.ts
Normal file
24
app/server/core/request-context.ts
Normal file
|
|
@ -0,0 +1,24 @@
|
||||||
|
import { AsyncLocalStorage } from "node:async_hooks";
|
||||||
|
|
||||||
|
export type RequestContext = {
|
||||||
|
organizationId: string;
|
||||||
|
userId?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const requestContextStorage = new AsyncLocalStorage<RequestContext>();
|
||||||
|
|
||||||
|
export const withContext = <T>(context: RequestContext, fn: () => T): T => {
|
||||||
|
return requestContextStorage.run(context, fn);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getRequestContext = (): RequestContext => {
|
||||||
|
const context = requestContextStorage.getStore();
|
||||||
|
|
||||||
|
if (!context?.organizationId) {
|
||||||
|
throw new Error("Organization context is missing");
|
||||||
|
}
|
||||||
|
|
||||||
|
return context;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getOrganizationId = () => getRequestContext().organizationId;
|
||||||
|
|
@ -4,6 +4,7 @@ import { logger } from "../utils/logger";
|
||||||
import { db } from "../db/db";
|
import { db } from "../db/db";
|
||||||
import { eq } from "drizzle-orm";
|
import { eq } from "drizzle-orm";
|
||||||
import { volumesTable } from "../db/schema";
|
import { volumesTable } from "../db/schema";
|
||||||
|
import { withContext } from "../core/request-context";
|
||||||
|
|
||||||
export class VolumeAutoRemountJob extends Job {
|
export class VolumeAutoRemountJob extends Job {
|
||||||
async run() {
|
async run() {
|
||||||
|
|
@ -16,7 +17,9 @@ export class VolumeAutoRemountJob extends Job {
|
||||||
for (const volume of volumes) {
|
for (const volume of volumes) {
|
||||||
if (volume.autoRemount) {
|
if (volume.autoRemount) {
|
||||||
try {
|
try {
|
||||||
await volumeService.mountVolume(volume.id, volume.organizationId);
|
await withContext({ organizationId: volume.organizationId }, async () => {
|
||||||
|
await volumeService.mountVolume(volume.id);
|
||||||
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
logger.error(`Failed to auto-remount volume ${volume.name}:`, err);
|
logger.error(`Failed to auto-remount volume ${volume.name}:`, err);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ import { Job } from "../core/scheduler";
|
||||||
import { backupsService } from "../modules/backups/backups.service";
|
import { backupsService } from "../modules/backups/backups.service";
|
||||||
import { logger } from "../utils/logger";
|
import { logger } from "../utils/logger";
|
||||||
import { db } from "../db/db";
|
import { db } from "../db/db";
|
||||||
|
import { withContext } from "../core/request-context";
|
||||||
|
|
||||||
export class BackupExecutionJob extends Job {
|
export class BackupExecutionJob extends Job {
|
||||||
async run() {
|
async run() {
|
||||||
|
|
@ -12,21 +13,23 @@ export class BackupExecutionJob extends Job {
|
||||||
let totalExecuted = 0;
|
let totalExecuted = 0;
|
||||||
|
|
||||||
for (const org of organizations) {
|
for (const org of organizations) {
|
||||||
const scheduleIds = await backupsService.getSchedulesToExecute(org.id);
|
await withContext({ organizationId: org.id }, async () => {
|
||||||
|
const scheduleIds = await backupsService.getSchedulesToExecute();
|
||||||
|
|
||||||
if (scheduleIds.length === 0) {
|
if (scheduleIds.length === 0) {
|
||||||
continue;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.info(`Found ${scheduleIds.length} backup schedule(s) to execute for organization ${org.name}`);
|
logger.info(`Found ${scheduleIds.length} backup schedule(s) to execute for organization ${org.name}`);
|
||||||
|
|
||||||
for (const scheduleId of scheduleIds) {
|
for (const scheduleId of scheduleIds) {
|
||||||
backupsService.executeBackup(scheduleId, org.id).catch((err) => {
|
backupsService.executeBackup(scheduleId).catch((err) => {
|
||||||
logger.error(`Error executing backup for schedule ${scheduleId}:`, err);
|
logger.error(`Error executing backup for schedule ${scheduleId}:`, err);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
totalExecuted += scheduleIds.length;
|
totalExecuted += scheduleIds.length;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (totalExecuted === 0) {
|
if (totalExecuted === 0) {
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@ import { executeUnmount } from "../modules/backends/utils/backend-utils";
|
||||||
import { toMessage } from "../utils/errors";
|
import { toMessage } from "../utils/errors";
|
||||||
import { VOLUME_MOUNT_BASE } from "../core/constants";
|
import { VOLUME_MOUNT_BASE } from "../core/constants";
|
||||||
import { db } from "../db/db";
|
import { db } from "../db/db";
|
||||||
|
import { withContext } from "../core/request-context";
|
||||||
|
|
||||||
export class CleanupDanglingMountsJob extends Job {
|
export class CleanupDanglingMountsJob extends Job {
|
||||||
async run() {
|
async run() {
|
||||||
|
|
@ -20,7 +21,7 @@ export class CleanupDanglingMountsJob extends Job {
|
||||||
|
|
||||||
const allVolumes = [];
|
const allVolumes = [];
|
||||||
for (const org of organizations) {
|
for (const org of organizations) {
|
||||||
const volumes = await volumeService.listVolumes(org.id);
|
const volumes = await withContext({ organizationId: org.id }, async () => volumeService.listVolumes());
|
||||||
allVolumes.push(...volumes);
|
allVolumes.push(...volumes);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ import { logger } from "../utils/logger";
|
||||||
import { db } from "../db/db";
|
import { db } from "../db/db";
|
||||||
import { eq, or } from "drizzle-orm";
|
import { eq, or } from "drizzle-orm";
|
||||||
import { volumesTable } from "../db/schema";
|
import { volumesTable } from "../db/schema";
|
||||||
|
import { withContext } from "../core/request-context";
|
||||||
|
|
||||||
export class VolumeHealthCheckJob extends Job {
|
export class VolumeHealthCheckJob extends Job {
|
||||||
async run() {
|
async run() {
|
||||||
|
|
@ -14,10 +15,12 @@ export class VolumeHealthCheckJob extends Job {
|
||||||
});
|
});
|
||||||
|
|
||||||
for (const volume of volumes) {
|
for (const volume of volumes) {
|
||||||
const { status } = await volumeService.checkHealth(volume.id, volume.organizationId);
|
await withContext({ organizationId: volume.organizationId }, async () => {
|
||||||
if (status === "error" && volume.autoRemount) {
|
const { status } = await volumeService.checkHealth(volume.id);
|
||||||
await volumeService.mountVolume(volume.id, volume.organizationId);
|
if (status === "error" && volume.autoRemount) {
|
||||||
}
|
await volumeService.mountVolume(volume.id);
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return { done: true, timestamp: new Date() };
|
return { done: true, timestamp: new Date() };
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ import { logger } from "../utils/logger";
|
||||||
import { db } from "../db/db";
|
import { db } from "../db/db";
|
||||||
import { eq, or } from "drizzle-orm";
|
import { eq, or } from "drizzle-orm";
|
||||||
import { repositoriesTable } from "../db/schema";
|
import { repositoriesTable } from "../db/schema";
|
||||||
|
import { withContext } from "../core/request-context";
|
||||||
|
|
||||||
export class RepositoryHealthCheckJob extends Job {
|
export class RepositoryHealthCheckJob extends Job {
|
||||||
async run() {
|
async run() {
|
||||||
|
|
@ -15,7 +16,9 @@ export class RepositoryHealthCheckJob extends Job {
|
||||||
|
|
||||||
for (const repository of repositories) {
|
for (const repository of repositories) {
|
||||||
try {
|
try {
|
||||||
await repositoriesService.checkHealth(repository.id, repository.organizationId);
|
await withContext({ organizationId: repository.organizationId }, async () => {
|
||||||
|
await repositoriesService.checkHealth(repository.id);
|
||||||
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error(`Health check failed for repository ${repository.name}:`, error);
|
logger.error(`Health check failed for repository ${repository.name}:`, error);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ import { auth } from "~/lib/auth";
|
||||||
import { db } from "~/server/db/db";
|
import { db } from "~/server/db/db";
|
||||||
import { member } from "~/server/db/schema";
|
import { member } from "~/server/db/schema";
|
||||||
import { eq, and } from "drizzle-orm";
|
import { eq, and } from "drizzle-orm";
|
||||||
|
import { withContext } from "~/server/core/request-context";
|
||||||
|
|
||||||
declare module "hono" {
|
declare module "hono" {
|
||||||
interface ContextVariableMap {
|
interface ContextVariableMap {
|
||||||
|
|
@ -35,7 +36,9 @@ export const requireAuth = createMiddleware(async (c, next) => {
|
||||||
c.set("user", user);
|
c.set("user", user);
|
||||||
c.set("organizationId", activeOrganizationId);
|
c.set("organizationId", activeOrganizationId);
|
||||||
|
|
||||||
await next();
|
await withContext({ organizationId: activeOrganizationId, userId: user.id }, async () => {
|
||||||
|
await next();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@ import { getVolumePath } from "../../volumes/helpers";
|
||||||
import { restic } from "~/server/utils/restic";
|
import { restic } from "~/server/utils/restic";
|
||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
import { TEST_ORG_ID } from "~/test/helpers/organization";
|
import { TEST_ORG_ID } from "~/test/helpers/organization";
|
||||||
|
import * as context from "~/server/core/request-context";
|
||||||
|
|
||||||
const backupMock = mock(() => Promise.resolve({ exitCode: 0, result: JSON.parse(generateBackupOutput()) }));
|
const backupMock = mock(() => Promise.resolve({ exitCode: 0, result: JSON.parse(generateBackupOutput()) }));
|
||||||
|
|
||||||
|
|
@ -15,6 +16,7 @@ beforeEach(() => {
|
||||||
backupMock.mockClear();
|
backupMock.mockClear();
|
||||||
spyOn(restic, "backup").mockImplementation(backupMock);
|
spyOn(restic, "backup").mockImplementation(backupMock);
|
||||||
spyOn(restic, "forget").mockImplementation(mock(() => Promise.resolve({ success: true })));
|
spyOn(restic, "forget").mockImplementation(mock(() => Promise.resolve({ success: true })));
|
||||||
|
spyOn(context, "getOrganizationId").mockReturnValue(TEST_ORG_ID);
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
|
|
@ -37,7 +39,7 @@ describe("executeBackup - include / exclude patterns", () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
// act
|
// act
|
||||||
await backupsService.executeBackup(schedule.id, TEST_ORG_ID);
|
await backupsService.executeBackup(schedule.id);
|
||||||
|
|
||||||
// assert
|
// assert
|
||||||
expect(backupMock).toHaveBeenCalledWith(
|
expect(backupMock).toHaveBeenCalledWith(
|
||||||
|
|
@ -68,7 +70,7 @@ describe("executeBackup - include / exclude patterns", () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
// act
|
// act
|
||||||
await backupsService.executeBackup(schedule.id, TEST_ORG_ID);
|
await backupsService.executeBackup(schedule.id);
|
||||||
|
|
||||||
// assert
|
// assert
|
||||||
expect(backupMock).toHaveBeenCalledWith(
|
expect(backupMock).toHaveBeenCalledWith(
|
||||||
|
|
@ -98,7 +100,7 @@ describe("executeBackup - include / exclude patterns", () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
// act
|
// act
|
||||||
await backupsService.executeBackup(schedule.id, TEST_ORG_ID);
|
await backupsService.executeBackup(schedule.id);
|
||||||
|
|
||||||
// assert
|
// assert
|
||||||
expect(backupMock).toHaveBeenCalledWith(
|
expect(backupMock).toHaveBeenCalledWith(
|
||||||
|
|
@ -122,7 +124,7 @@ describe("executeBackup - include / exclude patterns", () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
// act
|
// act
|
||||||
await backupsService.executeBackup(schedule.id, TEST_ORG_ID);
|
await backupsService.executeBackup(schedule.id);
|
||||||
|
|
||||||
// assert
|
// assert
|
||||||
expect(backupMock).toHaveBeenCalledWith(
|
expect(backupMock).toHaveBeenCalledWith(
|
||||||
|
|
|
||||||
|
|
@ -7,12 +7,14 @@ import { generateBackupOutput } from "~/test/helpers/restic";
|
||||||
import { faker } from "@faker-js/faker";
|
import { faker } from "@faker-js/faker";
|
||||||
import * as spawnModule from "~/server/utils/spawn";
|
import * as spawnModule from "~/server/utils/spawn";
|
||||||
import { TEST_ORG_ID } from "~/test/helpers/organization";
|
import { TEST_ORG_ID } from "~/test/helpers/organization";
|
||||||
|
import * as context from "~/server/core/request-context";
|
||||||
|
|
||||||
const resticBackupMock = mock(() => Promise.resolve({ exitCode: 0, summary: "", error: "" }));
|
const resticBackupMock = mock(() => Promise.resolve({ exitCode: 0, summary: "", error: "" }));
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
resticBackupMock.mockClear();
|
resticBackupMock.mockClear();
|
||||||
spyOn(spawnModule, "safeSpawn").mockImplementation(resticBackupMock);
|
spyOn(spawnModule, "safeSpawn").mockImplementation(resticBackupMock);
|
||||||
|
spyOn(context, "getOrganizationId").mockReturnValue(TEST_ORG_ID);
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
|
|
@ -36,10 +38,10 @@ describe("execute backup", () => {
|
||||||
);
|
);
|
||||||
|
|
||||||
// act
|
// act
|
||||||
await backupsService.executeBackup(schedule.id, TEST_ORG_ID);
|
await backupsService.executeBackup(schedule.id);
|
||||||
|
|
||||||
// assert
|
// assert
|
||||||
const updatedSchedule = await backupsService.getSchedule(schedule.id, TEST_ORG_ID);
|
const updatedSchedule = await backupsService.getSchedule(schedule.id);
|
||||||
expect(updatedSchedule.nextBackupAt).not.toBeNull();
|
expect(updatedSchedule.nextBackupAt).not.toBeNull();
|
||||||
|
|
||||||
const nextBackupAt = new Date(updatedSchedule.nextBackupAt ?? 0);
|
const nextBackupAt = new Date(updatedSchedule.nextBackupAt ?? 0);
|
||||||
|
|
@ -60,7 +62,7 @@ describe("execute backup", () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
// act
|
// act
|
||||||
await backupsService.executeBackup(schedule.id, TEST_ORG_ID);
|
await backupsService.executeBackup(schedule.id);
|
||||||
|
|
||||||
// assert
|
// assert
|
||||||
expect(resticBackupMock).not.toHaveBeenCalled();
|
expect(resticBackupMock).not.toHaveBeenCalled();
|
||||||
|
|
@ -81,7 +83,7 @@ describe("execute backup", () => {
|
||||||
);
|
);
|
||||||
|
|
||||||
// act
|
// act
|
||||||
await backupsService.executeBackup(schedule.id, TEST_ORG_ID, true);
|
await backupsService.executeBackup(schedule.id, true);
|
||||||
|
|
||||||
// assert
|
// assert
|
||||||
expect(resticBackupMock).toHaveBeenCalled();
|
expect(resticBackupMock).toHaveBeenCalled();
|
||||||
|
|
@ -102,9 +104,9 @@ describe("execute backup", () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
// act
|
// act
|
||||||
void backupsService.executeBackup(schedule.id, TEST_ORG_ID);
|
void backupsService.executeBackup(schedule.id);
|
||||||
await new Promise((resolve) => setTimeout(resolve, 10));
|
await new Promise((resolve) => setTimeout(resolve, 10));
|
||||||
await backupsService.executeBackup(schedule.id, TEST_ORG_ID);
|
await backupsService.executeBackup(schedule.id);
|
||||||
|
|
||||||
// assert
|
// assert
|
||||||
expect(resticBackupMock).toHaveBeenCalledTimes(1);
|
expect(resticBackupMock).toHaveBeenCalledTimes(1);
|
||||||
|
|
@ -124,10 +126,10 @@ describe("execute backup", () => {
|
||||||
);
|
);
|
||||||
|
|
||||||
// act
|
// act
|
||||||
await backupsService.executeBackup(schedule.id, TEST_ORG_ID);
|
await backupsService.executeBackup(schedule.id);
|
||||||
|
|
||||||
// assert
|
// assert
|
||||||
const updatedSchedule = await backupsService.getSchedule(schedule.id, TEST_ORG_ID);
|
const updatedSchedule = await backupsService.getSchedule(schedule.id);
|
||||||
expect(updatedSchedule.lastBackupStatus).toBe("warning");
|
expect(updatedSchedule.lastBackupStatus).toBe("warning");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -145,10 +147,10 @@ describe("execute backup", () => {
|
||||||
);
|
);
|
||||||
|
|
||||||
// act
|
// act
|
||||||
await backupsService.executeBackup(schedule.id, TEST_ORG_ID);
|
await backupsService.executeBackup(schedule.id);
|
||||||
|
|
||||||
// assert
|
// assert
|
||||||
const updatedSchedule = await backupsService.getSchedule(schedule.id, TEST_ORG_ID);
|
const updatedSchedule = await backupsService.getSchedule(schedule.id);
|
||||||
expect(updatedSchedule.lastBackupStatus).toBe("error");
|
expect(updatedSchedule.lastBackupStatus).toBe("error");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
@ -169,7 +171,7 @@ describe("getSchedulesToExecute", () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
// act
|
// act
|
||||||
const schedulesToExecute = await backupsService.getSchedulesToExecute(TEST_ORG_ID);
|
const schedulesToExecute = await backupsService.getSchedulesToExecute();
|
||||||
|
|
||||||
// assert
|
// assert
|
||||||
expect(schedulesToExecute).toContain(schedule.id);
|
expect(schedulesToExecute).toContain(schedule.id);
|
||||||
|
|
|
||||||
|
|
@ -46,49 +46,44 @@ import { requireAuth } from "../auth/auth.middleware";
|
||||||
export const backupScheduleController = new Hono()
|
export const backupScheduleController = new Hono()
|
||||||
.use(requireAuth)
|
.use(requireAuth)
|
||||||
.get("/", listBackupSchedulesDto, async (c) => {
|
.get("/", listBackupSchedulesDto, async (c) => {
|
||||||
const schedules = await backupsService.listSchedules(c.get("organizationId"));
|
const schedules = await backupsService.listSchedules();
|
||||||
|
|
||||||
return c.json<ListBackupSchedulesResponseDto>(schedules, 200);
|
return c.json<ListBackupSchedulesResponseDto>(schedules, 200);
|
||||||
})
|
})
|
||||||
.get("/:scheduleId", getBackupScheduleDto, async (c) => {
|
.get("/:scheduleId", getBackupScheduleDto, async (c) => {
|
||||||
const scheduleId = c.req.param("scheduleId");
|
const scheduleId = c.req.param("scheduleId");
|
||||||
|
const schedule = await backupsService.getSchedule(Number(scheduleId));
|
||||||
const schedule = await backupsService.getSchedule(Number(scheduleId), c.get("organizationId"));
|
|
||||||
|
|
||||||
return c.json<GetBackupScheduleDto>(schedule, 200);
|
return c.json<GetBackupScheduleDto>(schedule, 200);
|
||||||
})
|
})
|
||||||
.get("/volume/:volumeId", getBackupScheduleForVolumeDto, async (c) => {
|
.get("/volume/:volumeId", getBackupScheduleForVolumeDto, async (c) => {
|
||||||
const volumeId = c.req.param("volumeId");
|
const volumeId = c.req.param("volumeId");
|
||||||
const schedule = await backupsService.getScheduleForVolume(Number(volumeId), c.get("organizationId"));
|
const schedule = await backupsService.getScheduleForVolume(Number(volumeId));
|
||||||
|
|
||||||
return c.json<GetBackupScheduleForVolumeResponseDto>(schedule, 200);
|
return c.json<GetBackupScheduleForVolumeResponseDto>(schedule, 200);
|
||||||
})
|
})
|
||||||
.post("/", createBackupScheduleDto, validator("json", createBackupScheduleBody), async (c) => {
|
.post("/", createBackupScheduleDto, validator("json", createBackupScheduleBody), async (c) => {
|
||||||
const body = c.req.valid("json");
|
const body = c.req.valid("json");
|
||||||
|
const schedule = await backupsService.createSchedule(body);
|
||||||
const schedule = await backupsService.createSchedule(body, c.get("organizationId"));
|
|
||||||
|
|
||||||
return c.json<CreateBackupScheduleDto>(schedule, 201);
|
return c.json<CreateBackupScheduleDto>(schedule, 201);
|
||||||
})
|
})
|
||||||
.patch("/:scheduleId", updateBackupScheduleDto, validator("json", updateBackupScheduleBody), async (c) => {
|
.patch("/:scheduleId", updateBackupScheduleDto, validator("json", updateBackupScheduleBody), async (c) => {
|
||||||
const scheduleId = c.req.param("scheduleId");
|
const scheduleId = c.req.param("scheduleId");
|
||||||
const body = c.req.valid("json");
|
const body = c.req.valid("json");
|
||||||
|
const schedule = await backupsService.updateSchedule(Number(scheduleId), body);
|
||||||
const schedule = await backupsService.updateSchedule(Number(scheduleId), body, c.get("organizationId"));
|
|
||||||
|
|
||||||
return c.json<UpdateBackupScheduleDto>(schedule, 200);
|
return c.json<UpdateBackupScheduleDto>(schedule, 200);
|
||||||
})
|
})
|
||||||
.delete("/:scheduleId", deleteBackupScheduleDto, async (c) => {
|
.delete("/:scheduleId", deleteBackupScheduleDto, async (c) => {
|
||||||
const scheduleId = c.req.param("scheduleId");
|
const scheduleId = c.req.param("scheduleId");
|
||||||
|
await backupsService.deleteSchedule(Number(scheduleId));
|
||||||
await backupsService.deleteSchedule(Number(scheduleId), c.get("organizationId"));
|
|
||||||
|
|
||||||
return c.json<DeleteBackupScheduleDto>({ success: true }, 200);
|
return c.json<DeleteBackupScheduleDto>({ success: true }, 200);
|
||||||
})
|
})
|
||||||
.post("/:scheduleId/run", runBackupNowDto, async (c) => {
|
.post("/:scheduleId/run", runBackupNowDto, async (c) => {
|
||||||
const scheduleId = c.req.param("scheduleId");
|
const scheduleId = c.req.param("scheduleId");
|
||||||
|
backupsService.executeBackup(Number(scheduleId), true).catch((err) => {
|
||||||
backupsService.executeBackup(Number(scheduleId), c.get("organizationId"), true).catch((err) => {
|
|
||||||
console.error(`Error executing manual backup for schedule ${scheduleId}:`, err);
|
console.error(`Error executing manual backup for schedule ${scheduleId}:`, err);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -96,15 +91,13 @@ export const backupScheduleController = new Hono()
|
||||||
})
|
})
|
||||||
.post("/:scheduleId/stop", stopBackupDto, async (c) => {
|
.post("/:scheduleId/stop", stopBackupDto, async (c) => {
|
||||||
const scheduleId = c.req.param("scheduleId");
|
const scheduleId = c.req.param("scheduleId");
|
||||||
|
await backupsService.stopBackup(Number(scheduleId));
|
||||||
await backupsService.stopBackup(Number(scheduleId), c.get("organizationId"));
|
|
||||||
|
|
||||||
return c.json<StopBackupDto>({ success: true }, 200);
|
return c.json<StopBackupDto>({ success: true }, 200);
|
||||||
})
|
})
|
||||||
.post("/:scheduleId/forget", runForgetDto, async (c) => {
|
.post("/:scheduleId/forget", runForgetDto, async (c) => {
|
||||||
const scheduleId = c.req.param("scheduleId");
|
const scheduleId = c.req.param("scheduleId");
|
||||||
|
await backupsService.runForget(Number(scheduleId));
|
||||||
await backupsService.runForget(Number(scheduleId), c.get("organizationId"));
|
|
||||||
|
|
||||||
return c.json<RunForgetDto>({ success: true }, 200);
|
return c.json<RunForgetDto>({ success: true }, 200);
|
||||||
})
|
})
|
||||||
|
|
@ -128,27 +121,26 @@ export const backupScheduleController = new Hono()
|
||||||
)
|
)
|
||||||
.get("/:scheduleId/mirrors", getScheduleMirrorsDto, async (c) => {
|
.get("/:scheduleId/mirrors", getScheduleMirrorsDto, async (c) => {
|
||||||
const scheduleId = Number.parseInt(c.req.param("scheduleId"), 10);
|
const scheduleId = Number.parseInt(c.req.param("scheduleId"), 10);
|
||||||
const mirrors = await backupsService.getMirrors(scheduleId, c.get("organizationId"));
|
const mirrors = await backupsService.getMirrors(scheduleId);
|
||||||
|
|
||||||
return c.json<GetScheduleMirrorsDto>(mirrors, 200);
|
return c.json<GetScheduleMirrorsDto>(mirrors, 200);
|
||||||
})
|
})
|
||||||
.put("/:scheduleId/mirrors", updateScheduleMirrorsDto, validator("json", updateScheduleMirrorsBody), async (c) => {
|
.put("/:scheduleId/mirrors", updateScheduleMirrorsDto, validator("json", updateScheduleMirrorsBody), async (c) => {
|
||||||
const scheduleId = Number.parseInt(c.req.param("scheduleId"), 10);
|
const scheduleId = Number.parseInt(c.req.param("scheduleId"), 10);
|
||||||
const body = c.req.valid("json");
|
const body = c.req.valid("json");
|
||||||
const mirrors = await backupsService.updateMirrors(scheduleId, c.get("organizationId"), body);
|
const mirrors = await backupsService.updateMirrors(scheduleId, body);
|
||||||
|
|
||||||
return c.json<UpdateScheduleMirrorsDto>(mirrors, 200);
|
return c.json<UpdateScheduleMirrorsDto>(mirrors, 200);
|
||||||
})
|
})
|
||||||
.get("/:scheduleId/mirrors/compatibility", getMirrorCompatibilityDto, async (c) => {
|
.get("/:scheduleId/mirrors/compatibility", getMirrorCompatibilityDto, async (c) => {
|
||||||
const scheduleId = Number.parseInt(c.req.param("scheduleId"), 10);
|
const scheduleId = Number.parseInt(c.req.param("scheduleId"), 10);
|
||||||
const compatibility = await backupsService.getMirrorCompatibility(scheduleId, c.get("organizationId"));
|
const compatibility = await backupsService.getMirrorCompatibility(scheduleId);
|
||||||
|
|
||||||
return c.json<GetMirrorCompatibilityDto>(compatibility, 200);
|
return c.json<GetMirrorCompatibilityDto>(compatibility, 200);
|
||||||
})
|
})
|
||||||
.post("/reorder", reorderBackupSchedulesDto, validator("json", reorderBackupSchedulesBody), async (c) => {
|
.post("/reorder", reorderBackupSchedulesDto, validator("json", reorderBackupSchedulesBody), async (c) => {
|
||||||
const body = c.req.valid("json");
|
const body = c.req.valid("json");
|
||||||
|
await backupsService.reorderSchedules(body.scheduleIds);
|
||||||
await backupsService.reorderSchedules(body.scheduleIds, c.get("organizationId"));
|
|
||||||
|
|
||||||
return c.json<ReorderBackupSchedulesDto>({ success: true }, 200);
|
return c.json<ReorderBackupSchedulesDto>({ success: true }, 200);
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,7 @@ import { repoMutex } from "../../core/repository-mutex";
|
||||||
import { checkMirrorCompatibility, getIncompatibleMirrorError } from "~/server/utils/backend-compatibility";
|
import { checkMirrorCompatibility, getIncompatibleMirrorError } from "~/server/utils/backend-compatibility";
|
||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
import { generateShortId } from "~/server/utils/id";
|
import { generateShortId } from "~/server/utils/id";
|
||||||
|
import { getOrganizationId } from "~/server/core/request-context";
|
||||||
|
|
||||||
const runningBackups = new Map<number, AbortController>();
|
const runningBackups = new Map<number, AbortController>();
|
||||||
|
|
||||||
|
|
@ -56,7 +57,8 @@ const processPattern = (pattern: string, volumePath: string): string => {
|
||||||
return pattern;
|
return pattern;
|
||||||
};
|
};
|
||||||
|
|
||||||
const listSchedules = async (organizationId: string) => {
|
const listSchedules = async () => {
|
||||||
|
const organizationId = getOrganizationId();
|
||||||
const schedules = await db.query.backupSchedulesTable.findMany({
|
const schedules = await db.query.backupSchedulesTable.findMany({
|
||||||
where: eq(backupSchedulesTable.organizationId, organizationId),
|
where: eq(backupSchedulesTable.organizationId, organizationId),
|
||||||
with: {
|
with: {
|
||||||
|
|
@ -68,7 +70,8 @@ const listSchedules = async (organizationId: string) => {
|
||||||
return schedules;
|
return schedules;
|
||||||
};
|
};
|
||||||
|
|
||||||
const getSchedule = async (scheduleId: number, organizationId: string) => {
|
const getSchedule = async (scheduleId: number) => {
|
||||||
|
const organizationId = getOrganizationId();
|
||||||
const schedule = await db.query.backupSchedulesTable.findFirst({
|
const schedule = await db.query.backupSchedulesTable.findFirst({
|
||||||
where: and(eq(backupSchedulesTable.id, scheduleId), eq(backupSchedulesTable.organizationId, organizationId)),
|
where: and(eq(backupSchedulesTable.id, scheduleId), eq(backupSchedulesTable.organizationId, organizationId)),
|
||||||
with: {
|
with: {
|
||||||
|
|
@ -84,7 +87,8 @@ const getSchedule = async (scheduleId: number, organizationId: string) => {
|
||||||
return schedule;
|
return schedule;
|
||||||
};
|
};
|
||||||
|
|
||||||
const createSchedule = async (data: CreateBackupScheduleBody, organizationId: string) => {
|
const createSchedule = async (data: CreateBackupScheduleBody) => {
|
||||||
|
const organizationId = getOrganizationId();
|
||||||
if (!cron.validate(data.cronExpression)) {
|
if (!cron.validate(data.cronExpression)) {
|
||||||
throw new BadRequestError("Invalid cron expression");
|
throw new BadRequestError("Invalid cron expression");
|
||||||
}
|
}
|
||||||
|
|
@ -141,7 +145,8 @@ const createSchedule = async (data: CreateBackupScheduleBody, organizationId: st
|
||||||
return newSchedule;
|
return newSchedule;
|
||||||
};
|
};
|
||||||
|
|
||||||
const updateSchedule = async (scheduleId: number, data: UpdateBackupScheduleBody, organizationId: string) => {
|
const updateSchedule = async (scheduleId: number, data: UpdateBackupScheduleBody) => {
|
||||||
|
const organizationId = getOrganizationId();
|
||||||
const schedule = await db.query.backupSchedulesTable.findFirst({
|
const schedule = await db.query.backupSchedulesTable.findFirst({
|
||||||
where: and(eq(backupSchedulesTable.id, scheduleId), eq(backupSchedulesTable.organizationId, organizationId)),
|
where: and(eq(backupSchedulesTable.id, scheduleId), eq(backupSchedulesTable.organizationId, organizationId)),
|
||||||
});
|
});
|
||||||
|
|
@ -188,7 +193,8 @@ const updateSchedule = async (scheduleId: number, data: UpdateBackupScheduleBody
|
||||||
return updated;
|
return updated;
|
||||||
};
|
};
|
||||||
|
|
||||||
const deleteSchedule = async (scheduleId: number, organizationId: string) => {
|
const deleteSchedule = async (scheduleId: number) => {
|
||||||
|
const organizationId = getOrganizationId();
|
||||||
const schedule = await db.query.backupSchedulesTable.findFirst({
|
const schedule = await db.query.backupSchedulesTable.findFirst({
|
||||||
where: and(eq(backupSchedulesTable.id, scheduleId), eq(backupSchedulesTable.organizationId, organizationId)),
|
where: and(eq(backupSchedulesTable.id, scheduleId), eq(backupSchedulesTable.organizationId, organizationId)),
|
||||||
});
|
});
|
||||||
|
|
@ -202,7 +208,8 @@ const deleteSchedule = async (scheduleId: number, organizationId: string) => {
|
||||||
.where(and(eq(backupSchedulesTable.id, scheduleId), eq(backupSchedulesTable.organizationId, organizationId)));
|
.where(and(eq(backupSchedulesTable.id, scheduleId), eq(backupSchedulesTable.organizationId, organizationId)));
|
||||||
};
|
};
|
||||||
|
|
||||||
const executeBackup = async (scheduleId: number, organizationId: string, manual = false) => {
|
const executeBackup = async (scheduleId: number, manual = false) => {
|
||||||
|
const organizationId = getOrganizationId();
|
||||||
const schedule = await db.query.backupSchedulesTable.findFirst({
|
const schedule = await db.query.backupSchedulesTable.findFirst({
|
||||||
where: and(eq(backupSchedulesTable.id, scheduleId), eq(backupSchedulesTable.organizationId, organizationId)),
|
where: and(eq(backupSchedulesTable.id, scheduleId), eq(backupSchedulesTable.organizationId, organizationId)),
|
||||||
});
|
});
|
||||||
|
|
@ -324,12 +331,12 @@ const executeBackup = async (scheduleId: number, organizationId: string, manual
|
||||||
}
|
}
|
||||||
|
|
||||||
if (schedule.retentionPolicy) {
|
if (schedule.retentionPolicy) {
|
||||||
void runForget(schedule.id, organizationId).catch((error) => {
|
void runForget(schedule.id).catch((error) => {
|
||||||
logger.error(`Failed to run retention policy for schedule ${scheduleId}: ${toMessage(error)}`);
|
logger.error(`Failed to run retention policy for schedule ${scheduleId}: ${toMessage(error)}`);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
void copyToMirrors(scheduleId, organizationId, repository, schedule.retentionPolicy).catch((error) => {
|
void copyToMirrors(scheduleId, repository, schedule.retentionPolicy).catch((error) => {
|
||||||
logger.error(`Background mirror copy failed for schedule ${scheduleId}: ${toMessage(error)}`);
|
logger.error(`Background mirror copy failed for schedule ${scheduleId}: ${toMessage(error)}`);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -412,13 +419,14 @@ const executeBackup = async (scheduleId: number, organizationId: string, manual
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const getSchedulesToExecute = async (organizationId: string) => {
|
const getSchedulesToExecute = async () => {
|
||||||
|
const organizationId = getOrganizationId();
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
const schedules = await db.query.backupSchedulesTable.findMany({
|
const schedules = await db.query.backupSchedulesTable.findMany({
|
||||||
where: and(
|
where: and(
|
||||||
eq(backupSchedulesTable.enabled, true),
|
eq(backupSchedulesTable.enabled, true),
|
||||||
or(ne(backupSchedulesTable.lastBackupStatus, "in_progress"), isNull(backupSchedulesTable.lastBackupStatus)),
|
or(ne(backupSchedulesTable.lastBackupStatus, "in_progress"), isNull(backupSchedulesTable.lastBackupStatus)),
|
||||||
eq(backupSchedulesTable.organizationId, organizationId)
|
eq(backupSchedulesTable.organizationId, organizationId),
|
||||||
),
|
),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -433,7 +441,8 @@ const getSchedulesToExecute = async (organizationId: string) => {
|
||||||
return schedulesToRun;
|
return schedulesToRun;
|
||||||
};
|
};
|
||||||
|
|
||||||
const getScheduleForVolume = async (volumeId: number, organizationId: string) => {
|
const getScheduleForVolume = async (volumeId: number) => {
|
||||||
|
const organizationId = getOrganizationId();
|
||||||
const schedule = await db.query.backupSchedulesTable.findFirst({
|
const schedule = await db.query.backupSchedulesTable.findFirst({
|
||||||
where: and(eq(backupSchedulesTable.volumeId, volumeId), eq(backupSchedulesTable.organizationId, organizationId)),
|
where: and(eq(backupSchedulesTable.volumeId, volumeId), eq(backupSchedulesTable.organizationId, organizationId)),
|
||||||
with: { volume: true, repository: true },
|
with: { volume: true, repository: true },
|
||||||
|
|
@ -442,7 +451,8 @@ const getScheduleForVolume = async (volumeId: number, organizationId: string) =>
|
||||||
return schedule ?? null;
|
return schedule ?? null;
|
||||||
};
|
};
|
||||||
|
|
||||||
const stopBackup = async (scheduleId: number, organizationId: string) => {
|
const stopBackup = async (scheduleId: number) => {
|
||||||
|
const organizationId = getOrganizationId();
|
||||||
const schedule = await db.query.backupSchedulesTable.findFirst({
|
const schedule = await db.query.backupSchedulesTable.findFirst({
|
||||||
where: and(eq(backupSchedulesTable.id, scheduleId), eq(backupSchedulesTable.organizationId, organizationId)),
|
where: and(eq(backupSchedulesTable.id, scheduleId), eq(backupSchedulesTable.organizationId, organizationId)),
|
||||||
});
|
});
|
||||||
|
|
@ -472,7 +482,8 @@ const stopBackup = async (scheduleId: number, organizationId: string) => {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const runForget = async (scheduleId: number, organizationId: string, repositoryId?: string) => {
|
const runForget = async (scheduleId: number, repositoryId?: string) => {
|
||||||
|
const organizationId = getOrganizationId();
|
||||||
const schedule = await db.query.backupSchedulesTable.findFirst({
|
const schedule = await db.query.backupSchedulesTable.findFirst({
|
||||||
where: and(eq(backupSchedulesTable.id, scheduleId), eq(backupSchedulesTable.organizationId, organizationId)),
|
where: and(eq(backupSchedulesTable.id, scheduleId), eq(backupSchedulesTable.organizationId, organizationId)),
|
||||||
});
|
});
|
||||||
|
|
@ -505,7 +516,8 @@ const runForget = async (scheduleId: number, organizationId: string, repositoryI
|
||||||
logger.info(`Retention policy applied successfully for schedule ${scheduleId}`);
|
logger.info(`Retention policy applied successfully for schedule ${scheduleId}`);
|
||||||
};
|
};
|
||||||
|
|
||||||
const getMirrors = async (scheduleId: number, organizationId: string) => {
|
const getMirrors = async (scheduleId: number) => {
|
||||||
|
const organizationId = getOrganizationId();
|
||||||
const schedule = await db.query.backupSchedulesTable.findFirst({
|
const schedule = await db.query.backupSchedulesTable.findFirst({
|
||||||
where: and(eq(backupSchedulesTable.id, scheduleId), eq(backupSchedulesTable.organizationId, organizationId)),
|
where: and(eq(backupSchedulesTable.id, scheduleId), eq(backupSchedulesTable.organizationId, organizationId)),
|
||||||
});
|
});
|
||||||
|
|
@ -522,7 +534,8 @@ const getMirrors = async (scheduleId: number, organizationId: string) => {
|
||||||
return mirrors;
|
return mirrors;
|
||||||
};
|
};
|
||||||
|
|
||||||
const updateMirrors = async (scheduleId: number, organizationId: string, data: UpdateScheduleMirrorsBody) => {
|
const updateMirrors = async (scheduleId: number, data: UpdateScheduleMirrorsBody) => {
|
||||||
|
const organizationId = getOrganizationId();
|
||||||
const schedule = await db.query.backupSchedulesTable.findFirst({
|
const schedule = await db.query.backupSchedulesTable.findFirst({
|
||||||
where: and(eq(backupSchedulesTable.id, scheduleId), eq(backupSchedulesTable.organizationId, organizationId)),
|
where: and(eq(backupSchedulesTable.id, scheduleId), eq(backupSchedulesTable.organizationId, organizationId)),
|
||||||
with: { repository: true },
|
with: { repository: true },
|
||||||
|
|
@ -583,15 +596,15 @@ const updateMirrors = async (scheduleId: number, organizationId: string, data: U
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return getMirrors(scheduleId, organizationId);
|
return getMirrors(scheduleId);
|
||||||
};
|
};
|
||||||
|
|
||||||
const copyToMirrors = async (
|
const copyToMirrors = async (
|
||||||
scheduleId: number,
|
scheduleId: number,
|
||||||
organizationId: string,
|
|
||||||
sourceRepository: { id: string; config: (typeof repositoriesTable.$inferSelect)["config"] },
|
sourceRepository: { id: string; config: (typeof repositoriesTable.$inferSelect)["config"] },
|
||||||
retentionPolicy: (typeof backupSchedulesTable.$inferSelect)["retentionPolicy"],
|
retentionPolicy: (typeof backupSchedulesTable.$inferSelect)["retentionPolicy"],
|
||||||
) => {
|
) => {
|
||||||
|
const organizationId = getOrganizationId();
|
||||||
const schedule = await db.query.backupSchedulesTable.findFirst({
|
const schedule = await db.query.backupSchedulesTable.findFirst({
|
||||||
where: and(eq(backupSchedulesTable.id, scheduleId), eq(backupSchedulesTable.organizationId, organizationId)),
|
where: and(eq(backupSchedulesTable.id, scheduleId), eq(backupSchedulesTable.organizationId, organizationId)),
|
||||||
});
|
});
|
||||||
|
|
@ -637,7 +650,7 @@ const copyToMirrors = async (
|
||||||
}
|
}
|
||||||
|
|
||||||
if (retentionPolicy) {
|
if (retentionPolicy) {
|
||||||
void runForget(scheduleId, organizationId, mirror.repository.id).catch((error) => {
|
void runForget(scheduleId, mirror.repository.id).catch((error) => {
|
||||||
logger.error(
|
logger.error(
|
||||||
`Failed to run retention policy for mirror repository ${mirror.repository.name}: ${toMessage(error)}`,
|
`Failed to run retention policy for mirror repository ${mirror.repository.name}: ${toMessage(error)}`,
|
||||||
);
|
);
|
||||||
|
|
@ -677,7 +690,8 @@ const copyToMirrors = async (
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const getMirrorCompatibility = async (scheduleId: number, organizationId: string) => {
|
const getMirrorCompatibility = async (scheduleId: number) => {
|
||||||
|
const organizationId = getOrganizationId();
|
||||||
const schedule = await db.query.backupSchedulesTable.findFirst({
|
const schedule = await db.query.backupSchedulesTable.findFirst({
|
||||||
where: and(eq(backupSchedulesTable.id, scheduleId), eq(backupSchedulesTable.organizationId, organizationId)),
|
where: and(eq(backupSchedulesTable.id, scheduleId), eq(backupSchedulesTable.organizationId, organizationId)),
|
||||||
with: { repository: true },
|
with: { repository: true },
|
||||||
|
|
@ -699,7 +713,8 @@ const getMirrorCompatibility = async (scheduleId: number, organizationId: string
|
||||||
return compatibility;
|
return compatibility;
|
||||||
};
|
};
|
||||||
|
|
||||||
const reorderSchedules = async (scheduleIds: number[], organizationId: string) => {
|
const reorderSchedules = async (scheduleIds: number[]) => {
|
||||||
|
const organizationId = getOrganizationId();
|
||||||
const uniqueIds = new Set(scheduleIds);
|
const uniqueIds = new Set(scheduleIds);
|
||||||
if (uniqueIds.size !== scheduleIds.length) {
|
if (uniqueIds.size !== scheduleIds.length) {
|
||||||
throw new BadRequestError("Duplicate schedule IDs in reorder request");
|
throw new BadRequestError("Duplicate schedule IDs in reorder request");
|
||||||
|
|
|
||||||
|
|
@ -15,32 +15,37 @@ import { VolumeAutoRemountJob } from "~/server/jobs/auto-remount";
|
||||||
import { cache } from "~/server/utils/cache";
|
import { cache } from "~/server/utils/cache";
|
||||||
import { initAuth } from "~/lib/auth";
|
import { initAuth } from "~/lib/auth";
|
||||||
import { toMessage } from "~/server/utils/errors";
|
import { toMessage } from "~/server/utils/errors";
|
||||||
|
import { withContext } from "~/server/core/request-context";
|
||||||
|
|
||||||
const ensureLatestConfigurationSchema = async () => {
|
const ensureLatestConfigurationSchema = async () => {
|
||||||
const volumes = await db.query.volumesTable.findMany({});
|
const volumes = await db.query.volumesTable.findMany({});
|
||||||
|
|
||||||
for (const volume of volumes) {
|
for (const volume of volumes) {
|
||||||
await volumeService.updateVolume(volume.id, volume, volume.organizationId).catch((err) => {
|
await withContext({ organizationId: volume.organizationId }, async () => {
|
||||||
logger.error(`Failed to update volume ${volume.name}: ${err}`);
|
await volumeService.updateVolume(volume.id, volume).catch((err) => {
|
||||||
|
logger.error(`Failed to update volume ${volume.name}: ${err}`);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const repositories = await db.query.repositoriesTable.findMany({});
|
const repositories = await db.query.repositoriesTable.findMany({});
|
||||||
|
|
||||||
for (const repo of repositories) {
|
for (const repo of repositories) {
|
||||||
await repositoriesService.updateRepository(repo.id, repo.organizationId, {}).catch((err) => {
|
await withContext({ organizationId: repo.organizationId }, async () => {
|
||||||
logger.error(`Failed to update repository ${repo.name}: ${err}`);
|
await repositoriesService.updateRepository(repo.id, {}).catch((err) => {
|
||||||
|
logger.error(`Failed to update repository ${repo.name}: ${err}`);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const notifications = await db.query.notificationDestinationsTable.findMany({});
|
const notifications = await db.query.notificationDestinationsTable.findMany({});
|
||||||
|
|
||||||
for (const notification of notifications) {
|
for (const notification of notifications) {
|
||||||
await notificationsService
|
await withContext({ organizationId: notification.organizationId }, async () => {
|
||||||
.updateDestination(notification.id, notification.organizationId, notification)
|
await notificationsService.updateDestination(notification.id, notification).catch((err) => {
|
||||||
.catch((err) => {
|
|
||||||
logger.error(`Failed to update notification destination ${notification.id}: ${err}`);
|
logger.error(`Failed to update notification destination ${notification.id}: ${err}`);
|
||||||
});
|
});
|
||||||
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -69,8 +74,10 @@ export const startup = async () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
for (const volume of volumes) {
|
for (const volume of volumes) {
|
||||||
await volumeService.mountVolume(volume.id, volume.organizationId).catch((err) => {
|
await withContext({ organizationId: volume.organizationId }, async () => {
|
||||||
logger.error(`Error auto-remounting volume ${volume.name} on startup: ${err.message}`);
|
await volumeService.mountVolume(volume.id).catch((err) => {
|
||||||
|
logger.error(`Error auto-remounting volume ${volume.name} on startup: ${err.message}`);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -22,32 +22,32 @@ import { requireAuth } from "../auth/auth.middleware";
|
||||||
export const notificationsController = new Hono()
|
export const notificationsController = new Hono()
|
||||||
.use(requireAuth)
|
.use(requireAuth)
|
||||||
.get("/destinations", listDestinationsDto, async (c) => {
|
.get("/destinations", listDestinationsDto, async (c) => {
|
||||||
const destinations = await notificationsService.listDestinations(c.get("organizationId"));
|
const destinations = await notificationsService.listDestinations();
|
||||||
return c.json<ListDestinationsDto>(destinations, 200);
|
return c.json<ListDestinationsDto>(destinations, 200);
|
||||||
})
|
})
|
||||||
.post("/destinations", createDestinationDto, validator("json", createDestinationBody), async (c) => {
|
.post("/destinations", createDestinationDto, validator("json", createDestinationBody), async (c) => {
|
||||||
const body = c.req.valid("json");
|
const body = c.req.valid("json");
|
||||||
const destination = await notificationsService.createDestination(body.name, body.config, c.get("organizationId"));
|
const destination = await notificationsService.createDestination(body.name, body.config);
|
||||||
return c.json<CreateDestinationDto>(destination, 201);
|
return c.json<CreateDestinationDto>(destination, 201);
|
||||||
})
|
})
|
||||||
.get("/destinations/:id", getDestinationDto, async (c) => {
|
.get("/destinations/:id", getDestinationDto, async (c) => {
|
||||||
const id = Number.parseInt(c.req.param("id"), 10);
|
const id = Number.parseInt(c.req.param("id"), 10);
|
||||||
const destination = await notificationsService.getDestination(id, c.get("organizationId"));
|
const destination = await notificationsService.getDestination(id);
|
||||||
return c.json<GetDestinationDto>(destination, 200);
|
return c.json<GetDestinationDto>(destination, 200);
|
||||||
})
|
})
|
||||||
.patch("/destinations/:id", updateDestinationDto, validator("json", updateDestinationBody), async (c) => {
|
.patch("/destinations/:id", updateDestinationDto, validator("json", updateDestinationBody), async (c) => {
|
||||||
const id = Number.parseInt(c.req.param("id"), 10);
|
const id = Number.parseInt(c.req.param("id"), 10);
|
||||||
const body = c.req.valid("json");
|
const body = c.req.valid("json");
|
||||||
const destination = await notificationsService.updateDestination(id, c.get("organizationId"), body);
|
const destination = await notificationsService.updateDestination(id, body);
|
||||||
return c.json<UpdateDestinationDto>(destination, 200);
|
return c.json<UpdateDestinationDto>(destination, 200);
|
||||||
})
|
})
|
||||||
.delete("/destinations/:id", deleteDestinationDto, async (c) => {
|
.delete("/destinations/:id", deleteDestinationDto, async (c) => {
|
||||||
const id = Number.parseInt(c.req.param("id"), 10);
|
const id = Number.parseInt(c.req.param("id"), 10);
|
||||||
await notificationsService.deleteDestination(id, c.get("organizationId"));
|
await notificationsService.deleteDestination(id);
|
||||||
return c.json<DeleteDestinationDto>({ message: "Notification destination deleted" }, 200);
|
return c.json<DeleteDestinationDto>({ message: "Notification destination deleted" }, 200);
|
||||||
})
|
})
|
||||||
.post("/destinations/:id/test", testDestinationDto, async (c) => {
|
.post("/destinations/:id/test", testDestinationDto, async (c) => {
|
||||||
const id = Number.parseInt(c.req.param("id"), 10);
|
const id = Number.parseInt(c.req.param("id"), 10);
|
||||||
const result = await notificationsService.testDestination(id, c.get("organizationId"));
|
const result = await notificationsService.testDestination(id);
|
||||||
return c.json<TestDestinationDto>(result, 200);
|
return c.json<TestDestinationDto>(result, 200);
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -14,8 +14,10 @@ import { buildShoutrrrUrl } from "./builders";
|
||||||
import { notificationConfigSchema, type NotificationConfig, type NotificationEvent } from "~/schemas/notifications";
|
import { notificationConfigSchema, type NotificationConfig, type NotificationEvent } from "~/schemas/notifications";
|
||||||
import { toMessage } from "../../utils/errors";
|
import { toMessage } from "../../utils/errors";
|
||||||
import { type } from "arktype";
|
import { type } from "arktype";
|
||||||
|
import { getOrganizationId } from "~/server/core/request-context";
|
||||||
|
|
||||||
const listDestinations = async (organizationId: string) => {
|
const listDestinations = async () => {
|
||||||
|
const organizationId = getOrganizationId();
|
||||||
const destinations = await db.query.notificationDestinationsTable.findMany({
|
const destinations = await db.query.notificationDestinationsTable.findMany({
|
||||||
where: eq(notificationDestinationsTable.organizationId, organizationId),
|
where: eq(notificationDestinationsTable.organizationId, organizationId),
|
||||||
orderBy: (destinations, { asc }) => [asc(destinations.name)],
|
orderBy: (destinations, { asc }) => [asc(destinations.name)],
|
||||||
|
|
@ -23,7 +25,8 @@ const listDestinations = async (organizationId: string) => {
|
||||||
return destinations;
|
return destinations;
|
||||||
};
|
};
|
||||||
|
|
||||||
const getDestination = async (id: number, organizationId: string) => {
|
const getDestination = async (id: number) => {
|
||||||
|
const organizationId = getOrganizationId();
|
||||||
const destination = await db.query.notificationDestinationsTable.findFirst({
|
const destination = await db.query.notificationDestinationsTable.findFirst({
|
||||||
where: and(eq(notificationDestinationsTable.id, id), eq(notificationDestinationsTable.organizationId, organizationId)),
|
where: and(eq(notificationDestinationsTable.id, id), eq(notificationDestinationsTable.organizationId, organizationId)),
|
||||||
});
|
});
|
||||||
|
|
@ -133,7 +136,8 @@ async function decryptSensitiveFields(config: NotificationConfig): Promise<Notif
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const createDestination = async (name: string, config: NotificationConfig, organizationId: string) => {
|
const createDestination = async (name: string, config: NotificationConfig) => {
|
||||||
|
const organizationId = getOrganizationId();
|
||||||
const slug = slugify(name, { lower: true, strict: true });
|
const slug = slugify(name, { lower: true, strict: true });
|
||||||
|
|
||||||
const existing = await db.query.notificationDestinationsTable.findFirst({
|
const existing = await db.query.notificationDestinationsTable.findFirst({
|
||||||
|
|
@ -165,10 +169,10 @@ const createDestination = async (name: string, config: NotificationConfig, organ
|
||||||
|
|
||||||
const updateDestination = async (
|
const updateDestination = async (
|
||||||
id: number,
|
id: number,
|
||||||
organizationId: string,
|
|
||||||
updates: { name?: string; enabled?: boolean; config?: NotificationConfig },
|
updates: { name?: string; enabled?: boolean; config?: NotificationConfig },
|
||||||
) => {
|
) => {
|
||||||
const existing = await getDestination(id, organizationId);
|
const organizationId = getOrganizationId();
|
||||||
|
const existing = await getDestination(id);
|
||||||
|
|
||||||
if (!existing) {
|
if (!existing) {
|
||||||
throw new NotFoundError("Notification destination not found");
|
throw new NotFoundError("Notification destination not found");
|
||||||
|
|
@ -217,15 +221,16 @@ const updateDestination = async (
|
||||||
return updated;
|
return updated;
|
||||||
};
|
};
|
||||||
|
|
||||||
const deleteDestination = async (id: number, organizationId: string) => {
|
const deleteDestination = async (id: number) => {
|
||||||
await getDestination(id, organizationId);
|
const organizationId = getOrganizationId();
|
||||||
|
await getDestination(id);
|
||||||
await db
|
await db
|
||||||
.delete(notificationDestinationsTable)
|
.delete(notificationDestinationsTable)
|
||||||
.where(and(eq(notificationDestinationsTable.id, id), eq(notificationDestinationsTable.organizationId, organizationId)));
|
.where(and(eq(notificationDestinationsTable.id, id), eq(notificationDestinationsTable.organizationId, organizationId)));
|
||||||
};
|
};
|
||||||
|
|
||||||
const testDestination = async (id: number, organizationId: string) => {
|
const testDestination = async (id: number) => {
|
||||||
const destination = await getDestination(id, organizationId);
|
const destination = await getDestination(id);
|
||||||
|
|
||||||
if (!destination.enabled) {
|
if (!destination.enabled) {
|
||||||
throw new ConflictError("Cannot test disabled notification destination");
|
throw new ConflictError("Cannot test disabled notification destination");
|
||||||
|
|
|
||||||
|
|
@ -42,13 +42,13 @@ import { requireAuth } from "../auth/auth.middleware";
|
||||||
export const repositoriesController = new Hono()
|
export const repositoriesController = new Hono()
|
||||||
.use(requireAuth)
|
.use(requireAuth)
|
||||||
.get("/", listRepositoriesDto, async (c) => {
|
.get("/", listRepositoriesDto, async (c) => {
|
||||||
const repositories = await repositoriesService.listRepositories(c.get("organizationId"));
|
const repositories = await repositoriesService.listRepositories();
|
||||||
|
|
||||||
return c.json<ListRepositoriesDto>(repositories, 200);
|
return c.json<ListRepositoriesDto>(repositories, 200);
|
||||||
})
|
})
|
||||||
.post("/", createRepositoryDto, validator("json", createRepositoryBody), async (c) => {
|
.post("/", createRepositoryDto, validator("json", createRepositoryBody), async (c) => {
|
||||||
const body = c.req.valid("json");
|
const body = c.req.valid("json");
|
||||||
const res = await repositoriesService.createRepository(body.name, body.config, c.get("organizationId"), body.compressionMode);
|
const res = await repositoriesService.createRepository(body.name, body.config, body.compressionMode);
|
||||||
|
|
||||||
return c.json({ message: "Repository created", repository: res.repository }, 201);
|
return c.json({ message: "Repository created", repository: res.repository }, 201);
|
||||||
})
|
})
|
||||||
|
|
@ -69,21 +69,20 @@ export const repositoriesController = new Hono()
|
||||||
})
|
})
|
||||||
.get("/:id", getRepositoryDto, async (c) => {
|
.get("/:id", getRepositoryDto, async (c) => {
|
||||||
const { id } = c.req.param();
|
const { id } = c.req.param();
|
||||||
const res = await repositoriesService.getRepository(id, c.get("organizationId"));
|
const res = await repositoriesService.getRepository(id);
|
||||||
|
|
||||||
return c.json<GetRepositoryDto>(res.repository, 200);
|
return c.json<GetRepositoryDto>(res.repository, 200);
|
||||||
})
|
})
|
||||||
.delete("/:id", deleteRepositoryDto, async (c) => {
|
.delete("/:id", deleteRepositoryDto, async (c) => {
|
||||||
const { id } = c.req.param();
|
const { id } = c.req.param();
|
||||||
await repositoriesService.deleteRepository(id, c.get("organizationId"));
|
await repositoriesService.deleteRepository(id);
|
||||||
|
|
||||||
return c.json<DeleteRepositoryDto>({ message: "Repository deleted" }, 200);
|
return c.json<DeleteRepositoryDto>({ message: "Repository deleted" }, 200);
|
||||||
})
|
})
|
||||||
.get("/:id/snapshots", listSnapshotsDto, validator("query", listSnapshotsFilters), async (c) => {
|
.get("/:id/snapshots", listSnapshotsDto, validator("query", listSnapshotsFilters), async (c) => {
|
||||||
const { id } = c.req.param();
|
const { id } = c.req.param();
|
||||||
const { backupId } = c.req.valid("query");
|
const { backupId } = c.req.valid("query");
|
||||||
|
const res = await repositoriesService.listSnapshots(id, backupId);
|
||||||
const res = await repositoriesService.listSnapshots(id, c.get("organizationId"), backupId);
|
|
||||||
|
|
||||||
const snapshots = res.map((snapshot) => {
|
const snapshots = res.map((snapshot) => {
|
||||||
const { summary } = snapshot;
|
const { summary } = snapshot;
|
||||||
|
|
@ -108,7 +107,7 @@ export const repositoriesController = new Hono()
|
||||||
})
|
})
|
||||||
.get("/:id/snapshots/:snapshotId", getSnapshotDetailsDto, async (c) => {
|
.get("/:id/snapshots/:snapshotId", getSnapshotDetailsDto, async (c) => {
|
||||||
const { id, snapshotId } = c.req.param();
|
const { id, snapshotId } = c.req.param();
|
||||||
const snapshot = await repositoriesService.getSnapshotDetails(id, c.get("organizationId"), snapshotId);
|
const snapshot = await repositoriesService.getSnapshotDetails(id, snapshotId);
|
||||||
|
|
||||||
let duration = 0;
|
let duration = 0;
|
||||||
if (snapshot.summary) {
|
if (snapshot.summary) {
|
||||||
|
|
@ -137,7 +136,7 @@ export const repositoriesController = new Hono()
|
||||||
const { path } = c.req.valid("query");
|
const { path } = c.req.valid("query");
|
||||||
|
|
||||||
const decodedPath = path ? decodeURIComponent(path) : undefined;
|
const decodedPath = path ? decodeURIComponent(path) : undefined;
|
||||||
const result = await repositoriesService.listSnapshotFiles(id, c.get("organizationId"), snapshotId, decodedPath);
|
const result = await repositoriesService.listSnapshotFiles(id, snapshotId, decodedPath);
|
||||||
|
|
||||||
c.header("Cache-Control", "max-age=300, stale-while-revalidate=600");
|
c.header("Cache-Control", "max-age=300, stale-while-revalidate=600");
|
||||||
|
|
||||||
|
|
@ -147,46 +146,40 @@ export const repositoriesController = new Hono()
|
||||||
.post("/:id/restore", restoreSnapshotDto, validator("json", restoreSnapshotBody), async (c) => {
|
.post("/:id/restore", restoreSnapshotDto, validator("json", restoreSnapshotBody), async (c) => {
|
||||||
const { id } = c.req.param();
|
const { id } = c.req.param();
|
||||||
const { snapshotId, ...options } = c.req.valid("json");
|
const { snapshotId, ...options } = c.req.valid("json");
|
||||||
|
const result = await repositoriesService.restoreSnapshot(id, snapshotId, options);
|
||||||
const result = await repositoriesService.restoreSnapshot(id, c.get("organizationId"), snapshotId, options);
|
|
||||||
|
|
||||||
return c.json<RestoreSnapshotDto>(result, 200);
|
return c.json<RestoreSnapshotDto>(result, 200);
|
||||||
})
|
})
|
||||||
.post("/:id/doctor", doctorRepositoryDto, async (c) => {
|
.post("/:id/doctor", doctorRepositoryDto, async (c) => {
|
||||||
const { id } = c.req.param();
|
const { id } = c.req.param();
|
||||||
|
const result = await repositoriesService.doctorRepository(id);
|
||||||
const result = await repositoriesService.doctorRepository(id, c.get("organizationId"));
|
|
||||||
|
|
||||||
return c.json<DoctorRepositoryDto>(result, 200);
|
return c.json<DoctorRepositoryDto>(result, 200);
|
||||||
})
|
})
|
||||||
.delete("/:id/snapshots/:snapshotId", deleteSnapshotDto, async (c) => {
|
.delete("/:id/snapshots/:snapshotId", deleteSnapshotDto, async (c) => {
|
||||||
const { id, snapshotId } = c.req.param();
|
const { id, snapshotId } = c.req.param();
|
||||||
|
await repositoriesService.deleteSnapshot(id, snapshotId);
|
||||||
await repositoriesService.deleteSnapshot(id, c.get("organizationId"), snapshotId);
|
|
||||||
|
|
||||||
return c.json<DeleteSnapshotDto>({ message: "Snapshot deleted" }, 200);
|
return c.json<DeleteSnapshotDto>({ message: "Snapshot deleted" }, 200);
|
||||||
})
|
})
|
||||||
.delete("/:id/snapshots", deleteSnapshotsDto, validator("json", deleteSnapshotsBody), async (c) => {
|
.delete("/:id/snapshots", deleteSnapshotsDto, validator("json", deleteSnapshotsBody), async (c) => {
|
||||||
const { id } = c.req.param();
|
const { id } = c.req.param();
|
||||||
const { snapshotIds } = c.req.valid("json");
|
const { snapshotIds } = c.req.valid("json");
|
||||||
|
await repositoriesService.deleteSnapshots(id, snapshotIds);
|
||||||
await repositoriesService.deleteSnapshots(id, c.get("organizationId"), snapshotIds);
|
|
||||||
|
|
||||||
return c.json<DeleteSnapshotsResponseDto>({ message: "Snapshots deleted" }, 200);
|
return c.json<DeleteSnapshotsResponseDto>({ message: "Snapshots deleted" }, 200);
|
||||||
})
|
})
|
||||||
.post("/:id/snapshots/tag", tagSnapshotsDto, validator("json", tagSnapshotsBody), async (c) => {
|
.post("/:id/snapshots/tag", tagSnapshotsDto, validator("json", tagSnapshotsBody), async (c) => {
|
||||||
const { id } = c.req.param();
|
const { id } = c.req.param();
|
||||||
const { snapshotIds, ...tags } = c.req.valid("json");
|
const { snapshotIds, ...tags } = c.req.valid("json");
|
||||||
|
await repositoriesService.tagSnapshots(id, snapshotIds, tags);
|
||||||
await repositoriesService.tagSnapshots(id, c.get("organizationId"), snapshotIds, tags);
|
|
||||||
|
|
||||||
return c.json<TagSnapshotsResponseDto>({ message: "Snapshots tagged" }, 200);
|
return c.json<TagSnapshotsResponseDto>({ message: "Snapshots tagged" }, 200);
|
||||||
})
|
})
|
||||||
.patch("/:id", updateRepositoryDto, validator("json", updateRepositoryBody), async (c) => {
|
.patch("/:id", updateRepositoryDto, validator("json", updateRepositoryBody), async (c) => {
|
||||||
const { id } = c.req.param();
|
const { id } = c.req.param();
|
||||||
const body = c.req.valid("json");
|
const body = c.req.valid("json");
|
||||||
|
const res = await repositoriesService.updateRepository(id, body);
|
||||||
const res = await repositoriesService.updateRepository(id, c.get("organizationId"), body);
|
|
||||||
|
|
||||||
return c.json<UpdateRepositoryDto>(res.repository, 200);
|
return c.json<UpdateRepositoryDto>(res.repository, 200);
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -16,8 +16,10 @@ import {
|
||||||
type OverwriteMode,
|
type OverwriteMode,
|
||||||
type RepositoryConfig,
|
type RepositoryConfig,
|
||||||
} from "~/schemas/restic";
|
} from "~/schemas/restic";
|
||||||
|
import { getOrganizationId } from "~/server/core/request-context";
|
||||||
|
|
||||||
const findRepository = async (idOrShortId: string, organizationId: string) => {
|
const findRepository = async (idOrShortId: string) => {
|
||||||
|
const organizationId = getOrganizationId();
|
||||||
return await db.query.repositoriesTable.findFirst({
|
return await db.query.repositoriesTable.findFirst({
|
||||||
where: and(
|
where: and(
|
||||||
or(eq(repositoriesTable.id, idOrShortId), eq(repositoriesTable.shortId, idOrShortId)),
|
or(eq(repositoriesTable.id, idOrShortId), eq(repositoriesTable.shortId, idOrShortId)),
|
||||||
|
|
@ -26,7 +28,8 @@ const findRepository = async (idOrShortId: string, organizationId: string) => {
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const listRepositories = async (organizationId: string) => {
|
const listRepositories = async () => {
|
||||||
|
const organizationId = getOrganizationId();
|
||||||
const repositories = await db.query.repositoriesTable.findMany({
|
const repositories = await db.query.repositoriesTable.findMany({
|
||||||
where: eq(repositoriesTable.organizationId, organizationId),
|
where: eq(repositoriesTable.organizationId, organizationId),
|
||||||
});
|
});
|
||||||
|
|
@ -72,12 +75,8 @@ const encryptConfig = async (config: RepositoryConfig): Promise<RepositoryConfig
|
||||||
return encryptedConfig as RepositoryConfig;
|
return encryptedConfig as RepositoryConfig;
|
||||||
};
|
};
|
||||||
|
|
||||||
const createRepository = async (
|
const createRepository = async (name: string, config: RepositoryConfig, compressionMode?: CompressionMode) => {
|
||||||
name: string,
|
const organizationId = getOrganizationId();
|
||||||
config: RepositoryConfig,
|
|
||||||
organizationId: string,
|
|
||||||
compressionMode?: CompressionMode,
|
|
||||||
) => {
|
|
||||||
const id = crypto.randomUUID();
|
const id = crypto.randomUUID();
|
||||||
const shortId = generateShortId();
|
const shortId = generateShortId();
|
||||||
|
|
||||||
|
|
@ -135,8 +134,8 @@ const createRepository = async (
|
||||||
throw new InternalServerError(`Failed to initialize repository: ${errorMessage}`);
|
throw new InternalServerError(`Failed to initialize repository: ${errorMessage}`);
|
||||||
};
|
};
|
||||||
|
|
||||||
const getRepository = async (id: string, organizationId: string) => {
|
const getRepository = async (id: string) => {
|
||||||
const repository = await findRepository(id, organizationId);
|
const repository = await findRepository(id);
|
||||||
|
|
||||||
if (!repository) {
|
if (!repository) {
|
||||||
throw new NotFoundError("Repository not found");
|
throw new NotFoundError("Repository not found");
|
||||||
|
|
@ -145,8 +144,8 @@ const getRepository = async (id: string, organizationId: string) => {
|
||||||
return { repository };
|
return { repository };
|
||||||
};
|
};
|
||||||
|
|
||||||
const deleteRepository = async (id: string, organizationId: string) => {
|
const deleteRepository = async (id: string) => {
|
||||||
const repository = await findRepository(id, organizationId);
|
const repository = await findRepository(id);
|
||||||
|
|
||||||
if (!repository) {
|
if (!repository) {
|
||||||
throw new NotFoundError("Repository not found");
|
throw new NotFoundError("Repository not found");
|
||||||
|
|
@ -170,8 +169,9 @@ const deleteRepository = async (id: string, organizationId: string) => {
|
||||||
*
|
*
|
||||||
* @returns List of snapshots
|
* @returns List of snapshots
|
||||||
*/
|
*/
|
||||||
const listSnapshots = async (id: string, organizationId: string, backupId?: string) => {
|
const listSnapshots = async (id: string, backupId?: string) => {
|
||||||
const repository = await findRepository(id, organizationId);
|
const organizationId = getOrganizationId();
|
||||||
|
const repository = await findRepository(id);
|
||||||
|
|
||||||
if (!repository) {
|
if (!repository) {
|
||||||
throw new NotFoundError("Repository not found");
|
throw new NotFoundError("Repository not found");
|
||||||
|
|
@ -201,8 +201,9 @@ const listSnapshots = async (id: string, organizationId: string, backupId?: stri
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const listSnapshotFiles = async (id: string, organizationId: string, snapshotId: string, path?: string) => {
|
const listSnapshotFiles = async (id: string, snapshotId: string, path?: string) => {
|
||||||
const repository = await findRepository(id, organizationId);
|
const organizationId = getOrganizationId();
|
||||||
|
const repository = await findRepository(id);
|
||||||
|
|
||||||
if (!repository) {
|
if (!repository) {
|
||||||
throw new NotFoundError("Repository not found");
|
throw new NotFoundError("Repository not found");
|
||||||
|
|
@ -246,7 +247,6 @@ const listSnapshotFiles = async (id: string, organizationId: string, snapshotId:
|
||||||
|
|
||||||
const restoreSnapshot = async (
|
const restoreSnapshot = async (
|
||||||
id: string,
|
id: string,
|
||||||
organizationId: string,
|
|
||||||
snapshotId: string,
|
snapshotId: string,
|
||||||
options?: {
|
options?: {
|
||||||
include?: string[];
|
include?: string[];
|
||||||
|
|
@ -257,7 +257,8 @@ const restoreSnapshot = async (
|
||||||
overwrite?: OverwriteMode;
|
overwrite?: OverwriteMode;
|
||||||
},
|
},
|
||||||
) => {
|
) => {
|
||||||
const repository = await findRepository(id, organizationId);
|
const organizationId = getOrganizationId();
|
||||||
|
const repository = await findRepository(id);
|
||||||
|
|
||||||
if (!repository) {
|
if (!repository) {
|
||||||
throw new NotFoundError("Repository not found");
|
throw new NotFoundError("Repository not found");
|
||||||
|
|
@ -280,8 +281,9 @@ const restoreSnapshot = async (
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const getSnapshotDetails = async (id: string, organizationId: string, snapshotId: string) => {
|
const getSnapshotDetails = async (id: string, snapshotId: string) => {
|
||||||
const repository = await findRepository(id, organizationId);
|
const organizationId = getOrganizationId();
|
||||||
|
const repository = await findRepository(id);
|
||||||
|
|
||||||
if (!repository) {
|
if (!repository) {
|
||||||
throw new NotFoundError("Repository not found");
|
throw new NotFoundError("Repository not found");
|
||||||
|
|
@ -309,8 +311,9 @@ const getSnapshotDetails = async (id: string, organizationId: string, snapshotId
|
||||||
return snapshot;
|
return snapshot;
|
||||||
};
|
};
|
||||||
|
|
||||||
const checkHealth = async (repositoryId: string, organizationId: string) => {
|
const checkHealth = async (repositoryId: string) => {
|
||||||
const repository = await findRepository(repositoryId, organizationId);
|
const organizationId = getOrganizationId();
|
||||||
|
const repository = await findRepository(repositoryId);
|
||||||
|
|
||||||
if (!repository) {
|
if (!repository) {
|
||||||
throw new NotFoundError("Repository not found");
|
throw new NotFoundError("Repository not found");
|
||||||
|
|
@ -335,8 +338,9 @@ const checkHealth = async (repositoryId: string, organizationId: string) => {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const doctorRepository = async (id: string, organizationId: string) => {
|
const doctorRepository = async (id: string) => {
|
||||||
const repository = await findRepository(id, organizationId);
|
const organizationId = getOrganizationId();
|
||||||
|
const repository = await findRepository(id);
|
||||||
|
|
||||||
if (!repository) {
|
if (!repository) {
|
||||||
throw new NotFoundError("Repository not found");
|
throw new NotFoundError("Repository not found");
|
||||||
|
|
@ -424,8 +428,9 @@ const doctorRepository = async (id: string, organizationId: string) => {
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
const deleteSnapshot = async (id: string, organizationId: string, snapshotId: string) => {
|
const deleteSnapshot = async (id: string, snapshotId: string) => {
|
||||||
const repository = await findRepository(id, organizationId);
|
const organizationId = getOrganizationId();
|
||||||
|
const repository = await findRepository(id);
|
||||||
|
|
||||||
if (!repository) {
|
if (!repository) {
|
||||||
throw new NotFoundError("Repository not found");
|
throw new NotFoundError("Repository not found");
|
||||||
|
|
@ -441,8 +446,9 @@ const deleteSnapshot = async (id: string, organizationId: string, snapshotId: st
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const deleteSnapshots = async (id: string, organizationId: string, snapshotIds: string[]) => {
|
const deleteSnapshots = async (id: string, snapshotIds: string[]) => {
|
||||||
const repository = await findRepository(id, organizationId);
|
const organizationId = getOrganizationId();
|
||||||
|
const repository = await findRepository(id);
|
||||||
|
|
||||||
if (!repository) {
|
if (!repository) {
|
||||||
throw new NotFoundError("Repository not found");
|
throw new NotFoundError("Repository not found");
|
||||||
|
|
@ -460,13 +466,9 @@ const deleteSnapshots = async (id: string, organizationId: string, snapshotIds:
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const tagSnapshots = async (
|
const tagSnapshots = async (id: string, snapshotIds: string[], tags: { add?: string[]; remove?: string[]; set?: string[] }) => {
|
||||||
id: string,
|
const organizationId = getOrganizationId();
|
||||||
organizationId: string,
|
const repository = await findRepository(id);
|
||||||
snapshotIds: string[],
|
|
||||||
tags: { add?: string[]; remove?: string[]; set?: string[] },
|
|
||||||
) => {
|
|
||||||
const repository = await findRepository(id, organizationId);
|
|
||||||
|
|
||||||
if (!repository) {
|
if (!repository) {
|
||||||
throw new NotFoundError("Repository not found");
|
throw new NotFoundError("Repository not found");
|
||||||
|
|
@ -484,12 +486,8 @@ const tagSnapshots = async (
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const updateRepository = async (
|
const updateRepository = async (id: string, updates: { name?: string; compressionMode?: CompressionMode }) => {
|
||||||
id: string,
|
const existing = await findRepository(id);
|
||||||
organizationId: string,
|
|
||||||
updates: { name?: string; compressionMode?: CompressionMode },
|
|
||||||
) => {
|
|
||||||
const existing = await findRepository(id, organizationId);
|
|
||||||
|
|
||||||
if (!existing) {
|
if (!existing) {
|
||||||
throw new NotFoundError("Repository not found");
|
throw new NotFoundError("Repository not found");
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,7 @@ import { eq } from "drizzle-orm";
|
||||||
import { verifyUserPassword } from "../auth/helpers";
|
import { verifyUserPassword } from "../auth/helpers";
|
||||||
import { cryptoUtils } from "../../utils/crypto";
|
import { cryptoUtils } from "../../utils/crypto";
|
||||||
import { createMiddleware } from "hono/factory";
|
import { createMiddleware } from "hono/factory";
|
||||||
|
import { getOrganizationId } from "~/server/core/request-context";
|
||||||
|
|
||||||
const requireGlobalAdmin = createMiddleware(async (c, next) => {
|
const requireGlobalAdmin = createMiddleware(async (c, next) => {
|
||||||
const user = c.get("user");
|
const user = c.get("user");
|
||||||
|
|
@ -68,7 +69,7 @@ export const systemController = new Hono()
|
||||||
validator("json", downloadResticPasswordBodySchema),
|
validator("json", downloadResticPasswordBodySchema),
|
||||||
async (c) => {
|
async (c) => {
|
||||||
const user = c.get("user");
|
const user = c.get("user");
|
||||||
const organizationId = c.get("organizationId");
|
const organizationId = getOrganizationId();
|
||||||
const body = c.req.valid("json");
|
const body = c.req.valid("json");
|
||||||
|
|
||||||
const isPasswordValid = await verifyUserPassword({ password: body.password, userId: user.id });
|
const isPasswordValid = await verifyUserPassword({ password: body.password, userId: user.id });
|
||||||
|
|
|
||||||
|
|
@ -29,13 +29,13 @@ import { requireAuth } from "../auth/auth.middleware";
|
||||||
export const volumeController = new Hono()
|
export const volumeController = new Hono()
|
||||||
.use(requireAuth)
|
.use(requireAuth)
|
||||||
.get("/", listVolumesDto, async (c) => {
|
.get("/", listVolumesDto, async (c) => {
|
||||||
const volumes = await volumeService.listVolumes(c.get("organizationId"));
|
const volumes = await volumeService.listVolumes();
|
||||||
|
|
||||||
return c.json<ListVolumesDto>(volumes, 200);
|
return c.json<ListVolumesDto>(volumes, 200);
|
||||||
})
|
})
|
||||||
.post("/", createVolumeDto, validator("json", createVolumeBody), async (c) => {
|
.post("/", createVolumeDto, validator("json", createVolumeBody), async (c) => {
|
||||||
const body = c.req.valid("json");
|
const body = c.req.valid("json");
|
||||||
const res = await volumeService.createVolume(body.name, body.config, c.get("organizationId"));
|
const res = await volumeService.createVolume(body.name, body.config);
|
||||||
|
|
||||||
const response = {
|
const response = {
|
||||||
...res.volume,
|
...res.volume,
|
||||||
|
|
@ -52,13 +52,13 @@ export const volumeController = new Hono()
|
||||||
})
|
})
|
||||||
.delete("/:id", deleteVolumeDto, async (c) => {
|
.delete("/:id", deleteVolumeDto, async (c) => {
|
||||||
const { id } = c.req.param();
|
const { id } = c.req.param();
|
||||||
await volumeService.deleteVolume(id, c.get("organizationId"));
|
await volumeService.deleteVolume(id);
|
||||||
|
|
||||||
return c.json({ message: "Volume deleted" }, 200);
|
return c.json({ message: "Volume deleted" }, 200);
|
||||||
})
|
})
|
||||||
.get("/:id", getVolumeDto, async (c) => {
|
.get("/:id", getVolumeDto, async (c) => {
|
||||||
const { id } = c.req.param();
|
const { id } = c.req.param();
|
||||||
const res = await volumeService.getVolume(id, c.get("organizationId"));
|
const res = await volumeService.getVolume(id);
|
||||||
|
|
||||||
const response = {
|
const response = {
|
||||||
volume: {
|
volume: {
|
||||||
|
|
@ -77,7 +77,7 @@ export const volumeController = new Hono()
|
||||||
.put("/:id", updateVolumeDto, validator("json", updateVolumeBody), async (c) => {
|
.put("/:id", updateVolumeDto, validator("json", updateVolumeBody), async (c) => {
|
||||||
const { id } = c.req.param();
|
const { id } = c.req.param();
|
||||||
const body = c.req.valid("json");
|
const body = c.req.valid("json");
|
||||||
const res = await volumeService.updateVolume(id, body, c.get("organizationId"));
|
const res = await volumeService.updateVolume(id, body);
|
||||||
|
|
||||||
const response = {
|
const response = {
|
||||||
...res.volume,
|
...res.volume,
|
||||||
|
|
@ -88,26 +88,26 @@ export const volumeController = new Hono()
|
||||||
})
|
})
|
||||||
.post("/:id/mount", mountVolumeDto, async (c) => {
|
.post("/:id/mount", mountVolumeDto, async (c) => {
|
||||||
const { id } = c.req.param();
|
const { id } = c.req.param();
|
||||||
const { error, status } = await volumeService.mountVolume(id, c.get("organizationId"));
|
const { error, status } = await volumeService.mountVolume(id);
|
||||||
|
|
||||||
return c.json({ error, status }, error ? 500 : 200);
|
return c.json({ error, status }, error ? 500 : 200);
|
||||||
})
|
})
|
||||||
.post("/:id/unmount", unmountVolumeDto, async (c) => {
|
.post("/:id/unmount", unmountVolumeDto, async (c) => {
|
||||||
const { id } = c.req.param();
|
const { id } = c.req.param();
|
||||||
const { error, status } = await volumeService.unmountVolume(id, c.get("organizationId"));
|
const { error, status } = await volumeService.unmountVolume(id);
|
||||||
|
|
||||||
return c.json({ error, status }, error ? 500 : 200);
|
return c.json({ error, status }, error ? 500 : 200);
|
||||||
})
|
})
|
||||||
.post("/:id/health-check", healthCheckDto, async (c) => {
|
.post("/:id/health-check", healthCheckDto, async (c) => {
|
||||||
const { id } = c.req.param();
|
const { id } = c.req.param();
|
||||||
const { error, status } = await volumeService.checkHealth(id, c.get("organizationId"));
|
const { error, status } = await volumeService.checkHealth(id);
|
||||||
|
|
||||||
return c.json({ error, status }, 200);
|
return c.json({ error, status }, 200);
|
||||||
})
|
})
|
||||||
.get("/:id/files", listFilesDto, async (c) => {
|
.get("/:id/files", listFilesDto, async (c) => {
|
||||||
const { id } = c.req.param();
|
const { id } = c.req.param();
|
||||||
const subPath = c.req.query("path");
|
const subPath = c.req.query("path");
|
||||||
const result = await volumeService.listFiles(id, c.get("organizationId"), subPath);
|
const result = await volumeService.listFiles(id, subPath);
|
||||||
|
|
||||||
const response = {
|
const response = {
|
||||||
files: result.files,
|
files: result.files,
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,7 @@ import { logger } from "../../utils/logger";
|
||||||
import { serverEvents } from "../../core/events";
|
import { serverEvents } from "../../core/events";
|
||||||
import { volumeConfigSchema, type BackendConfig } from "~/schemas/volumes";
|
import { volumeConfigSchema, type BackendConfig } from "~/schemas/volumes";
|
||||||
import { type } from "arktype";
|
import { type } from "arktype";
|
||||||
|
import { getOrganizationId } from "~/server/core/request-context";
|
||||||
|
|
||||||
async function encryptSensitiveFields(config: BackendConfig): Promise<BackendConfig> {
|
async function encryptSensitiveFields(config: BackendConfig): Promise<BackendConfig> {
|
||||||
switch (config.backend) {
|
switch (config.backend) {
|
||||||
|
|
@ -42,7 +43,8 @@ async function encryptSensitiveFields(config: BackendConfig): Promise<BackendCon
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const listVolumes = async (organizationId: string) => {
|
const listVolumes = async () => {
|
||||||
|
const organizationId = getOrganizationId();
|
||||||
const volumes = await db.query.volumesTable.findMany({
|
const volumes = await db.query.volumesTable.findMany({
|
||||||
where: eq(volumesTable.organizationId, organizationId),
|
where: eq(volumesTable.organizationId, organizationId),
|
||||||
});
|
});
|
||||||
|
|
@ -50,7 +52,8 @@ const listVolumes = async (organizationId: string) => {
|
||||||
return volumes;
|
return volumes;
|
||||||
};
|
};
|
||||||
|
|
||||||
const findVolume = async (idOrShortId: string | number, organizationId: string) => {
|
const findVolume = async (idOrShortId: string | number) => {
|
||||||
|
const organizationId = getOrganizationId();
|
||||||
const isNumeric = typeof idOrShortId === "number" || /^\d+$/.test(String(idOrShortId));
|
const isNumeric = typeof idOrShortId === "number" || /^\d+$/.test(String(idOrShortId));
|
||||||
return await db.query.volumesTable.findFirst({
|
return await db.query.volumesTable.findFirst({
|
||||||
where: and(
|
where: and(
|
||||||
|
|
@ -60,7 +63,8 @@ const findVolume = async (idOrShortId: string | number, organizationId: string)
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const createVolume = async (name: string, backendConfig: BackendConfig, organizationId: string) => {
|
const createVolume = async (name: string, backendConfig: BackendConfig) => {
|
||||||
|
const organizationId = getOrganizationId();
|
||||||
const slug = slugify(name, { lower: true, strict: true });
|
const slug = slugify(name, { lower: true, strict: true });
|
||||||
|
|
||||||
const existing = await db.query.volumesTable.findFirst({
|
const existing = await db.query.volumesTable.findFirst({
|
||||||
|
|
@ -100,8 +104,9 @@ const createVolume = async (name: string, backendConfig: BackendConfig, organiza
|
||||||
return { volume: created, status: 201 };
|
return { volume: created, status: 201 };
|
||||||
};
|
};
|
||||||
|
|
||||||
const deleteVolume = async (idOrShortId: string | number, organizationId: string) => {
|
const deleteVolume = async (idOrShortId: string | number) => {
|
||||||
const volume = await findVolume(idOrShortId, organizationId);
|
const organizationId = getOrganizationId();
|
||||||
|
const volume = await findVolume(idOrShortId);
|
||||||
|
|
||||||
if (!volume) {
|
if (!volume) {
|
||||||
throw new NotFoundError("Volume not found");
|
throw new NotFoundError("Volume not found");
|
||||||
|
|
@ -114,8 +119,9 @@ const deleteVolume = async (idOrShortId: string | number, organizationId: string
|
||||||
.where(and(eq(volumesTable.id, volume.id), eq(volumesTable.organizationId, organizationId)));
|
.where(and(eq(volumesTable.id, volume.id), eq(volumesTable.organizationId, organizationId)));
|
||||||
};
|
};
|
||||||
|
|
||||||
const mountVolume = async (idOrShortId: string | number, organizationId: string) => {
|
const mountVolume = async (idOrShortId: string | number) => {
|
||||||
const volume = await findVolume(idOrShortId, organizationId);
|
const organizationId = getOrganizationId();
|
||||||
|
const volume = await findVolume(idOrShortId);
|
||||||
|
|
||||||
if (!volume) {
|
if (!volume) {
|
||||||
throw new NotFoundError("Volume not found");
|
throw new NotFoundError("Volume not found");
|
||||||
|
|
@ -136,8 +142,9 @@ const mountVolume = async (idOrShortId: string | number, organizationId: string)
|
||||||
return { error, status };
|
return { error, status };
|
||||||
};
|
};
|
||||||
|
|
||||||
const unmountVolume = async (idOrShortId: string | number, organizationId: string) => {
|
const unmountVolume = async (idOrShortId: string | number) => {
|
||||||
const volume = await findVolume(idOrShortId, organizationId);
|
const organizationId = getOrganizationId();
|
||||||
|
const volume = await findVolume(idOrShortId);
|
||||||
|
|
||||||
if (!volume) {
|
if (!volume) {
|
||||||
throw new NotFoundError("Volume not found");
|
throw new NotFoundError("Volume not found");
|
||||||
|
|
@ -158,8 +165,8 @@ const unmountVolume = async (idOrShortId: string | number, organizationId: strin
|
||||||
return { error, status };
|
return { error, status };
|
||||||
};
|
};
|
||||||
|
|
||||||
const getVolume = async (idOrShortId: string | number, organizationId: string) => {
|
const getVolume = async (idOrShortId: string | number) => {
|
||||||
const volume = await findVolume(idOrShortId, organizationId);
|
const volume = await findVolume(idOrShortId);
|
||||||
|
|
||||||
if (!volume) {
|
if (!volume) {
|
||||||
throw new NotFoundError("Volume not found");
|
throw new NotFoundError("Volume not found");
|
||||||
|
|
@ -176,8 +183,9 @@ const getVolume = async (idOrShortId: string | number, organizationId: string) =
|
||||||
return { volume, statfs };
|
return { volume, statfs };
|
||||||
};
|
};
|
||||||
|
|
||||||
const updateVolume = async (idOrShortId: string | number, volumeData: UpdateVolumeBody, organizationId: string) => {
|
const updateVolume = async (idOrShortId: string | number, volumeData: UpdateVolumeBody) => {
|
||||||
const existing = await findVolume(idOrShortId, organizationId);
|
const organizationId = getOrganizationId();
|
||||||
|
const existing = await findVolume(idOrShortId);
|
||||||
|
|
||||||
if (!existing) {
|
if (!existing) {
|
||||||
throw new NotFoundError("Volume not found");
|
throw new NotFoundError("Volume not found");
|
||||||
|
|
@ -188,7 +196,11 @@ const updateVolume = async (idOrShortId: string | number, volumeData: UpdateVolu
|
||||||
const newSlug = slugify(volumeData.name, { lower: true, strict: true });
|
const newSlug = slugify(volumeData.name, { lower: true, strict: true });
|
||||||
|
|
||||||
const conflict = await db.query.volumesTable.findFirst({
|
const conflict = await db.query.volumesTable.findFirst({
|
||||||
where: and(eq(volumesTable.name, newSlug), ne(volumesTable.id, existing.id), eq(volumesTable.organizationId, organizationId)),
|
where: and(
|
||||||
|
eq(volumesTable.name, newSlug),
|
||||||
|
ne(volumesTable.id, existing.id),
|
||||||
|
eq(volumesTable.organizationId, organizationId),
|
||||||
|
),
|
||||||
});
|
});
|
||||||
|
|
||||||
if (conflict) {
|
if (conflict) {
|
||||||
|
|
@ -277,8 +289,9 @@ const testConnection = async (backendConfig: BackendConfig) => {
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
const checkHealth = async (idOrShortId: string | number, organizationId: string) => {
|
const checkHealth = async (idOrShortId: string | number) => {
|
||||||
const volume = await findVolume(idOrShortId, organizationId);
|
const organizationId = getOrganizationId();
|
||||||
|
const volume = await findVolume(idOrShortId);
|
||||||
|
|
||||||
if (!volume) {
|
if (!volume) {
|
||||||
throw new NotFoundError("Volume not found");
|
throw new NotFoundError("Volume not found");
|
||||||
|
|
@ -299,8 +312,8 @@ const checkHealth = async (idOrShortId: string | number, organizationId: string)
|
||||||
return { status, error };
|
return { status, error };
|
||||||
};
|
};
|
||||||
|
|
||||||
const listFiles = async (idOrShortId: string | number, organizationId: string, subPath?: string) => {
|
const listFiles = async (idOrShortId: string | number, subPath?: string) => {
|
||||||
const volume = await findVolume(idOrShortId, organizationId);
|
const volume = await findVolume(idOrShortId);
|
||||||
|
|
||||||
if (!volume) {
|
if (!volume) {
|
||||||
throw new NotFoundError("Volume not found");
|
throw new NotFoundError("Volume not found");
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue