refactor: filter all backend queries to surface only organization specific entities
This commit is contained in:
parent
0fafe4114b
commit
5895bc3ae3
18 changed files with 1934 additions and 157 deletions
95
app/drizzle/0040_tidy_maelstrom.sql
Normal file
95
app/drizzle/0040_tidy_maelstrom.sql
Normal file
|
|
@ -0,0 +1,95 @@
|
|||
PRAGMA foreign_keys=OFF;--> statement-breakpoint
|
||||
CREATE TABLE `__new_backup_schedules_table` (
|
||||
`id` integer PRIMARY KEY AUTOINCREMENT NOT NULL,
|
||||
`short_id` text NOT NULL,
|
||||
`name` text NOT NULL,
|
||||
`volume_id` integer NOT NULL,
|
||||
`repository_id` text NOT NULL,
|
||||
`enabled` integer DEFAULT true NOT NULL,
|
||||
`cron_expression` text NOT NULL,
|
||||
`retention_policy` text,
|
||||
`exclude_patterns` text DEFAULT '[]',
|
||||
`exclude_if_present` text DEFAULT '[]',
|
||||
`include_patterns` text DEFAULT '[]',
|
||||
`last_backup_at` integer,
|
||||
`last_backup_status` text,
|
||||
`last_backup_error` text,
|
||||
`next_backup_at` integer,
|
||||
`one_file_system` integer DEFAULT false NOT NULL,
|
||||
`sort_order` integer DEFAULT 0 NOT NULL,
|
||||
`created_at` integer DEFAULT (unixepoch() * 1000) NOT NULL,
|
||||
`updated_at` integer DEFAULT (unixepoch() * 1000) NOT NULL,
|
||||
`organization_id` text NOT NULL,
|
||||
FOREIGN KEY (`volume_id`) REFERENCES `volumes_table`(`id`) ON UPDATE no action ON DELETE cascade,
|
||||
FOREIGN KEY (`repository_id`) REFERENCES `repositories_table`(`id`) ON UPDATE no action ON DELETE cascade,
|
||||
FOREIGN KEY (`organization_id`) REFERENCES `organization`(`id`) ON UPDATE no action ON DELETE cascade
|
||||
);
|
||||
--> statement-breakpoint
|
||||
INSERT INTO `__new_backup_schedules_table`("id", "short_id", "name", "volume_id", "repository_id", "enabled", "cron_expression", "retention_policy", "exclude_patterns", "exclude_if_present", "include_patterns", "last_backup_at", "last_backup_status", "last_backup_error", "next_backup_at", "one_file_system", "sort_order", "created_at", "updated_at", "organization_id") SELECT "id", "short_id", "name", "volume_id", "repository_id", "enabled", "cron_expression", "retention_policy", "exclude_patterns", "exclude_if_present", "include_patterns", "last_backup_at", "last_backup_status", "last_backup_error", "next_backup_at", "one_file_system", "sort_order", "created_at", "updated_at", "organization_id" FROM `backup_schedules_table`;--> statement-breakpoint
|
||||
DROP TABLE `backup_schedules_table`;--> statement-breakpoint
|
||||
ALTER TABLE `__new_backup_schedules_table` RENAME TO `backup_schedules_table`;--> statement-breakpoint
|
||||
PRAGMA foreign_keys=ON;--> statement-breakpoint
|
||||
CREATE UNIQUE INDEX `backup_schedules_table_short_id_unique` ON `backup_schedules_table` (`short_id`);--> statement-breakpoint
|
||||
CREATE UNIQUE INDEX `backup_schedules_table_name_unique` ON `backup_schedules_table` (`name`);--> statement-breakpoint
|
||||
CREATE TABLE `__new_notification_destinations_table` (
|
||||
`id` integer PRIMARY KEY AUTOINCREMENT NOT NULL,
|
||||
`name` text NOT NULL,
|
||||
`enabled` integer DEFAULT true NOT NULL,
|
||||
`type` text NOT NULL,
|
||||
`config` text NOT NULL,
|
||||
`created_at` integer DEFAULT (unixepoch() * 1000) NOT NULL,
|
||||
`updated_at` integer DEFAULT (unixepoch() * 1000) NOT NULL,
|
||||
`organization_id` text NOT NULL,
|
||||
FOREIGN KEY (`organization_id`) REFERENCES `organization`(`id`) ON UPDATE no action ON DELETE cascade
|
||||
);
|
||||
--> statement-breakpoint
|
||||
INSERT INTO `__new_notification_destinations_table`("id", "name", "enabled", "type", "config", "created_at", "updated_at", "organization_id") SELECT "id", "name", "enabled", "type", "config", "created_at", "updated_at", "organization_id" FROM `notification_destinations_table`;--> statement-breakpoint
|
||||
DROP TABLE `notification_destinations_table`;--> statement-breakpoint
|
||||
ALTER TABLE `__new_notification_destinations_table` RENAME TO `notification_destinations_table`;--> statement-breakpoint
|
||||
CREATE UNIQUE INDEX `notification_destinations_table_name_unique` ON `notification_destinations_table` (`name`);--> statement-breakpoint
|
||||
CREATE TABLE `__new_repositories_table` (
|
||||
`id` text PRIMARY KEY NOT NULL,
|
||||
`short_id` text NOT NULL,
|
||||
`name` text NOT NULL,
|
||||
`type` text NOT NULL,
|
||||
`config` text NOT NULL,
|
||||
`compression_mode` text DEFAULT 'auto',
|
||||
`status` text DEFAULT 'unknown',
|
||||
`last_checked` integer,
|
||||
`last_error` text,
|
||||
`upload_limit_enabled` integer DEFAULT false NOT NULL,
|
||||
`upload_limit_value` real DEFAULT 1 NOT NULL,
|
||||
`upload_limit_unit` text DEFAULT 'Mbps' NOT NULL,
|
||||
`download_limit_enabled` integer DEFAULT false NOT NULL,
|
||||
`download_limit_value` real DEFAULT 1 NOT NULL,
|
||||
`download_limit_unit` text DEFAULT 'Mbps' NOT NULL,
|
||||
`created_at` integer DEFAULT (unixepoch() * 1000) NOT NULL,
|
||||
`updated_at` integer DEFAULT (unixepoch() * 1000) NOT NULL,
|
||||
`organization_id` text NOT NULL,
|
||||
FOREIGN KEY (`organization_id`) REFERENCES `organization`(`id`) ON UPDATE no action ON DELETE cascade
|
||||
);
|
||||
--> statement-breakpoint
|
||||
INSERT INTO `__new_repositories_table`("id", "short_id", "name", "type", "config", "compression_mode", "status", "last_checked", "last_error", "upload_limit_enabled", "upload_limit_value", "upload_limit_unit", "download_limit_enabled", "download_limit_value", "download_limit_unit", "created_at", "updated_at", "organization_id") SELECT "id", "short_id", "name", "type", "config", "compression_mode", "status", "last_checked", "last_error", "upload_limit_enabled", "upload_limit_value", "upload_limit_unit", "download_limit_enabled", "download_limit_value", "download_limit_unit", "created_at", "updated_at", "organization_id" FROM `repositories_table`;--> statement-breakpoint
|
||||
DROP TABLE `repositories_table`;--> statement-breakpoint
|
||||
ALTER TABLE `__new_repositories_table` RENAME TO `repositories_table`;--> statement-breakpoint
|
||||
CREATE UNIQUE INDEX `repositories_table_short_id_unique` ON `repositories_table` (`short_id`);--> statement-breakpoint
|
||||
CREATE TABLE `__new_volumes_table` (
|
||||
`id` integer PRIMARY KEY AUTOINCREMENT NOT NULL,
|
||||
`short_id` text NOT NULL,
|
||||
`name` text NOT NULL,
|
||||
`type` text NOT NULL,
|
||||
`status` text DEFAULT 'unmounted' NOT NULL,
|
||||
`last_error` text,
|
||||
`last_health_check` integer DEFAULT (unixepoch() * 1000) NOT NULL,
|
||||
`created_at` integer DEFAULT (unixepoch() * 1000) NOT NULL,
|
||||
`updated_at` integer DEFAULT (unixepoch() * 1000) NOT NULL,
|
||||
`config` text NOT NULL,
|
||||
`auto_remount` integer DEFAULT true NOT NULL,
|
||||
`organization_id` text NOT NULL,
|
||||
FOREIGN KEY (`organization_id`) REFERENCES `organization`(`id`) ON UPDATE no action ON DELETE cascade
|
||||
);
|
||||
--> statement-breakpoint
|
||||
INSERT INTO `__new_volumes_table`("id", "short_id", "name", "type", "status", "last_error", "last_health_check", "created_at", "updated_at", "config", "auto_remount", "organization_id") SELECT "id", "short_id", "name", "type", "status", "last_error", "last_health_check", "created_at", "updated_at", "config", "auto_remount", "organization_id" FROM `volumes_table`;--> statement-breakpoint
|
||||
DROP TABLE `volumes_table`;--> statement-breakpoint
|
||||
ALTER TABLE `__new_volumes_table` RENAME TO `volumes_table`;--> statement-breakpoint
|
||||
CREATE UNIQUE INDEX `volumes_table_short_id_unique` ON `volumes_table` (`short_id`);
|
||||
1626
app/drizzle/meta/0040_snapshot.json
Normal file
1626
app/drizzle/meta/0040_snapshot.json
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -281,6 +281,13 @@
|
|||
"when": 1768658359845,
|
||||
"tag": "0039_backfill-entities-org-id",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 40,
|
||||
"version": "6",
|
||||
"when": 1768659604809,
|
||||
"tag": "0040_tidy_maelstrom",
|
||||
"breakpoints": true
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -185,7 +185,9 @@ export const volumesTable = sqliteTable("volumes_table", {
|
|||
.default(sql`(unixepoch() * 1000)`),
|
||||
config: text("config", { mode: "json" }).$type<typeof volumeConfigSchema.inferOut>().notNull(),
|
||||
autoRemount: int("auto_remount", { mode: "boolean" }).notNull().default(true),
|
||||
organizationId: text("organization_id").references(() => organization.id, { onDelete: "cascade" }),
|
||||
organizationId: text("organization_id")
|
||||
.notNull()
|
||||
.references(() => organization.id, { onDelete: "cascade" }),
|
||||
});
|
||||
export type Volume = typeof volumesTable.$inferSelect;
|
||||
export type VolumeInsert = typeof volumesTable.$inferInsert;
|
||||
|
|
@ -215,7 +217,9 @@ export const repositoriesTable = sqliteTable("repositories_table", {
|
|||
updatedAt: int("updated_at", { mode: "number" })
|
||||
.notNull()
|
||||
.default(sql`(unixepoch() * 1000)`),
|
||||
organizationId: text("organization_id").references(() => organization.id, { onDelete: "cascade" }),
|
||||
organizationId: text("organization_id")
|
||||
.notNull()
|
||||
.references(() => organization.id, { onDelete: "cascade" }),
|
||||
});
|
||||
export type Repository = typeof repositoriesTable.$inferSelect;
|
||||
export type RepositoryInsert = typeof repositoriesTable.$inferInsert;
|
||||
|
|
@ -259,7 +263,9 @@ export const backupSchedulesTable = sqliteTable("backup_schedules_table", {
|
|||
updatedAt: int("updated_at", { mode: "number" })
|
||||
.notNull()
|
||||
.default(sql`(unixepoch() * 1000)`),
|
||||
organizationId: text("organization_id").references(() => organization.id, { onDelete: "cascade" }),
|
||||
organizationId: text("organization_id")
|
||||
.notNull()
|
||||
.references(() => organization.id, { onDelete: "cascade" }),
|
||||
});
|
||||
export type BackupScheduleInsert = typeof backupSchedulesTable.$inferInsert;
|
||||
|
||||
|
|
@ -280,7 +286,9 @@ export const notificationDestinationsTable = sqliteTable("notification_destinati
|
|||
updatedAt: int("updated_at", { mode: "number" })
|
||||
.notNull()
|
||||
.default(sql`(unixepoch() * 1000)`),
|
||||
organizationId: text("organization_id").references(() => organization.id, { onDelete: "cascade" }),
|
||||
organizationId: text("organization_id")
|
||||
.notNull()
|
||||
.references(() => organization.id, { onDelete: "cascade" }),
|
||||
});
|
||||
export type NotificationDestination = typeof notificationDestinationsTable.$inferSelect;
|
||||
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ export class VolumeAutoRemountJob extends Job {
|
|||
for (const volume of volumes) {
|
||||
if (volume.autoRemount) {
|
||||
try {
|
||||
await volumeService.mountVolume(volume.name);
|
||||
await volumeService.mountVolume(volume.name, volume.organizationId);
|
||||
} catch (err) {
|
||||
logger.error(`Failed to auto-remount volume ${volume.name}:`, err);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,26 +1,38 @@
|
|||
import { Job } from "../core/scheduler";
|
||||
import { backupsService } from "../modules/backups/backups.service";
|
||||
import { logger } from "../utils/logger";
|
||||
import { db } from "../db/db";
|
||||
|
||||
export class BackupExecutionJob extends Job {
|
||||
async run() {
|
||||
logger.debug("Checking for backup schedules to execute...");
|
||||
|
||||
const scheduleIds = await backupsService.getSchedulesToExecute();
|
||||
const organizations = await db.query.organization.findMany({});
|
||||
|
||||
if (scheduleIds.length === 0) {
|
||||
let totalExecuted = 0;
|
||||
|
||||
for (const org of organizations) {
|
||||
const scheduleIds = await backupsService.getSchedulesToExecute(org.id);
|
||||
|
||||
if (scheduleIds.length === 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
logger.info(`Found ${scheduleIds.length} backup schedule(s) to execute for organization ${org.name}`);
|
||||
|
||||
for (const scheduleId of scheduleIds) {
|
||||
backupsService.executeBackup(scheduleId, org.id).catch((err) => {
|
||||
logger.error(`Error executing backup for schedule ${scheduleId}:`, err);
|
||||
});
|
||||
}
|
||||
|
||||
totalExecuted += scheduleIds.length;
|
||||
}
|
||||
|
||||
if (totalExecuted === 0) {
|
||||
logger.debug("No backup schedules to execute");
|
||||
return { done: true, timestamp: new Date(), executed: 0 };
|
||||
}
|
||||
|
||||
logger.info(`Found ${scheduleIds.length} backup schedule(s) to execute`);
|
||||
|
||||
for (const scheduleId of scheduleIds) {
|
||||
backupsService.executeBackup(scheduleId).catch((err) => {
|
||||
logger.error(`Error executing backup for schedule ${scheduleId}:`, err);
|
||||
});
|
||||
}
|
||||
|
||||
return { done: true, timestamp: new Date(), executed: scheduleIds.length };
|
||||
return { done: true, timestamp: new Date(), executed: totalExecuted };
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,10 +8,18 @@ import { logger } from "../utils/logger";
|
|||
import { executeUnmount } from "../modules/backends/utils/backend-utils";
|
||||
import { toMessage } from "../utils/errors";
|
||||
import { VOLUME_MOUNT_BASE } from "../core/constants";
|
||||
import { db } from "../db/db";
|
||||
|
||||
export class CleanupDanglingMountsJob extends Job {
|
||||
async run() {
|
||||
const allVolumes = await volumeService.listVolumes();
|
||||
const organizations = await db.query.organization.findMany({});
|
||||
|
||||
const allVolumes = [];
|
||||
for (const org of organizations) {
|
||||
const volumes = await volumeService.listVolumes(org.id);
|
||||
allVolumes.push(...volumes);
|
||||
}
|
||||
|
||||
const allSystemMounts = await readMountInfo();
|
||||
|
||||
for (const mount of allSystemMounts) {
|
||||
|
|
|
|||
|
|
@ -14,9 +14,9 @@ export class VolumeHealthCheckJob extends Job {
|
|||
});
|
||||
|
||||
for (const volume of volumes) {
|
||||
const { status } = await volumeService.checkHealth(volume.name);
|
||||
const { status } = await volumeService.checkHealth(volume.name, volume.organizationId);
|
||||
if (status === "error" && volume.autoRemount) {
|
||||
await volumeService.mountVolume(volume.name);
|
||||
await volumeService.mountVolume(volume.name, volume.organizationId);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ export class RepositoryHealthCheckJob extends Job {
|
|||
|
||||
for (const repository of repositories) {
|
||||
try {
|
||||
await repositoriesService.checkHealth(repository.id);
|
||||
await repositoriesService.checkHealth(repository.id, repository.organizationId);
|
||||
} catch (error) {
|
||||
logger.error(`Health check failed for repository ${repository.name}:`, error);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -46,27 +46,27 @@ import { requireAuth } from "../auth/auth.middleware";
|
|||
export const backupScheduleController = new Hono()
|
||||
.use(requireAuth)
|
||||
.get("/", listBackupSchedulesDto, async (c) => {
|
||||
const schedules = await backupsService.listSchedules();
|
||||
const schedules = await backupsService.listSchedules(c.get("organizationId"));
|
||||
|
||||
return c.json<ListBackupSchedulesResponseDto>(schedules, 200);
|
||||
})
|
||||
.get("/:scheduleId", getBackupScheduleDto, async (c) => {
|
||||
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);
|
||||
})
|
||||
.get("/volume/:volumeId", getBackupScheduleForVolumeDto, async (c) => {
|
||||
const volumeId = c.req.param("volumeId");
|
||||
const schedule = await backupsService.getScheduleForVolume(Number(volumeId));
|
||||
const schedule = await backupsService.getScheduleForVolume(Number(volumeId), c.get("organizationId"));
|
||||
|
||||
return c.json<GetBackupScheduleForVolumeResponseDto>(schedule, 200);
|
||||
})
|
||||
.post("/", createBackupScheduleDto, validator("json", createBackupScheduleBody), async (c) => {
|
||||
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);
|
||||
})
|
||||
|
|
@ -74,21 +74,21 @@ export const backupScheduleController = new Hono()
|
|||
const scheduleId = c.req.param("scheduleId");
|
||||
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);
|
||||
})
|
||||
.delete("/:scheduleId", deleteBackupScheduleDto, async (c) => {
|
||||
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);
|
||||
})
|
||||
.post("/:scheduleId/run", runBackupNowDto, async (c) => {
|
||||
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);
|
||||
});
|
||||
|
||||
|
|
@ -97,14 +97,14 @@ export const backupScheduleController = new Hono()
|
|||
.post("/:scheduleId/stop", stopBackupDto, async (c) => {
|
||||
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);
|
||||
})
|
||||
.post("/:scheduleId/forget", runForgetDto, async (c) => {
|
||||
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);
|
||||
})
|
||||
|
|
@ -128,27 +128,27 @@ export const backupScheduleController = new Hono()
|
|||
)
|
||||
.get("/:scheduleId/mirrors", getScheduleMirrorsDto, async (c) => {
|
||||
const scheduleId = Number.parseInt(c.req.param("scheduleId"), 10);
|
||||
const mirrors = await backupsService.getMirrors(scheduleId);
|
||||
const mirrors = await backupsService.getMirrors(scheduleId, c.get("organizationId"));
|
||||
|
||||
return c.json<GetScheduleMirrorsDto>(mirrors, 200);
|
||||
})
|
||||
.put("/:scheduleId/mirrors", updateScheduleMirrorsDto, validator("json", updateScheduleMirrorsBody), async (c) => {
|
||||
const scheduleId = Number.parseInt(c.req.param("scheduleId"), 10);
|
||||
const body = c.req.valid("json");
|
||||
const mirrors = await backupsService.updateMirrors(scheduleId, body);
|
||||
const mirrors = await backupsService.updateMirrors(scheduleId, c.get("organizationId"), body);
|
||||
|
||||
return c.json<UpdateScheduleMirrorsDto>(mirrors, 200);
|
||||
})
|
||||
.get("/:scheduleId/mirrors/compatibility", getMirrorCompatibilityDto, async (c) => {
|
||||
const scheduleId = Number.parseInt(c.req.param("scheduleId"), 10);
|
||||
const compatibility = await backupsService.getMirrorCompatibility(scheduleId);
|
||||
const compatibility = await backupsService.getMirrorCompatibility(scheduleId, c.get("organizationId"));
|
||||
|
||||
return c.json<GetMirrorCompatibilityDto>(compatibility, 200);
|
||||
})
|
||||
.post("/reorder", reorderBackupSchedulesDto, validator("json", reorderBackupSchedulesBody), async (c) => {
|
||||
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);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -56,8 +56,9 @@ const processPattern = (pattern: string, volumePath: string): string => {
|
|||
return pattern;
|
||||
};
|
||||
|
||||
const listSchedules = async () => {
|
||||
const listSchedules = async (organizationId: string) => {
|
||||
const schedules = await db.query.backupSchedulesTable.findMany({
|
||||
where: eq(backupSchedulesTable.organizationId, organizationId),
|
||||
with: {
|
||||
volume: true,
|
||||
repository: true,
|
||||
|
|
@ -67,9 +68,9 @@ const listSchedules = async () => {
|
|||
return schedules;
|
||||
};
|
||||
|
||||
const getSchedule = async (scheduleId: number) => {
|
||||
const getSchedule = async (scheduleId: number, organizationId: string) => {
|
||||
const schedule = await db.query.backupSchedulesTable.findFirst({
|
||||
where: eq(backupSchedulesTable.id, scheduleId),
|
||||
where: and(eq(backupSchedulesTable.id, scheduleId), eq(backupSchedulesTable.organizationId, organizationId)),
|
||||
with: {
|
||||
volume: true,
|
||||
repository: true,
|
||||
|
|
@ -83,13 +84,13 @@ const getSchedule = async (scheduleId: number) => {
|
|||
return schedule;
|
||||
};
|
||||
|
||||
const createSchedule = async (data: CreateBackupScheduleBody) => {
|
||||
const createSchedule = async (data: CreateBackupScheduleBody, organizationId: string) => {
|
||||
if (!cron.validate(data.cronExpression)) {
|
||||
throw new BadRequestError("Invalid cron expression");
|
||||
}
|
||||
|
||||
const existingName = await db.query.backupSchedulesTable.findFirst({
|
||||
where: eq(backupSchedulesTable.name, data.name),
|
||||
where: and(eq(backupSchedulesTable.name, data.name), eq(backupSchedulesTable.organizationId, organizationId)),
|
||||
});
|
||||
|
||||
if (existingName) {
|
||||
|
|
@ -97,7 +98,7 @@ const createSchedule = async (data: CreateBackupScheduleBody) => {
|
|||
}
|
||||
|
||||
const volume = await db.query.volumesTable.findFirst({
|
||||
where: eq(volumesTable.id, data.volumeId),
|
||||
where: and(eq(volumesTable.id, data.volumeId), eq(volumesTable.organizationId, organizationId)),
|
||||
});
|
||||
|
||||
if (!volume) {
|
||||
|
|
@ -105,7 +106,7 @@ const createSchedule = async (data: CreateBackupScheduleBody) => {
|
|||
}
|
||||
|
||||
const repository = await db.query.repositoriesTable.findFirst({
|
||||
where: eq(repositoriesTable.id, data.repositoryId),
|
||||
where: and(eq(repositoriesTable.id, data.repositoryId), eq(repositoriesTable.organizationId, organizationId)),
|
||||
});
|
||||
|
||||
if (!repository) {
|
||||
|
|
@ -129,6 +130,7 @@ const createSchedule = async (data: CreateBackupScheduleBody) => {
|
|||
oneFileSystem: data.oneFileSystem,
|
||||
nextBackupAt: nextBackupAt,
|
||||
shortId: generateShortId(),
|
||||
organizationId,
|
||||
})
|
||||
.returning();
|
||||
|
||||
|
|
@ -139,9 +141,9 @@ const createSchedule = async (data: CreateBackupScheduleBody) => {
|
|||
return newSchedule;
|
||||
};
|
||||
|
||||
const updateSchedule = async (scheduleId: number, data: UpdateBackupScheduleBody) => {
|
||||
const updateSchedule = async (scheduleId: number, data: UpdateBackupScheduleBody, organizationId: string) => {
|
||||
const schedule = await db.query.backupSchedulesTable.findFirst({
|
||||
where: eq(backupSchedulesTable.id, scheduleId),
|
||||
where: and(eq(backupSchedulesTable.id, scheduleId), eq(backupSchedulesTable.organizationId, organizationId)),
|
||||
});
|
||||
|
||||
if (!schedule) {
|
||||
|
|
@ -154,7 +156,7 @@ const updateSchedule = async (scheduleId: number, data: UpdateBackupScheduleBody
|
|||
|
||||
if (data.name) {
|
||||
const existingName = await db.query.backupSchedulesTable.findFirst({
|
||||
where: and(eq(backupSchedulesTable.name, data.name), ne(backupSchedulesTable.id, scheduleId)),
|
||||
where: and(eq(backupSchedulesTable.name, data.name), ne(backupSchedulesTable.id, scheduleId), eq(backupSchedulesTable.organizationId, organizationId)),
|
||||
});
|
||||
|
||||
if (existingName) {
|
||||
|
|
@ -163,7 +165,7 @@ const updateSchedule = async (scheduleId: number, data: UpdateBackupScheduleBody
|
|||
}
|
||||
|
||||
const repository = await db.query.repositoriesTable.findFirst({
|
||||
where: eq(repositoriesTable.id, data.repositoryId),
|
||||
where: and(eq(repositoriesTable.id, data.repositoryId), eq(repositoriesTable.organizationId, organizationId)),
|
||||
});
|
||||
|
||||
if (!repository) {
|
||||
|
|
@ -186,9 +188,9 @@ const updateSchedule = async (scheduleId: number, data: UpdateBackupScheduleBody
|
|||
return updated;
|
||||
};
|
||||
|
||||
const deleteSchedule = async (scheduleId: number) => {
|
||||
const deleteSchedule = async (scheduleId: number, organizationId: string) => {
|
||||
const schedule = await db.query.backupSchedulesTable.findFirst({
|
||||
where: eq(backupSchedulesTable.id, scheduleId),
|
||||
where: and(eq(backupSchedulesTable.id, scheduleId), eq(backupSchedulesTable.organizationId, organizationId)),
|
||||
});
|
||||
|
||||
if (!schedule) {
|
||||
|
|
@ -198,9 +200,9 @@ const deleteSchedule = async (scheduleId: number) => {
|
|||
await db.delete(backupSchedulesTable).where(eq(backupSchedulesTable.id, scheduleId));
|
||||
};
|
||||
|
||||
const executeBackup = async (scheduleId: number, manual = false) => {
|
||||
const executeBackup = async (scheduleId: number, organizationId: string, manual = false) => {
|
||||
const schedule = await db.query.backupSchedulesTable.findFirst({
|
||||
where: eq(backupSchedulesTable.id, scheduleId),
|
||||
where: and(eq(backupSchedulesTable.id, scheduleId), eq(backupSchedulesTable.organizationId, organizationId)),
|
||||
});
|
||||
|
||||
if (!schedule) {
|
||||
|
|
@ -319,12 +321,12 @@ const executeBackup = async (scheduleId: number, manual = false) => {
|
|||
}
|
||||
|
||||
if (schedule.retentionPolicy) {
|
||||
void runForget(schedule.id).catch((error) => {
|
||||
void runForget(schedule.id, organizationId).catch((error) => {
|
||||
logger.error(`Failed to run retention policy for schedule ${scheduleId}: ${toMessage(error)}`);
|
||||
});
|
||||
}
|
||||
|
||||
void copyToMirrors(scheduleId, repository, schedule.retentionPolicy).catch((error) => {
|
||||
void copyToMirrors(scheduleId, organizationId, repository, schedule.retentionPolicy).catch((error) => {
|
||||
logger.error(`Background mirror copy failed for schedule ${scheduleId}: ${toMessage(error)}`);
|
||||
});
|
||||
|
||||
|
|
@ -407,12 +409,13 @@ const executeBackup = async (scheduleId: number, manual = false) => {
|
|||
}
|
||||
};
|
||||
|
||||
const getSchedulesToExecute = async () => {
|
||||
const getSchedulesToExecute = async (organizationId: string) => {
|
||||
const now = Date.now();
|
||||
const schedules = await db.query.backupSchedulesTable.findMany({
|
||||
where: and(
|
||||
eq(backupSchedulesTable.enabled, true),
|
||||
or(ne(backupSchedulesTable.lastBackupStatus, "in_progress"), isNull(backupSchedulesTable.lastBackupStatus)),
|
||||
eq(backupSchedulesTable.organizationId, organizationId)
|
||||
),
|
||||
});
|
||||
|
||||
|
|
@ -427,18 +430,18 @@ const getSchedulesToExecute = async () => {
|
|||
return schedulesToRun;
|
||||
};
|
||||
|
||||
const getScheduleForVolume = async (volumeId: number) => {
|
||||
const getScheduleForVolume = async (volumeId: number, organizationId: string) => {
|
||||
const schedule = await db.query.backupSchedulesTable.findFirst({
|
||||
where: eq(backupSchedulesTable.volumeId, volumeId),
|
||||
where: and(eq(backupSchedulesTable.volumeId, volumeId), eq(backupSchedulesTable.organizationId, organizationId)),
|
||||
with: { volume: true, repository: true },
|
||||
});
|
||||
|
||||
return schedule ?? null;
|
||||
};
|
||||
|
||||
const stopBackup = async (scheduleId: number) => {
|
||||
const stopBackup = async (scheduleId: number, organizationId: string) => {
|
||||
const schedule = await db.query.backupSchedulesTable.findFirst({
|
||||
where: eq(backupSchedulesTable.id, scheduleId),
|
||||
where: and(eq(backupSchedulesTable.id, scheduleId), eq(backupSchedulesTable.organizationId, organizationId)),
|
||||
});
|
||||
|
||||
if (!schedule) {
|
||||
|
|
@ -466,9 +469,9 @@ const stopBackup = async (scheduleId: number) => {
|
|||
}
|
||||
};
|
||||
|
||||
const runForget = async (scheduleId: number, repositoryId?: string) => {
|
||||
const runForget = async (scheduleId: number, organizationId: string, repositoryId?: string) => {
|
||||
const schedule = await db.query.backupSchedulesTable.findFirst({
|
||||
where: eq(backupSchedulesTable.id, scheduleId),
|
||||
where: and(eq(backupSchedulesTable.id, scheduleId), eq(backupSchedulesTable.organizationId, organizationId)),
|
||||
});
|
||||
|
||||
if (!schedule) {
|
||||
|
|
@ -480,7 +483,7 @@ const runForget = async (scheduleId: number, repositoryId?: string) => {
|
|||
}
|
||||
|
||||
const repository = await db.query.repositoriesTable.findFirst({
|
||||
where: eq(repositoriesTable.id, repositoryId ?? schedule.repositoryId),
|
||||
where: and(eq(repositoriesTable.id, repositoryId ?? schedule.repositoryId), eq(repositoriesTable.organizationId, organizationId)),
|
||||
});
|
||||
|
||||
if (!repository) {
|
||||
|
|
@ -499,9 +502,9 @@ const runForget = async (scheduleId: number, repositoryId?: string) => {
|
|||
logger.info(`Retention policy applied successfully for schedule ${scheduleId}`);
|
||||
};
|
||||
|
||||
const getMirrors = async (scheduleId: number) => {
|
||||
const getMirrors = async (scheduleId: number, organizationId: string) => {
|
||||
const schedule = await db.query.backupSchedulesTable.findFirst({
|
||||
where: eq(backupSchedulesTable.id, scheduleId),
|
||||
where: and(eq(backupSchedulesTable.id, scheduleId), eq(backupSchedulesTable.organizationId, organizationId)),
|
||||
});
|
||||
|
||||
if (!schedule) {
|
||||
|
|
@ -516,9 +519,9 @@ const getMirrors = async (scheduleId: number) => {
|
|||
return mirrors;
|
||||
};
|
||||
|
||||
const updateMirrors = async (scheduleId: number, data: UpdateScheduleMirrorsBody) => {
|
||||
const updateMirrors = async (scheduleId: number, organizationId: string, data: UpdateScheduleMirrorsBody) => {
|
||||
const schedule = await db.query.backupSchedulesTable.findFirst({
|
||||
where: eq(backupSchedulesTable.id, scheduleId),
|
||||
where: and(eq(backupSchedulesTable.id, scheduleId), eq(backupSchedulesTable.organizationId, organizationId)),
|
||||
with: { repository: true },
|
||||
});
|
||||
|
||||
|
|
@ -532,7 +535,7 @@ const updateMirrors = async (scheduleId: number, data: UpdateScheduleMirrorsBody
|
|||
}
|
||||
|
||||
const repo = await db.query.repositoriesTable.findFirst({
|
||||
where: eq(repositoriesTable.id, mirror.repositoryId),
|
||||
where: and(eq(repositoriesTable.id, mirror.repositoryId), eq(repositoriesTable.organizationId, organizationId)),
|
||||
});
|
||||
|
||||
if (!repo) {
|
||||
|
|
@ -577,16 +580,17 @@ const updateMirrors = async (scheduleId: number, data: UpdateScheduleMirrorsBody
|
|||
);
|
||||
}
|
||||
|
||||
return getMirrors(scheduleId);
|
||||
return getMirrors(scheduleId, organizationId);
|
||||
};
|
||||
|
||||
const copyToMirrors = async (
|
||||
scheduleId: number,
|
||||
organizationId: string,
|
||||
sourceRepository: { id: string; config: (typeof repositoriesTable.$inferSelect)["config"] },
|
||||
retentionPolicy: (typeof backupSchedulesTable.$inferSelect)["retentionPolicy"],
|
||||
) => {
|
||||
const schedule = await db.query.backupSchedulesTable.findFirst({
|
||||
where: eq(backupSchedulesTable.id, scheduleId),
|
||||
where: and(eq(backupSchedulesTable.id, scheduleId), eq(backupSchedulesTable.organizationId, organizationId)),
|
||||
});
|
||||
|
||||
if (!schedule) {
|
||||
|
|
@ -630,7 +634,7 @@ const copyToMirrors = async (
|
|||
}
|
||||
|
||||
if (retentionPolicy) {
|
||||
void runForget(scheduleId, mirror.repository.id).catch((error) => {
|
||||
void runForget(scheduleId, organizationId, mirror.repository.id).catch((error) => {
|
||||
logger.error(
|
||||
`Failed to run retention policy for mirror repository ${mirror.repository.name}: ${toMessage(error)}`,
|
||||
);
|
||||
|
|
@ -670,9 +674,9 @@ const copyToMirrors = async (
|
|||
}
|
||||
};
|
||||
|
||||
const getMirrorCompatibility = async (scheduleId: number) => {
|
||||
const getMirrorCompatibility = async (scheduleId: number, organizationId: string) => {
|
||||
const schedule = await db.query.backupSchedulesTable.findFirst({
|
||||
where: eq(backupSchedulesTable.id, scheduleId),
|
||||
where: and(eq(backupSchedulesTable.id, scheduleId), eq(backupSchedulesTable.organizationId, organizationId)),
|
||||
with: { repository: true },
|
||||
});
|
||||
|
||||
|
|
@ -680,7 +684,9 @@ const getMirrorCompatibility = async (scheduleId: number) => {
|
|||
throw new NotFoundError("Backup schedule not found");
|
||||
}
|
||||
|
||||
const allRepositories = await db.query.repositoriesTable.findMany();
|
||||
const allRepositories = await db.query.repositoriesTable.findMany({
|
||||
where: eq(repositoriesTable.organizationId, organizationId),
|
||||
});
|
||||
const repos = allRepositories.filter((repo) => repo.id !== schedule.repositoryId);
|
||||
|
||||
const compatibility = await Promise.all(
|
||||
|
|
@ -690,13 +696,14 @@ const getMirrorCompatibility = async (scheduleId: number) => {
|
|||
return compatibility;
|
||||
};
|
||||
|
||||
const reorderSchedules = async (scheduleIds: number[]) => {
|
||||
const reorderSchedules = async (scheduleIds: number[], organizationId: string) => {
|
||||
const uniqueIds = new Set(scheduleIds);
|
||||
if (uniqueIds.size !== scheduleIds.length) {
|
||||
throw new BadRequestError("Duplicate schedule IDs in reorder request");
|
||||
}
|
||||
|
||||
const existingSchedules = await db.query.backupSchedulesTable.findMany({
|
||||
where: eq(backupSchedulesTable.organizationId, organizationId),
|
||||
columns: { id: true },
|
||||
});
|
||||
const existingIds = new Set(existingSchedules.map((s) => s.id));
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@ const ensureLatestConfigurationSchema = async () => {
|
|||
const volumes = await db.query.volumesTable.findMany({});
|
||||
|
||||
for (const volume of volumes) {
|
||||
await volumeService.updateVolume(volume.name, volume).catch((err) => {
|
||||
await volumeService.updateVolume(volume.name, volume, volume.organizationId).catch((err) => {
|
||||
logger.error(`Failed to update volume ${volume.name}: ${err}`);
|
||||
});
|
||||
}
|
||||
|
|
@ -28,7 +28,7 @@ const ensureLatestConfigurationSchema = async () => {
|
|||
const repositories = await db.query.repositoriesTable.findMany({});
|
||||
|
||||
for (const repo of repositories) {
|
||||
await repositoriesService.updateRepository(repo.id, {}).catch((err) => {
|
||||
await repositoriesService.updateRepository(repo.id, repo.organizationId, {}).catch((err) => {
|
||||
logger.error(`Failed to update repository ${repo.name}: ${err}`);
|
||||
});
|
||||
}
|
||||
|
|
@ -36,9 +36,11 @@ const ensureLatestConfigurationSchema = async () => {
|
|||
const notifications = await db.query.notificationDestinationsTable.findMany({});
|
||||
|
||||
for (const notification of notifications) {
|
||||
await notificationsService.updateDestination(notification.id, notification).catch((err) => {
|
||||
logger.error(`Failed to update notification destination ${notification.id}: ${err}`);
|
||||
});
|
||||
await notificationsService
|
||||
.updateDestination(notification.id, notification.organizationId, notification)
|
||||
.catch((err) => {
|
||||
logger.error(`Failed to update notification destination ${notification.id}: ${err}`);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
|
|
@ -67,7 +69,7 @@ export const startup = async () => {
|
|||
});
|
||||
|
||||
for (const volume of volumes) {
|
||||
await volumeService.mountVolume(volume.name).catch((err) => {
|
||||
await volumeService.mountVolume(volume.name, volume.organizationId).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()
|
||||
.use(requireAuth)
|
||||
.get("/destinations", listDestinationsDto, async (c) => {
|
||||
const destinations = await notificationsService.listDestinations();
|
||||
const destinations = await notificationsService.listDestinations(c.get("organizationId"));
|
||||
return c.json<ListDestinationsDto>(destinations, 200);
|
||||
})
|
||||
.post("/destinations", createDestinationDto, validator("json", createDestinationBody), async (c) => {
|
||||
const body = c.req.valid("json");
|
||||
const destination = await notificationsService.createDestination(body.name, body.config);
|
||||
const destination = await notificationsService.createDestination(body.name, body.config, c.get("organizationId"));
|
||||
return c.json<CreateDestinationDto>(destination, 201);
|
||||
})
|
||||
.get("/destinations/:id", getDestinationDto, async (c) => {
|
||||
const id = Number.parseInt(c.req.param("id"), 10);
|
||||
const destination = await notificationsService.getDestination(id);
|
||||
const destination = await notificationsService.getDestination(id, c.get("organizationId"));
|
||||
return c.json<GetDestinationDto>(destination, 200);
|
||||
})
|
||||
.patch("/destinations/:id", updateDestinationDto, validator("json", updateDestinationBody), async (c) => {
|
||||
const id = Number.parseInt(c.req.param("id"), 10);
|
||||
const body = c.req.valid("json");
|
||||
const destination = await notificationsService.updateDestination(id, body);
|
||||
const destination = await notificationsService.updateDestination(id, c.get("organizationId"), body);
|
||||
return c.json<UpdateDestinationDto>(destination, 200);
|
||||
})
|
||||
.delete("/destinations/:id", deleteDestinationDto, async (c) => {
|
||||
const id = Number.parseInt(c.req.param("id"), 10);
|
||||
await notificationsService.deleteDestination(id);
|
||||
await notificationsService.deleteDestination(id, c.get("organizationId"));
|
||||
return c.json<DeleteDestinationDto>({ message: "Notification destination deleted" }, 200);
|
||||
})
|
||||
.post("/destinations/:id/test", testDestinationDto, async (c) => {
|
||||
const id = Number.parseInt(c.req.param("id"), 10);
|
||||
const result = await notificationsService.testDestination(id);
|
||||
const result = await notificationsService.testDestination(id, c.get("organizationId"));
|
||||
return c.json<TestDestinationDto>(result, 200);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -15,16 +15,17 @@ import { notificationConfigSchema, type NotificationConfig, type NotificationEve
|
|||
import { toMessage } from "../../utils/errors";
|
||||
import { type } from "arktype";
|
||||
|
||||
const listDestinations = async () => {
|
||||
const listDestinations = async (organizationId: string) => {
|
||||
const destinations = await db.query.notificationDestinationsTable.findMany({
|
||||
where: eq(notificationDestinationsTable.organizationId, organizationId),
|
||||
orderBy: (destinations, { asc }) => [asc(destinations.name)],
|
||||
});
|
||||
return destinations;
|
||||
};
|
||||
|
||||
const getDestination = async (id: number) => {
|
||||
const getDestination = async (id: number, organizationId: string) => {
|
||||
const destination = await db.query.notificationDestinationsTable.findFirst({
|
||||
where: eq(notificationDestinationsTable.id, id),
|
||||
where: and(eq(notificationDestinationsTable.id, id), eq(notificationDestinationsTable.organizationId, organizationId)),
|
||||
});
|
||||
|
||||
if (!destination) {
|
||||
|
|
@ -132,11 +133,11 @@ async function decryptSensitiveFields(config: NotificationConfig): Promise<Notif
|
|||
}
|
||||
}
|
||||
|
||||
const createDestination = async (name: string, config: NotificationConfig) => {
|
||||
const createDestination = async (name: string, config: NotificationConfig, organizationId: string) => {
|
||||
const slug = slugify(name, { lower: true, strict: true });
|
||||
|
||||
const existing = await db.query.notificationDestinationsTable.findFirst({
|
||||
where: eq(notificationDestinationsTable.name, slug),
|
||||
where: and(eq(notificationDestinationsTable.name, slug), eq(notificationDestinationsTable.organizationId, organizationId)),
|
||||
});
|
||||
|
||||
if (existing) {
|
||||
|
|
@ -151,6 +152,7 @@ const createDestination = async (name: string, config: NotificationConfig) => {
|
|||
name: slug,
|
||||
type: config.type,
|
||||
config: encryptedConfig,
|
||||
organizationId,
|
||||
})
|
||||
.returning();
|
||||
|
||||
|
|
@ -163,9 +165,10 @@ const createDestination = async (name: string, config: NotificationConfig) => {
|
|||
|
||||
const updateDestination = async (
|
||||
id: number,
|
||||
organizationId: string,
|
||||
updates: { name?: string; enabled?: boolean; config?: NotificationConfig },
|
||||
) => {
|
||||
const existing = await getDestination(id);
|
||||
const existing = await getDestination(id, organizationId);
|
||||
|
||||
if (!existing) {
|
||||
throw new NotFoundError("Notification destination not found");
|
||||
|
|
@ -179,7 +182,7 @@ const updateDestination = async (
|
|||
const slug = slugify(updates.name, { lower: true, strict: true });
|
||||
|
||||
const conflict = await db.query.notificationDestinationsTable.findFirst({
|
||||
where: and(eq(notificationDestinationsTable.name, slug), ne(notificationDestinationsTable.id, id)),
|
||||
where: and(eq(notificationDestinationsTable.name, slug), ne(notificationDestinationsTable.id, id), eq(notificationDestinationsTable.organizationId, organizationId)),
|
||||
});
|
||||
|
||||
if (conflict) {
|
||||
|
|
@ -214,12 +217,13 @@ const updateDestination = async (
|
|||
return updated;
|
||||
};
|
||||
|
||||
const deleteDestination = async (id: number) => {
|
||||
const deleteDestination = async (id: number, organizationId: string) => {
|
||||
await getDestination(id, organizationId);
|
||||
await db.delete(notificationDestinationsTable).where(eq(notificationDestinationsTable.id, id));
|
||||
};
|
||||
|
||||
const testDestination = async (id: number) => {
|
||||
const destination = await getDestination(id);
|
||||
const testDestination = async (id: number, organizationId: string) => {
|
||||
const destination = await getDestination(id, organizationId);
|
||||
|
||||
if (!destination.enabled) {
|
||||
throw new ConflictError("Cannot test disabled notification destination");
|
||||
|
|
|
|||
|
|
@ -42,13 +42,13 @@ import { requireAuth } from "../auth/auth.middleware";
|
|||
export const repositoriesController = new Hono()
|
||||
.use(requireAuth)
|
||||
.get("/", listRepositoriesDto, async (c) => {
|
||||
const repositories = await repositoriesService.listRepositories();
|
||||
const repositories = await repositoriesService.listRepositories(c.get("organizationId"));
|
||||
|
||||
return c.json<ListRepositoriesDto>(repositories, 200);
|
||||
})
|
||||
.post("/", createRepositoryDto, validator("json", createRepositoryBody), async (c) => {
|
||||
const body = c.req.valid("json");
|
||||
const res = await repositoriesService.createRepository(body.name, body.config, body.compressionMode);
|
||||
const res = await repositoriesService.createRepository(body.name, body.config, c.get("organizationId"), body.compressionMode);
|
||||
|
||||
return c.json({ message: "Repository created", repository: res.repository }, 201);
|
||||
})
|
||||
|
|
@ -69,13 +69,13 @@ export const repositoriesController = new Hono()
|
|||
})
|
||||
.get("/:id", getRepositoryDto, async (c) => {
|
||||
const { id } = c.req.param();
|
||||
const res = await repositoriesService.getRepository(id);
|
||||
const res = await repositoriesService.getRepository(id, c.get("organizationId"));
|
||||
|
||||
return c.json<GetRepositoryDto>(res.repository, 200);
|
||||
})
|
||||
.delete("/:id", deleteRepositoryDto, async (c) => {
|
||||
const { id } = c.req.param();
|
||||
await repositoriesService.deleteRepository(id);
|
||||
await repositoriesService.deleteRepository(id, c.get("organizationId"));
|
||||
|
||||
return c.json<DeleteRepositoryDto>({ message: "Repository deleted" }, 200);
|
||||
})
|
||||
|
|
@ -83,7 +83,7 @@ export const repositoriesController = new Hono()
|
|||
const { id } = c.req.param();
|
||||
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 { summary } = snapshot;
|
||||
|
|
@ -108,7 +108,7 @@ export const repositoriesController = new Hono()
|
|||
})
|
||||
.get("/:id/snapshots/:snapshotId", getSnapshotDetailsDto, async (c) => {
|
||||
const { id, snapshotId } = c.req.param();
|
||||
const snapshot = await repositoriesService.getSnapshotDetails(id, snapshotId);
|
||||
const snapshot = await repositoriesService.getSnapshotDetails(id, c.get("organizationId"), snapshotId);
|
||||
|
||||
let duration = 0;
|
||||
if (snapshot.summary) {
|
||||
|
|
@ -137,7 +137,7 @@ export const repositoriesController = new Hono()
|
|||
const { path } = c.req.valid("query");
|
||||
|
||||
const decodedPath = path ? decodeURIComponent(path) : undefined;
|
||||
const result = await repositoriesService.listSnapshotFiles(id, snapshotId, decodedPath);
|
||||
const result = await repositoriesService.listSnapshotFiles(id, c.get("organizationId"), snapshotId, decodedPath);
|
||||
|
||||
c.header("Cache-Control", "max-age=300, stale-while-revalidate=600");
|
||||
|
||||
|
|
@ -148,21 +148,21 @@ export const repositoriesController = new Hono()
|
|||
const { id } = c.req.param();
|
||||
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);
|
||||
})
|
||||
.post("/:id/doctor", doctorRepositoryDto, async (c) => {
|
||||
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);
|
||||
})
|
||||
.delete("/:id/snapshots/:snapshotId", deleteSnapshotDto, async (c) => {
|
||||
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);
|
||||
})
|
||||
|
|
@ -170,7 +170,7 @@ export const repositoriesController = new Hono()
|
|||
const { id } = c.req.param();
|
||||
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);
|
||||
})
|
||||
|
|
@ -178,7 +178,7 @@ export const repositoriesController = new Hono()
|
|||
const { id } = c.req.param();
|
||||
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);
|
||||
})
|
||||
|
|
@ -186,7 +186,7 @@ export const repositoriesController = new Hono()
|
|||
const { id } = c.req.param();
|
||||
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);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import crypto from "node:crypto";
|
||||
import { eq, or } from "drizzle-orm";
|
||||
import { and, eq, or } from "drizzle-orm";
|
||||
import { InternalServerError, NotFoundError } from "http-errors-enhanced";
|
||||
import { db } from "../../db/db";
|
||||
import { repositoriesTable } from "../../db/schema";
|
||||
|
|
@ -17,14 +17,19 @@ import {
|
|||
} from "~/schemas/restic";
|
||||
import { type } from "arktype";
|
||||
|
||||
const findRepository = async (idOrShortId: string) => {
|
||||
const findRepository = async (idOrShortId: string, organizationId: string) => {
|
||||
return await db.query.repositoriesTable.findFirst({
|
||||
where: or(eq(repositoriesTable.id, idOrShortId), eq(repositoriesTable.shortId, idOrShortId)),
|
||||
where: and(
|
||||
or(eq(repositoriesTable.id, idOrShortId), eq(repositoriesTable.shortId, idOrShortId)),
|
||||
eq(repositoriesTable.organizationId, organizationId)
|
||||
),
|
||||
});
|
||||
};
|
||||
|
||||
const listRepositories = async () => {
|
||||
const repositories = await db.query.repositoriesTable.findMany({});
|
||||
const listRepositories = async (organizationId: string) => {
|
||||
const repositories = await db.query.repositoriesTable.findMany({
|
||||
where: eq(repositoriesTable.organizationId, organizationId),
|
||||
});
|
||||
return repositories;
|
||||
};
|
||||
|
||||
|
|
@ -67,7 +72,7 @@ const encryptConfig = async (config: RepositoryConfig): Promise<RepositoryConfig
|
|||
return encryptedConfig as RepositoryConfig;
|
||||
};
|
||||
|
||||
const createRepository = async (name: string, config: RepositoryConfig, compressionMode?: CompressionMode) => {
|
||||
const createRepository = async (name: string, config: RepositoryConfig, organizationId: string, compressionMode?: CompressionMode) => {
|
||||
const id = crypto.randomUUID();
|
||||
const shortId = generateShortId();
|
||||
|
||||
|
|
@ -88,6 +93,7 @@ const createRepository = async (name: string, config: RepositoryConfig, compress
|
|||
config: encryptedConfig,
|
||||
compressionMode: compressionMode ?? "auto",
|
||||
status: "unknown",
|
||||
organizationId,
|
||||
})
|
||||
.returning();
|
||||
|
||||
|
|
@ -124,8 +130,8 @@ const createRepository = async (name: string, config: RepositoryConfig, compress
|
|||
throw new InternalServerError(`Failed to initialize repository: ${errorMessage}`);
|
||||
};
|
||||
|
||||
const getRepository = async (id: string) => {
|
||||
const repository = await findRepository(id);
|
||||
const getRepository = async (id: string, organizationId: string) => {
|
||||
const repository = await findRepository(id, organizationId);
|
||||
|
||||
if (!repository) {
|
||||
throw new NotFoundError("Repository not found");
|
||||
|
|
@ -134,8 +140,8 @@ const getRepository = async (id: string) => {
|
|||
return { repository };
|
||||
};
|
||||
|
||||
const deleteRepository = async (id: string) => {
|
||||
const repository = await findRepository(id);
|
||||
const deleteRepository = async (id: string, organizationId: string) => {
|
||||
const repository = await findRepository(id, organizationId);
|
||||
|
||||
if (!repository) {
|
||||
throw new NotFoundError("Repository not found");
|
||||
|
|
@ -157,8 +163,8 @@ const deleteRepository = async (id: string) => {
|
|||
*
|
||||
* @returns List of snapshots
|
||||
*/
|
||||
const listSnapshots = async (id: string, backupId?: string) => {
|
||||
const repository = await findRepository(id);
|
||||
const listSnapshots = async (id: string, organizationId: string, backupId?: string) => {
|
||||
const repository = await findRepository(id, organizationId);
|
||||
|
||||
if (!repository) {
|
||||
throw new NotFoundError("Repository not found");
|
||||
|
|
@ -188,8 +194,8 @@ const listSnapshots = async (id: string, backupId?: string) => {
|
|||
}
|
||||
};
|
||||
|
||||
const listSnapshotFiles = async (id: string, snapshotId: string, path?: string) => {
|
||||
const repository = await findRepository(id);
|
||||
const listSnapshotFiles = async (id: string, organizationId: string, snapshotId: string, path?: string) => {
|
||||
const repository = await findRepository(id, organizationId);
|
||||
|
||||
if (!repository) {
|
||||
throw new NotFoundError("Repository not found");
|
||||
|
|
@ -233,6 +239,7 @@ const listSnapshotFiles = async (id: string, snapshotId: string, path?: string)
|
|||
|
||||
const restoreSnapshot = async (
|
||||
id: string,
|
||||
organizationId: string,
|
||||
snapshotId: string,
|
||||
options?: {
|
||||
include?: string[];
|
||||
|
|
@ -243,7 +250,7 @@ const restoreSnapshot = async (
|
|||
overwrite?: OverwriteMode;
|
||||
},
|
||||
) => {
|
||||
const repository = await findRepository(id);
|
||||
const repository = await findRepository(id, organizationId);
|
||||
|
||||
if (!repository) {
|
||||
throw new NotFoundError("Repository not found");
|
||||
|
|
@ -266,8 +273,8 @@ const restoreSnapshot = async (
|
|||
}
|
||||
};
|
||||
|
||||
const getSnapshotDetails = async (id: string, snapshotId: string) => {
|
||||
const repository = await findRepository(id);
|
||||
const getSnapshotDetails = async (id: string, organizationId: string, snapshotId: string) => {
|
||||
const repository = await findRepository(id, organizationId);
|
||||
|
||||
if (!repository) {
|
||||
throw new NotFoundError("Repository not found");
|
||||
|
|
@ -295,8 +302,8 @@ const getSnapshotDetails = async (id: string, snapshotId: string) => {
|
|||
return snapshot;
|
||||
};
|
||||
|
||||
const checkHealth = async (repositoryId: string) => {
|
||||
const repository = await findRepository(repositoryId);
|
||||
const checkHealth = async (repositoryId: string, organizationId: string) => {
|
||||
const repository = await findRepository(repositoryId, organizationId);
|
||||
|
||||
if (!repository) {
|
||||
throw new NotFoundError("Repository not found");
|
||||
|
|
@ -321,8 +328,8 @@ const checkHealth = async (repositoryId: string) => {
|
|||
}
|
||||
};
|
||||
|
||||
const doctorRepository = async (id: string) => {
|
||||
const repository = await findRepository(id);
|
||||
const doctorRepository = async (id: string, organizationId: string) => {
|
||||
const repository = await findRepository(id, organizationId);
|
||||
|
||||
if (!repository) {
|
||||
throw new NotFoundError("Repository not found");
|
||||
|
|
@ -410,8 +417,8 @@ const doctorRepository = async (id: string) => {
|
|||
};
|
||||
};
|
||||
|
||||
const deleteSnapshot = async (id: string, snapshotId: string) => {
|
||||
const repository = await findRepository(id);
|
||||
const deleteSnapshot = async (id: string, organizationId: string, snapshotId: string) => {
|
||||
const repository = await findRepository(id, organizationId);
|
||||
|
||||
if (!repository) {
|
||||
throw new NotFoundError("Repository not found");
|
||||
|
|
@ -427,8 +434,8 @@ const deleteSnapshot = async (id: string, snapshotId: string) => {
|
|||
}
|
||||
};
|
||||
|
||||
const deleteSnapshots = async (id: string, snapshotIds: string[]) => {
|
||||
const repository = await findRepository(id);
|
||||
const deleteSnapshots = async (id: string, organizationId: string, snapshotIds: string[]) => {
|
||||
const repository = await findRepository(id, organizationId);
|
||||
|
||||
if (!repository) {
|
||||
throw new NotFoundError("Repository not found");
|
||||
|
|
@ -448,10 +455,11 @@ const deleteSnapshots = async (id: string, snapshotIds: string[]) => {
|
|||
|
||||
const tagSnapshots = async (
|
||||
id: string,
|
||||
organizationId: string,
|
||||
snapshotIds: string[],
|
||||
tags: { add?: string[]; remove?: string[]; set?: string[] },
|
||||
) => {
|
||||
const repository = await findRepository(id);
|
||||
const repository = await findRepository(id, organizationId);
|
||||
|
||||
if (!repository) {
|
||||
throw new NotFoundError("Repository not found");
|
||||
|
|
@ -469,8 +477,8 @@ const tagSnapshots = async (
|
|||
}
|
||||
};
|
||||
|
||||
const updateRepository = async (id: string, updates: { name?: string; compressionMode?: CompressionMode }) => {
|
||||
const existing = await findRepository(id);
|
||||
const updateRepository = async (id: string, organizationId: string, updates: { name?: string; compressionMode?: CompressionMode }) => {
|
||||
const existing = await findRepository(id, organizationId);
|
||||
|
||||
if (!existing) {
|
||||
throw new NotFoundError("Repository not found");
|
||||
|
|
|
|||
|
|
@ -52,13 +52,13 @@ export const volumeController = new Hono()
|
|||
})
|
||||
.delete("/:name", deleteVolumeDto, async (c) => {
|
||||
const { name } = c.req.param();
|
||||
await volumeService.deleteVolume(name);
|
||||
await volumeService.deleteVolume(name, c.get("organizationId"));
|
||||
|
||||
return c.json({ message: "Volume deleted" }, 200);
|
||||
})
|
||||
.get("/:name", getVolumeDto, async (c) => {
|
||||
const { name } = c.req.param();
|
||||
const res = await volumeService.getVolume(name);
|
||||
const res = await volumeService.getVolume(name, c.get("organizationId"));
|
||||
|
||||
const response = {
|
||||
volume: {
|
||||
|
|
@ -77,7 +77,7 @@ export const volumeController = new Hono()
|
|||
.put("/:name", updateVolumeDto, validator("json", updateVolumeBody), async (c) => {
|
||||
const { name } = c.req.param();
|
||||
const body = c.req.valid("json");
|
||||
const res = await volumeService.updateVolume(name, body);
|
||||
const res = await volumeService.updateVolume(name, body, c.get("organizationId"));
|
||||
|
||||
const response = {
|
||||
...res.volume,
|
||||
|
|
@ -88,26 +88,26 @@ export const volumeController = new Hono()
|
|||
})
|
||||
.post("/:name/mount", mountVolumeDto, async (c) => {
|
||||
const { name } = c.req.param();
|
||||
const { error, status } = await volumeService.mountVolume(name);
|
||||
const { error, status } = await volumeService.mountVolume(name, c.get("organizationId"));
|
||||
|
||||
return c.json({ error, status }, error ? 500 : 200);
|
||||
})
|
||||
.post("/:name/unmount", unmountVolumeDto, async (c) => {
|
||||
const { name } = c.req.param();
|
||||
const { error, status } = await volumeService.unmountVolume(name);
|
||||
const { error, status } = await volumeService.unmountVolume(name, c.get("organizationId"));
|
||||
|
||||
return c.json({ error, status }, error ? 500 : 200);
|
||||
})
|
||||
.post("/:name/health-check", healthCheckDto, async (c) => {
|
||||
const { name } = c.req.param();
|
||||
const { error, status } = await volumeService.checkHealth(name);
|
||||
const { error, status } = await volumeService.checkHealth(name, c.get("organizationId"));
|
||||
|
||||
return c.json({ error, status }, 200);
|
||||
})
|
||||
.get("/:name/files", listFilesDto, async (c) => {
|
||||
const { name } = c.req.param();
|
||||
const subPath = c.req.query("path");
|
||||
const result = await volumeService.listFiles(name, subPath);
|
||||
const result = await volumeService.listFiles(name, c.get("organizationId"), subPath);
|
||||
|
||||
const response = {
|
||||
files: result.files,
|
||||
|
|
|
|||
|
|
@ -54,7 +54,7 @@ const createVolume = async (name: string, backendConfig: BackendConfig, organiza
|
|||
const slug = slugify(name, { lower: true, strict: true });
|
||||
|
||||
const existing = await db.query.volumesTable.findFirst({
|
||||
where: eq(volumesTable.name, slug),
|
||||
where: and(eq(volumesTable.name, slug), eq(volumesTable.organizationId, organizationId)),
|
||||
});
|
||||
|
||||
if (existing) {
|
||||
|
|
@ -90,9 +90,9 @@ const createVolume = async (name: string, backendConfig: BackendConfig, organiza
|
|||
return { volume: created, status: 201 };
|
||||
};
|
||||
|
||||
const deleteVolume = async (name: string) => {
|
||||
const deleteVolume = async (name: string, organizationId: string) => {
|
||||
const volume = await db.query.volumesTable.findFirst({
|
||||
where: eq(volumesTable.name, name),
|
||||
where: and(eq(volumesTable.name, name), eq(volumesTable.organizationId, organizationId)),
|
||||
});
|
||||
|
||||
if (!volume) {
|
||||
|
|
@ -104,9 +104,9 @@ const deleteVolume = async (name: string) => {
|
|||
await db.delete(volumesTable).where(eq(volumesTable.name, name));
|
||||
};
|
||||
|
||||
const mountVolume = async (name: string) => {
|
||||
const mountVolume = async (name: string, organizationId: string) => {
|
||||
const volume = await db.query.volumesTable.findFirst({
|
||||
where: eq(volumesTable.name, name),
|
||||
where: and(eq(volumesTable.name, name), eq(volumesTable.organizationId, organizationId)),
|
||||
});
|
||||
|
||||
if (!volume) {
|
||||
|
|
@ -128,9 +128,9 @@ const mountVolume = async (name: string) => {
|
|||
return { error, status };
|
||||
};
|
||||
|
||||
const unmountVolume = async (name: string) => {
|
||||
const unmountVolume = async (name: string, organizationId: string) => {
|
||||
const volume = await db.query.volumesTable.findFirst({
|
||||
where: eq(volumesTable.name, name),
|
||||
where: and(eq(volumesTable.name, name), eq(volumesTable.organizationId, organizationId)),
|
||||
});
|
||||
|
||||
if (!volume) {
|
||||
|
|
@ -149,9 +149,9 @@ const unmountVolume = async (name: string) => {
|
|||
return { error, status };
|
||||
};
|
||||
|
||||
const getVolume = async (name: string) => {
|
||||
const getVolume = async (name: string, organizationId: string) => {
|
||||
const volume = await db.query.volumesTable.findFirst({
|
||||
where: eq(volumesTable.name, name),
|
||||
where: and(eq(volumesTable.name, name), eq(volumesTable.organizationId, organizationId)),
|
||||
});
|
||||
|
||||
if (!volume) {
|
||||
|
|
@ -169,9 +169,9 @@ const getVolume = async (name: string) => {
|
|||
return { volume, statfs };
|
||||
};
|
||||
|
||||
const updateVolume = async (name: string, volumeData: UpdateVolumeBody) => {
|
||||
const updateVolume = async (name: string, volumeData: UpdateVolumeBody, organizationId: string) => {
|
||||
const existing = await db.query.volumesTable.findFirst({
|
||||
where: eq(volumesTable.name, name),
|
||||
where: and(eq(volumesTable.name, name), eq(volumesTable.organizationId, organizationId)),
|
||||
});
|
||||
|
||||
if (!existing) {
|
||||
|
|
@ -183,7 +183,7 @@ const updateVolume = async (name: string, volumeData: UpdateVolumeBody) => {
|
|||
const newSlug = slugify(volumeData.name, { lower: true, strict: true });
|
||||
|
||||
const conflict = await db.query.volumesTable.findFirst({
|
||||
where: and(eq(volumesTable.name, newSlug), ne(volumesTable.id, existing.id)),
|
||||
where: and(eq(volumesTable.name, newSlug), ne(volumesTable.id, existing.id), eq(volumesTable.organizationId, organizationId)),
|
||||
});
|
||||
|
||||
if (conflict) {
|
||||
|
|
@ -272,9 +272,9 @@ const testConnection = async (backendConfig: BackendConfig) => {
|
|||
};
|
||||
};
|
||||
|
||||
const checkHealth = async (name: string) => {
|
||||
const checkHealth = async (name: string, organizationId: string) => {
|
||||
const volume = await db.query.volumesTable.findFirst({
|
||||
where: eq(volumesTable.name, name),
|
||||
where: and(eq(volumesTable.name, name), eq(volumesTable.organizationId, organizationId)),
|
||||
});
|
||||
|
||||
if (!volume) {
|
||||
|
|
@ -296,9 +296,9 @@ const checkHealth = async (name: string) => {
|
|||
return { status, error };
|
||||
};
|
||||
|
||||
const listFiles = async (name: string, subPath?: string) => {
|
||||
const listFiles = async (name: string, organizationId: string, subPath?: string) => {
|
||||
const volume = await db.query.volumesTable.findFirst({
|
||||
where: eq(volumesTable.name, name),
|
||||
where: and(eq(volumesTable.name, name), eq(volumesTable.organizationId, organizationId)),
|
||||
});
|
||||
|
||||
if (!volume) {
|
||||
|
|
|
|||
Loading…
Reference in a new issue