chore: pr feedbacks

This commit is contained in:
Nicolas Meienberger
2025-12-03 20:24:41 +01:00
parent 16b8be2cd9
commit fc6f628dd4
8 changed files with 994 additions and 181 deletions

View File

@@ -1,5 +1,5 @@
import { relations, sql } from "drizzle-orm";
import { int, integer, sqliteTable, text, primaryKey } from "drizzle-orm/sqlite-core";
import { int, integer, sqliteTable, text, primaryKey, uniqueIndex, unique } from "drizzle-orm/sqlite-core";
import type { CompressionMode, RepositoryBackend, repositoryConfigSchema, RepositoryStatus } from "~/schemas/restic";
import type { BackendStatus, BackendType, volumeConfigSchema } from "~/schemas/volumes";
import type { NotificationType, notificationConfigSchema } from "~/schemas/notifications";
@@ -160,20 +160,24 @@ export type BackupScheduleNotification = typeof backupScheduleNotificationsTable
* Backup Schedule Mirrors Junction Table (Many-to-Many)
* Allows copying snapshots to secondary repositories after backup completes
*/
export const backupScheduleMirrorsTable = sqliteTable("backup_schedule_mirrors_table", {
id: int().primaryKey({ autoIncrement: true }),
scheduleId: int("schedule_id")
.notNull()
.references(() => backupSchedulesTable.id, { onDelete: "cascade" }),
repositoryId: text("repository_id")
.notNull()
.references(() => repositoriesTable.id, { onDelete: "cascade" }),
enabled: int("enabled", { mode: "boolean" }).notNull().default(true),
lastCopyAt: int("last_copy_at", { mode: "number" }),
lastCopyStatus: text("last_copy_status").$type<"success" | "error">(),
lastCopyError: text("last_copy_error"),
createdAt: int("created_at", { mode: "number" }).notNull().default(sql`(unixepoch() * 1000)`),
});
export const backupScheduleMirrorsTable = sqliteTable(
"backup_schedule_mirrors_table",
{
id: int().primaryKey({ autoIncrement: true }),
scheduleId: int("schedule_id")
.notNull()
.references(() => backupSchedulesTable.id, { onDelete: "cascade" }),
repositoryId: text("repository_id")
.notNull()
.references(() => repositoriesTable.id, { onDelete: "cascade" }),
enabled: int("enabled", { mode: "boolean" }).notNull().default(true),
lastCopyAt: int("last_copy_at", { mode: "number" }),
lastCopyStatus: text("last_copy_status").$type<"success" | "error">(),
lastCopyError: text("last_copy_error"),
createdAt: int("created_at", { mode: "number" }).notNull().default(sql`(unixepoch() * 1000)`),
},
(table) => [unique().on(table.scheduleId, table.repositoryId)],
);
export const backupScheduleMirrorRelations = relations(backupScheduleMirrorsTable, ({ one }) => ({
schedule: one(backupSchedulesTable, {

View File

@@ -522,13 +522,25 @@ const copyToMirrors = async (
repositoryName: mirror.repository.name,
});
await restic.copy(sourceRepository.config, mirror.repository.config, {
tag: scheduleId.toString(),
});
const releaseSource = await repoMutex.acquireShared(sourceRepository.id, `mirror_source:${scheduleId}`);
const releaseMirror = await repoMutex.acquireShared(mirror.repository.id, `mirror:${scheduleId}`);
try {
await restic.copy(sourceRepository.config, mirror.repository.config, { tag: scheduleId.toString() });
} finally {
releaseSource();
releaseMirror();
}
if (retentionPolicy) {
logger.info(`[Background] Applying retention policy to mirror repository: ${mirror.repository.name}`);
await restic.forget(mirror.repository.config, retentionPolicy, { tag: scheduleId.toString() });
const releaseForget = await repoMutex.acquireExclusive(mirror.repository.id, `forget:mirror:${scheduleId}`);
try {
logger.info(`[Background] Applying retention policy to mirror repository: ${mirror.repository.name}`);
await restic.forget(mirror.repository.config, retentionPolicy, { tag: scheduleId.toString() });
} finally {
releaseForget();
}
}
await db

View File

@@ -202,7 +202,7 @@ const init = async (config: RepositoryConfig) => {
const env = await buildEnv(config);
const args = ["init", "--repo", repoUrl];
addCommonArgs(args, config, env);
addCommonArgs(args, env);
const res = await $`restic ${args}`.env(env).nothrow();
await cleanupTemporaryKeys(config, env);
@@ -278,7 +278,7 @@ const backup = async (
}
}
addCommonArgs(args, config, env);
addCommonArgs(args, env);
const logData = throttle((data: string) => {
logger.info(data.trim());
@@ -404,7 +404,7 @@ const restore = async (
}
}
addCommonArgs(args, config, env);
addCommonArgs(args, env);
logger.debug(`Executing: restic ${args.join(" ")}`);
const res = await $`restic ${args}`.env(env).nothrow();
@@ -467,7 +467,7 @@ const snapshots = async (config: RepositoryConfig, options: { tags?: string[] }
}
}
addCommonArgs(args, config, env);
addCommonArgs(args, env);
const res = await $`restic ${args}`.env(env).nothrow().quiet();
await cleanupTemporaryKeys(config, env);
@@ -516,7 +516,7 @@ const forget = async (config: RepositoryConfig, options: RetentionPolicy, extra:
}
args.push("--prune");
addCommonArgs(args, config, env);
addCommonArgs(args, env);
const res = await $`restic ${args}`.env(env).nothrow();
await cleanupTemporaryKeys(config, env);
@@ -534,7 +534,7 @@ const deleteSnapshot = async (config: RepositoryConfig, snapshotId: string) => {
const env = await buildEnv(config);
const args: string[] = ["--repo", repoUrl, "forget", snapshotId, "--prune"];
addCommonArgs(args, config, env);
addCommonArgs(args, env);
const res = await $`restic ${args}`.env(env).nothrow();
await cleanupTemporaryKeys(config, env);
@@ -584,7 +584,7 @@ const ls = async (config: RepositoryConfig, snapshotId: string, path?: string) =
args.push(path);
}
addCommonArgs(args, config, env);
addCommonArgs(args, env);
const res = await safeSpawn({ command: "restic", args, env });
await cleanupTemporaryKeys(config, env);
@@ -635,7 +635,7 @@ const unlock = async (config: RepositoryConfig) => {
const env = await buildEnv(config);
const args = ["unlock", "--repo", repoUrl, "--remove-all"];
addCommonArgs(args, config, env);
addCommonArgs(args, env);
const res = await $`restic ${args}`.env(env).nothrow();
await cleanupTemporaryKeys(config, env);
@@ -659,7 +659,7 @@ const check = async (config: RepositoryConfig, options?: { readData?: boolean })
args.push("--read-data");
}
addCommonArgs(args, config, env);
addCommonArgs(args, env);
const res = await $`restic ${args}`.env(env).nothrow();
await cleanupTemporaryKeys(config, env);
@@ -693,7 +693,7 @@ const repairIndex = async (config: RepositoryConfig) => {
const env = await buildEnv(config);
const args = ["repair", "index", "--repo", repoUrl];
addCommonArgs(args, config, env);
addCommonArgs(args, env);
const res = await $`restic ${args}`.env(env).nothrow();
await cleanupTemporaryKeys(config, env);
@@ -746,7 +746,7 @@ const copy = async (
args.push("latest");
}
addCommonArgs(args, destConfig, destEnv);
addCommonArgs(args, env);
if (sourceConfig.backend === "sftp" && sourceEnv._SFTP_SSH_ARGS) {
args.push("-o", `sftp.args=${sourceEnv._SFTP_SSH_ARGS}`);
@@ -785,9 +785,10 @@ const cleanupTemporaryKeys = async (config: RepositoryConfig, env: Record<string
}
};
const addCommonArgs = (args: string[], config: RepositoryConfig, env: Record<string, string>) => {
const addCommonArgs = (args: string[], env: Record<string, string>) => {
args.push("--retry-lock", "1m", "--json");
if (config.backend === "sftp" && env._SFTP_SSH_ARGS) {
if (env._SFTP_SSH_ARGS) {
args.push("-o", `sftp.args=${env._SFTP_SSH_ARGS}`);
}
};