mirror of
https://github.com/nicotsx/ironmount.git
synced 2025-12-10 12:10:51 +01:00
refactor(backups): tag snapshots by backup id and run forget by grouping first by tags
This commit is contained in:
@@ -32,7 +32,6 @@ import {
|
||||
getBackupSchedule,
|
||||
updateBackupSchedule,
|
||||
getBackupScheduleForVolume,
|
||||
upsertBackupSchedule,
|
||||
runBackupNow,
|
||||
} from "../sdk.gen";
|
||||
import { queryOptions, type UseMutationOptions, type DefaultError } from "@tanstack/react-query";
|
||||
@@ -83,8 +82,6 @@ import type {
|
||||
UpdateBackupScheduleData,
|
||||
UpdateBackupScheduleResponse,
|
||||
GetBackupScheduleForVolumeData,
|
||||
UpsertBackupScheduleData,
|
||||
UpsertBackupScheduleResponse,
|
||||
RunBackupNowData,
|
||||
RunBackupNowResponse,
|
||||
} from "../types.gen";
|
||||
@@ -957,29 +954,6 @@ export const getBackupScheduleForVolumeOptions = (options: Options<GetBackupSche
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Create or update a backup schedule for a volume
|
||||
*/
|
||||
export const upsertBackupScheduleMutation = (
|
||||
options?: Partial<Options<UpsertBackupScheduleData>>,
|
||||
): UseMutationOptions<UpsertBackupScheduleResponse, DefaultError, Options<UpsertBackupScheduleData>> => {
|
||||
const mutationOptions: UseMutationOptions<
|
||||
UpsertBackupScheduleResponse,
|
||||
DefaultError,
|
||||
Options<UpsertBackupScheduleData>
|
||||
> = {
|
||||
mutationFn: async (localOptions) => {
|
||||
const { data } = await upsertBackupSchedule({
|
||||
...options,
|
||||
...localOptions,
|
||||
throwOnError: true,
|
||||
});
|
||||
return data;
|
||||
},
|
||||
};
|
||||
return mutationOptions;
|
||||
};
|
||||
|
||||
export const runBackupNowQueryKey = (options: Options<RunBackupNowData>) => createQueryKey("runBackupNow", options);
|
||||
|
||||
/**
|
||||
|
||||
@@ -68,8 +68,6 @@ import type {
|
||||
UpdateBackupScheduleResponses,
|
||||
GetBackupScheduleForVolumeData,
|
||||
GetBackupScheduleForVolumeResponses,
|
||||
UpsertBackupScheduleData,
|
||||
UpsertBackupScheduleResponses,
|
||||
RunBackupNowData,
|
||||
RunBackupNowResponses,
|
||||
} from "./types.gen";
|
||||
@@ -474,22 +472,6 @@ export const getBackupScheduleForVolume = <ThrowOnError extends boolean = false>
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Create or update a backup schedule for a volume
|
||||
*/
|
||||
export const upsertBackupSchedule = <ThrowOnError extends boolean = false>(
|
||||
options?: Options<UpsertBackupScheduleData, ThrowOnError>,
|
||||
) => {
|
||||
return (options?.client ?? _heyApiClient).put<UpsertBackupScheduleResponses, unknown, ThrowOnError>({
|
||||
url: "/api/v1/backups/upsert",
|
||||
...options,
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
...options?.headers,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Trigger a backup immediately for a schedule
|
||||
*/
|
||||
|
||||
@@ -782,7 +782,7 @@ export type ListSnapshotsData = {
|
||||
name: string;
|
||||
};
|
||||
query?: {
|
||||
volumeId?: string;
|
||||
backupId?: string;
|
||||
};
|
||||
url: "/api/v1/repositories/{name}/snapshots";
|
||||
};
|
||||
@@ -1181,11 +1181,11 @@ export type GetBackupScheduleResponse = GetBackupScheduleResponses[keyof GetBack
|
||||
|
||||
export type UpdateBackupScheduleData = {
|
||||
body?: {
|
||||
cronExpression?: string;
|
||||
cronExpression: string;
|
||||
repositoryId: string;
|
||||
enabled?: boolean;
|
||||
excludePatterns?: Array<string>;
|
||||
includePatterns?: Array<string>;
|
||||
repositoryId?: string;
|
||||
retentionPolicy?: {
|
||||
keepDaily?: number;
|
||||
keepHourly?: number;
|
||||
@@ -1342,62 +1342,6 @@ export type GetBackupScheduleForVolumeResponses = {
|
||||
export type GetBackupScheduleForVolumeResponse =
|
||||
GetBackupScheduleForVolumeResponses[keyof GetBackupScheduleForVolumeResponses];
|
||||
|
||||
export type UpsertBackupScheduleData = {
|
||||
body?: {
|
||||
cronExpression: string;
|
||||
enabled: boolean;
|
||||
repositoryId: string;
|
||||
volumeId: number;
|
||||
excludePatterns?: Array<string>;
|
||||
includePatterns?: Array<string>;
|
||||
retentionPolicy?: {
|
||||
keepDaily?: number;
|
||||
keepHourly?: number;
|
||||
keepLast?: number;
|
||||
keepMonthly?: number;
|
||||
keepWeekly?: number;
|
||||
keepWithinDuration?: string;
|
||||
keepYearly?: number;
|
||||
};
|
||||
tags?: Array<string>;
|
||||
};
|
||||
path?: never;
|
||||
query?: never;
|
||||
url: "/api/v1/backups/upsert";
|
||||
};
|
||||
|
||||
export type UpsertBackupScheduleResponses = {
|
||||
/**
|
||||
* Backup schedule upserted successfully
|
||||
*/
|
||||
200: {
|
||||
createdAt: number;
|
||||
cronExpression: string;
|
||||
enabled: boolean;
|
||||
excludePatterns: Array<string> | null;
|
||||
id: number;
|
||||
includePatterns: Array<string> | null;
|
||||
lastBackupAt: number | null;
|
||||
lastBackupError: string | null;
|
||||
lastBackupStatus: "error" | "success" | null;
|
||||
nextBackupAt: number | null;
|
||||
repositoryId: string;
|
||||
retentionPolicy: {
|
||||
keepDaily?: number;
|
||||
keepHourly?: number;
|
||||
keepLast?: number;
|
||||
keepMonthly?: number;
|
||||
keepWeekly?: number;
|
||||
keepWithinDuration?: string;
|
||||
keepYearly?: number;
|
||||
} | null;
|
||||
updatedAt: number;
|
||||
volumeId: number;
|
||||
};
|
||||
};
|
||||
|
||||
export type UpsertBackupScheduleResponse = UpsertBackupScheduleResponses[keyof UpsertBackupScheduleResponses];
|
||||
|
||||
export type RunBackupNowData = {
|
||||
body?: never;
|
||||
path: {
|
||||
|
||||
@@ -6,7 +6,7 @@ import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "~
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from "~/components/ui/tooltip";
|
||||
import { formatSnapshotDuration } from "~/modules/repositories/tabs/snapshots";
|
||||
|
||||
type Snapshot = ListSnapshotsResponse["snapshots"][0];
|
||||
type Snapshot = ListSnapshotsResponse[number];
|
||||
|
||||
type Props = {
|
||||
snapshots: Snapshot[];
|
||||
|
||||
@@ -4,11 +4,11 @@ import { redirect, useNavigate } from "react-router";
|
||||
import { toast } from "sonner";
|
||||
import { Button } from "~/components/ui/button";
|
||||
import {
|
||||
upsertBackupScheduleMutation,
|
||||
getBackupScheduleOptions,
|
||||
runBackupNowMutation,
|
||||
deleteBackupScheduleMutation,
|
||||
listSnapshotsOptions,
|
||||
updateBackupScheduleMutation,
|
||||
} from "~/api-client/@tanstack/react-query.gen";
|
||||
import { parseError } from "~/lib/errors";
|
||||
import { getCronExpression } from "~/utils/utils";
|
||||
@@ -26,7 +26,7 @@ export const clientLoader = async ({ params }: Route.LoaderArgs) => {
|
||||
|
||||
const snapshots = await listSnapshots({
|
||||
path: { name: data.repository.name },
|
||||
query: { volumeId: data.volumeId.toString() },
|
||||
query: { backupId: params.id },
|
||||
});
|
||||
|
||||
if (snapshots.data) return { snapshots: snapshots.data, schedule: data };
|
||||
@@ -49,13 +49,13 @@ export default function ScheduleDetailsPage({ params, loaderData }: Route.Compon
|
||||
const { data: snapshots } = useQuery({
|
||||
...listSnapshotsOptions({
|
||||
path: { name: schedule.repository.name },
|
||||
query: { volumeId: schedule.volumeId.toString() },
|
||||
query: { backupId: schedule.id.toString() },
|
||||
}),
|
||||
initialData: loaderData.snapshots,
|
||||
});
|
||||
|
||||
const upsertSchedule = useMutation({
|
||||
...upsertBackupScheduleMutation(),
|
||||
...updateBackupScheduleMutation(),
|
||||
onSuccess: () => {
|
||||
toast.success("Backup schedule saved successfully");
|
||||
setIsEditMode(false);
|
||||
@@ -106,8 +106,8 @@ export default function ScheduleDetailsPage({ params, loaderData }: Route.Compon
|
||||
if (formValues.keepYearly) retentionPolicy.keepYearly = formValues.keepYearly;
|
||||
|
||||
upsertSchedule.mutate({
|
||||
path: { scheduleId: schedule.id.toString() },
|
||||
body: {
|
||||
volumeId: schedule.volumeId,
|
||||
repositoryId: formValues.repositoryId,
|
||||
enabled: schedule.enabled,
|
||||
cronExpression,
|
||||
@@ -122,8 +122,8 @@ export default function ScheduleDetailsPage({ params, loaderData }: Route.Compon
|
||||
if (!schedule) return;
|
||||
|
||||
upsertSchedule.mutate({
|
||||
path: { scheduleId: schedule.id.toString() },
|
||||
body: {
|
||||
volumeId: schedule.volumeId,
|
||||
repositoryId: schedule.repositoryId,
|
||||
enabled,
|
||||
cronExpression: schedule.cronExpression,
|
||||
|
||||
@@ -8,10 +8,8 @@ import {
|
||||
getBackupScheduleForVolumeDto,
|
||||
listBackupSchedulesDto,
|
||||
runBackupNowDto,
|
||||
updateBackupScheduleBody,
|
||||
updateBackupScheduleDto,
|
||||
upsertBackupScheduleBody,
|
||||
upsertBackupScheduleDto,
|
||||
updateBackupScheduleBody,
|
||||
type CreateBackupScheduleDto,
|
||||
type DeleteBackupScheduleDto,
|
||||
type GetBackupScheduleDto,
|
||||
@@ -19,7 +17,6 @@ import {
|
||||
type ListBackupSchedulesResponseDto,
|
||||
type RunBackupNowDto,
|
||||
type UpdateBackupScheduleDto,
|
||||
type UpsertBackupScheduleDto,
|
||||
} from "./backups.dto";
|
||||
import { backupsService } from "./backups.service";
|
||||
|
||||
@@ -57,13 +54,6 @@ export const backupScheduleController = new Hono()
|
||||
|
||||
return c.json<UpdateBackupScheduleDto>(schedule, 200);
|
||||
})
|
||||
.put("/upsert", upsertBackupScheduleDto, validator("json", upsertBackupScheduleBody), async (c) => {
|
||||
const body = c.req.valid("json");
|
||||
|
||||
const schedule = await backupsService.upsertSchedule(body);
|
||||
|
||||
return c.json<UpsertBackupScheduleDto>(schedule, 200);
|
||||
})
|
||||
.delete("/:scheduleId", deleteBackupScheduleDto, async (c) => {
|
||||
const scheduleId = c.req.param("scheduleId");
|
||||
|
||||
@@ -74,7 +64,7 @@ export const backupScheduleController = new Hono()
|
||||
.post("/:scheduleId/run", runBackupNowDto, async (c) => {
|
||||
const scheduleId = c.req.param("scheduleId");
|
||||
|
||||
backupsService.executeBackup(Number(scheduleId)).catch((error) => {
|
||||
backupsService.executeBackup(Number(scheduleId), true).catch((error) => {
|
||||
console.error("Backup execution failed:", error);
|
||||
});
|
||||
|
||||
|
||||
@@ -143,9 +143,9 @@ export const createBackupScheduleDto = describeRoute({
|
||||
* Update a backup schedule
|
||||
*/
|
||||
export const updateBackupScheduleBody = type({
|
||||
repositoryId: "string?",
|
||||
repositoryId: "string",
|
||||
enabled: "boolean?",
|
||||
cronExpression: "string?",
|
||||
cronExpression: "string",
|
||||
retentionPolicy: retentionPolicySchema.optional(),
|
||||
excludePatterns: "string[]?",
|
||||
includePatterns: "string[]?",
|
||||
@@ -174,42 +174,6 @@ export const updateBackupScheduleDto = describeRoute({
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* Upsert a backup schedule (create or update)
|
||||
*/
|
||||
export const upsertBackupScheduleBody = type({
|
||||
volumeId: "number",
|
||||
repositoryId: "string",
|
||||
enabled: "boolean",
|
||||
cronExpression: "string",
|
||||
retentionPolicy: retentionPolicySchema.optional(),
|
||||
excludePatterns: "string[]?",
|
||||
includePatterns: "string[]?",
|
||||
tags: "string[]?",
|
||||
});
|
||||
|
||||
export type UpsertBackupScheduleBody = typeof upsertBackupScheduleBody.infer;
|
||||
|
||||
export const upsertBackupScheduleResponse = backupScheduleSchema.omit("volume", "repository");
|
||||
|
||||
export type UpsertBackupScheduleDto = typeof upsertBackupScheduleResponse.infer;
|
||||
|
||||
export const upsertBackupScheduleDto = describeRoute({
|
||||
description: "Create or update a backup schedule for a volume",
|
||||
operationId: "upsertBackupSchedule",
|
||||
tags: ["Backups"],
|
||||
responses: {
|
||||
200: {
|
||||
description: "Backup schedule upserted successfully",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: resolver(upsertBackupScheduleResponse),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* Delete a backup schedule
|
||||
*/
|
||||
|
||||
@@ -7,7 +7,7 @@ import { backupSchedulesTable, repositoriesTable, volumesTable } from "../../db/
|
||||
import { restic } from "../../utils/restic";
|
||||
import { logger } from "../../utils/logger";
|
||||
import { getVolumePath } from "../volumes/helpers";
|
||||
import type { CreateBackupScheduleBody, UpdateBackupScheduleBody, UpsertBackupScheduleBody } from "./backups.dto";
|
||||
import type { CreateBackupScheduleBody, UpdateBackupScheduleBody } from "./backups.dto";
|
||||
import { toMessage } from "../../utils/errors";
|
||||
|
||||
const calculateNextRun = (cronExpression: string): number => {
|
||||
@@ -109,14 +109,12 @@ const updateSchedule = async (scheduleId: number, data: UpdateBackupScheduleBody
|
||||
throw new BadRequestError("Invalid cron expression");
|
||||
}
|
||||
|
||||
if (data.repositoryId) {
|
||||
const repository = await db.query.repositoriesTable.findFirst({
|
||||
where: eq(repositoriesTable.id, data.repositoryId),
|
||||
});
|
||||
const repository = await db.query.repositoriesTable.findFirst({
|
||||
where: eq(repositoriesTable.id, data.repositoryId),
|
||||
});
|
||||
|
||||
if (!repository) {
|
||||
throw new NotFoundError("Repository not found");
|
||||
}
|
||||
if (!repository) {
|
||||
throw new NotFoundError("Repository not found");
|
||||
}
|
||||
|
||||
const cronExpression = data.cronExpression ?? schedule.cronExpression;
|
||||
@@ -147,7 +145,7 @@ const deleteSchedule = async (scheduleId: number) => {
|
||||
await db.delete(backupSchedulesTable).where(eq(backupSchedulesTable.id, scheduleId));
|
||||
};
|
||||
|
||||
const executeBackup = async (scheduleId: number) => {
|
||||
const executeBackup = async (scheduleId: number, manual = false) => {
|
||||
const schedule = await db.query.backupSchedulesTable.findFirst({
|
||||
where: eq(backupSchedulesTable.id, scheduleId),
|
||||
});
|
||||
@@ -156,7 +154,7 @@ const executeBackup = async (scheduleId: number) => {
|
||||
throw new NotFoundError("Backup schedule not found");
|
||||
}
|
||||
|
||||
if (!schedule.enabled) {
|
||||
if (!schedule.enabled && !manual) {
|
||||
logger.info(`Backup schedule ${scheduleId} is disabled. Skipping execution.`);
|
||||
return;
|
||||
}
|
||||
@@ -190,7 +188,9 @@ const executeBackup = async (scheduleId: number) => {
|
||||
exclude?: string[];
|
||||
include?: string[];
|
||||
tags?: string[];
|
||||
} = {};
|
||||
} = {
|
||||
tags: [schedule.id.toString()],
|
||||
};
|
||||
|
||||
if (schedule.excludePatterns && schedule.excludePatterns.length > 0) {
|
||||
backupOptions.exclude = schedule.excludePatterns;
|
||||
@@ -203,7 +203,7 @@ const executeBackup = async (scheduleId: number) => {
|
||||
await restic.backup(repository.config, volumePath, backupOptions);
|
||||
|
||||
if (schedule.retentionPolicy) {
|
||||
await restic.forget(repository.config, schedule.retentionPolicy);
|
||||
await restic.forget(repository.config, schedule.retentionPolicy, { tag: schedule.id.toString() });
|
||||
}
|
||||
|
||||
const nextBackupAt = calculateNextRun(schedule.cronExpression);
|
||||
@@ -262,77 +262,6 @@ const getScheduleForVolume = async (volumeId: number) => {
|
||||
return schedule ?? null;
|
||||
};
|
||||
|
||||
const upsertSchedule = async (data: UpsertBackupScheduleBody) => {
|
||||
if (!cron.validate(data.cronExpression)) {
|
||||
throw new BadRequestError("Invalid cron expression");
|
||||
}
|
||||
|
||||
const volume = await db.query.volumesTable.findFirst({
|
||||
where: eq(volumesTable.id, data.volumeId),
|
||||
});
|
||||
|
||||
if (!volume) {
|
||||
throw new NotFoundError("Volume not found");
|
||||
}
|
||||
|
||||
const repository = await db.query.repositoriesTable.findFirst({
|
||||
where: eq(repositoriesTable.id, data.repositoryId),
|
||||
});
|
||||
|
||||
if (!repository) {
|
||||
throw new NotFoundError("Repository not found");
|
||||
}
|
||||
|
||||
const existingSchedule = await db.query.backupSchedulesTable.findFirst({
|
||||
where: eq(backupSchedulesTable.volumeId, data.volumeId),
|
||||
});
|
||||
|
||||
const nextBackupAt = calculateNextRun(data.cronExpression);
|
||||
|
||||
if (existingSchedule) {
|
||||
const [updated] = await db
|
||||
.update(backupSchedulesTable)
|
||||
.set({
|
||||
repositoryId: data.repositoryId,
|
||||
enabled: data.enabled,
|
||||
cronExpression: data.cronExpression,
|
||||
retentionPolicy: data.retentionPolicy ?? null,
|
||||
excludePatterns: data.excludePatterns ?? [],
|
||||
includePatterns: data.includePatterns ?? [],
|
||||
nextBackupAt: nextBackupAt,
|
||||
updatedAt: Date.now(),
|
||||
})
|
||||
.where(eq(backupSchedulesTable.id, existingSchedule.id))
|
||||
.returning();
|
||||
|
||||
if (!updated) {
|
||||
throw new Error("Failed to update backup schedule");
|
||||
}
|
||||
|
||||
return updated;
|
||||
}
|
||||
|
||||
const [newSchedule] = await db
|
||||
.insert(backupSchedulesTable)
|
||||
.values({
|
||||
volumeId: data.volumeId,
|
||||
repositoryId: data.repositoryId,
|
||||
enabled: data.enabled,
|
||||
cronExpression: data.cronExpression,
|
||||
retentionPolicy: data.retentionPolicy ?? null,
|
||||
excludePatterns: data.excludePatterns ?? [],
|
||||
includePatterns: data.includePatterns ?? [],
|
||||
nextBackupAt: nextBackupAt,
|
||||
})
|
||||
.returning();
|
||||
|
||||
if (!newSchedule) {
|
||||
throw new Error("Failed to create backup schedule");
|
||||
}
|
||||
|
||||
return newSchedule;
|
||||
};
|
||||
|
||||
export const backupsService = {
|
||||
listSchedules,
|
||||
getSchedule,
|
||||
@@ -342,5 +271,4 @@ export const backupsService = {
|
||||
executeBackup,
|
||||
getSchedulesToExecute,
|
||||
getScheduleForVolume,
|
||||
upsertSchedule,
|
||||
};
|
||||
|
||||
@@ -49,9 +49,9 @@ export const repositoriesController = new Hono()
|
||||
})
|
||||
.get("/:name/snapshots", listSnapshotsDto, validator("query", listSnapshotsFilters), async (c) => {
|
||||
const { name } = c.req.param();
|
||||
const { volumeId } = c.req.valid("query");
|
||||
const { backupId } = c.req.valid("query");
|
||||
|
||||
const res = await repositoriesService.listSnapshots(name, Number(volumeId));
|
||||
const res = await repositoriesService.listSnapshots(name, backupId);
|
||||
|
||||
const snapshots = res.map((snapshot) => {
|
||||
const { summary } = snapshot;
|
||||
|
||||
@@ -144,7 +144,7 @@ const listSnapshotsResponse = snapshotSchema.array();
|
||||
export type ListSnapshotsDto = typeof listSnapshotsResponse.infer;
|
||||
|
||||
export const listSnapshotsFilters = type({
|
||||
volumeId: "string?",
|
||||
backupId: "string?",
|
||||
});
|
||||
|
||||
export const listSnapshotsDto = describeRoute({
|
||||
|
||||
@@ -8,7 +8,6 @@ import { repositoriesTable, volumesTable } from "../../db/schema";
|
||||
import { toMessage } from "../../utils/errors";
|
||||
import { restic } from "../../utils/restic";
|
||||
import { cryptoUtils } from "../../utils/crypto";
|
||||
import { getVolumePath } from "../volumes/helpers";
|
||||
|
||||
const listRepositories = async () => {
|
||||
const repositories = await db.query.repositoriesTable.findMany({});
|
||||
@@ -106,7 +105,15 @@ const deleteRepository = async (name: string) => {
|
||||
await db.delete(repositoriesTable).where(eq(repositoriesTable.name, name));
|
||||
};
|
||||
|
||||
const listSnapshots = async (name: string, volumeId?: number) => {
|
||||
/**
|
||||
* List snapshots for a given repository
|
||||
* If backupId is provided, filter snapshots by that backup ID (tag)
|
||||
* @param name Repository name
|
||||
* @param backupId Optional backup ID to filter snapshots for a specific backup schedule
|
||||
*
|
||||
* @returns List of snapshots
|
||||
*/
|
||||
const listSnapshots = async (name: string, backupId?: string) => {
|
||||
const repository = await db.query.repositoriesTable.findFirst({
|
||||
where: eq(repositoriesTable.name, name),
|
||||
});
|
||||
@@ -115,20 +122,12 @@ const listSnapshots = async (name: string, volumeId?: number) => {
|
||||
throw new NotFoundError("Repository not found");
|
||||
}
|
||||
|
||||
let snapshots = await restic.snapshots(repository.config);
|
||||
let snapshots = [];
|
||||
|
||||
if (volumeId) {
|
||||
const volume = await db.query.volumesTable.findFirst({
|
||||
where: eq(volumesTable.id, volumeId),
|
||||
});
|
||||
|
||||
if (!volume) {
|
||||
throw new NotFoundError("Volume not found");
|
||||
}
|
||||
|
||||
snapshots = snapshots.filter((snapshot) => {
|
||||
return snapshot.paths.some((path) => path.includes(getVolumePath(volume.name)));
|
||||
});
|
||||
if (backupId) {
|
||||
snapshots = await restic.snapshots(repository.config, { tags: [backupId.toString()] });
|
||||
} else {
|
||||
snapshots = await restic.snapshots(repository.config);
|
||||
}
|
||||
|
||||
return snapshots;
|
||||
|
||||
@@ -114,13 +114,19 @@ const init = async (config: RepositoryConfig) => {
|
||||
const backup = async (
|
||||
config: RepositoryConfig,
|
||||
source: string,
|
||||
options?: { exclude?: string[]; include?: string[] },
|
||||
options?: { exclude?: string[]; include?: string[]; tags?: string[] },
|
||||
) => {
|
||||
const repoUrl = buildRepoUrl(config);
|
||||
const env = await buildEnv(config);
|
||||
|
||||
const args: string[] = ["--repo", repoUrl, "backup", "--one-file-system"];
|
||||
|
||||
if (options?.tags && options.tags.length > 0) {
|
||||
for (const tag of options.tags) {
|
||||
args.push("--tag", tag);
|
||||
}
|
||||
}
|
||||
|
||||
let includeFile: string | null = null;
|
||||
if (options?.include && options.include.length > 0) {
|
||||
const tmp = await fs.mkdtemp("restic-include");
|
||||
@@ -263,11 +269,23 @@ const restore = async (
|
||||
return result;
|
||||
};
|
||||
|
||||
const snapshots = async (config: RepositoryConfig) => {
|
||||
const snapshots = async (config: RepositoryConfig, options: { tags?: string[] } = {}) => {
|
||||
const { tags } = options;
|
||||
|
||||
const repoUrl = buildRepoUrl(config);
|
||||
const env = await buildEnv(config);
|
||||
|
||||
const res = await $`restic --repo ${repoUrl} snapshots --json`.env(env).nothrow();
|
||||
const args = ["--repo", repoUrl, "snapshots"];
|
||||
|
||||
if (tags && tags.length > 0) {
|
||||
for (const tag of tags) {
|
||||
args.push("--tag", tag);
|
||||
}
|
||||
}
|
||||
|
||||
args.push("--json");
|
||||
|
||||
const res = await $`restic ${args}`.env(env).nothrow();
|
||||
|
||||
if (res.exitCode !== 0) {
|
||||
logger.error(`Restic snapshots retrieval failed: ${res.stderr}`);
|
||||
@@ -284,11 +302,11 @@ const snapshots = async (config: RepositoryConfig) => {
|
||||
return result;
|
||||
};
|
||||
|
||||
const forget = async (config: RepositoryConfig, options: RetentionPolicy) => {
|
||||
const forget = async (config: RepositoryConfig, options: RetentionPolicy, extra: { tag: string }) => {
|
||||
const repoUrl = buildRepoUrl(config);
|
||||
const env = await buildEnv(config);
|
||||
|
||||
const args: string[] = ["--repo", repoUrl, "forget"];
|
||||
const args: string[] = ["--repo", repoUrl, "forget", "--group-by", "tags", "--tag", extra.tag];
|
||||
|
||||
if (options.keepLast) {
|
||||
args.push("--keep-last", String(options.keepLast));
|
||||
@@ -322,7 +340,6 @@ const forget = async (config: RepositoryConfig, options: RetentionPolicy) => {
|
||||
throw new Error(`Restic forget failed: ${res.stderr}`);
|
||||
}
|
||||
|
||||
logger.info("Restic forget completed successfully");
|
||||
return { success: true };
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user