parent
429b69ec92
commit
1b5ae2b3cc
5 changed files with 59 additions and 39 deletions
|
|
@ -1,8 +1,8 @@
|
|||
import { useEffect, useState } from "react";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { ByteSize } from "~/client/components/bytes-size";
|
||||
import { Card } from "~/client/components/ui/card";
|
||||
import { Progress } from "~/client/components/ui/progress";
|
||||
import { useServerEvents } from "~/client/hooks/use-server-events";
|
||||
import { getBackupProgressOptions } from "~/client/api-client/@tanstack/react-query.gen";
|
||||
import type { GetBackupProgressResponse } from "~/client/api-client/types.gen";
|
||||
import { formatDuration } from "~/utils/utils";
|
||||
import { formatBytes } from "~/utils/format-bytes";
|
||||
|
|
@ -13,34 +13,11 @@ type Props = {
|
|||
};
|
||||
|
||||
export const BackupProgressCard = ({ scheduleShortId, initialProgress }: Props) => {
|
||||
const { addEventListener } = useServerEvents();
|
||||
const [progress, setProgress] = useState<GetBackupProgressResponse>(initialProgress ?? null);
|
||||
|
||||
useEffect(() => {
|
||||
const abortController = new AbortController();
|
||||
|
||||
addEventListener(
|
||||
"backup:progress",
|
||||
(progressData) => {
|
||||
if (progressData.scheduleId === scheduleShortId) {
|
||||
setProgress(progressData);
|
||||
}
|
||||
},
|
||||
{ signal: abortController.signal },
|
||||
);
|
||||
|
||||
addEventListener(
|
||||
"backup:completed",
|
||||
(completedData) => {
|
||||
if (completedData.scheduleId === scheduleShortId) {
|
||||
setProgress(null);
|
||||
}
|
||||
},
|
||||
{ signal: abortController.signal },
|
||||
);
|
||||
|
||||
return () => abortController.abort();
|
||||
}, [addEventListener, scheduleShortId]);
|
||||
const { data: progress } = useQuery({
|
||||
...getBackupProgressOptions({ path: { shortId: scheduleShortId } }),
|
||||
initialData: initialProgress,
|
||||
refetchInterval: 1000,
|
||||
});
|
||||
|
||||
const percentDone = progress ? Math.round(progress.percent_done * 100) : 0;
|
||||
const currentFile = progress?.current_files?.[0] || "";
|
||||
|
|
|
|||
|
|
@ -76,6 +76,7 @@ export function ScheduleDetailsPage(props: Props) {
|
|||
|
||||
const { data: schedule } = useSuspenseQuery({
|
||||
...getBackupScheduleOptions({ path: { shortId: scheduleId } }),
|
||||
refetchInterval: 1000,
|
||||
});
|
||||
|
||||
const {
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import { createTestSession, getAuthHeaders } from "~/test/helpers/auth";
|
|||
import { createTestVolume } from "~/test/helpers/volume";
|
||||
import { createTestRepository } from "~/test/helpers/repository";
|
||||
import { createTestBackupSchedule } from "~/test/helpers/backup";
|
||||
import { cache, cacheKeys } from "~/server/utils/cache";
|
||||
|
||||
const app = createApp();
|
||||
|
||||
|
|
@ -81,6 +82,47 @@ describe("backups security", () => {
|
|||
});
|
||||
|
||||
describe("input validation", () => {
|
||||
test("should return cached progress for a running backup", async () => {
|
||||
const { headers, organizationId } = await createTestSession();
|
||||
const volume = await createTestVolume({ organizationId });
|
||||
const repository = await createTestRepository({ organizationId });
|
||||
const schedule = await createTestBackupSchedule({
|
||||
organizationId,
|
||||
volumeId: volume.id,
|
||||
repositoryId: repository.id,
|
||||
});
|
||||
|
||||
cache.set(cacheKeys.backup.progress(schedule.id), {
|
||||
scheduleId: schedule.shortId,
|
||||
volumeName: volume.name,
|
||||
repositoryName: repository.name,
|
||||
message_type: "status",
|
||||
seconds_elapsed: 12,
|
||||
seconds_remaining: 24,
|
||||
percent_done: 0.5,
|
||||
total_files: 100,
|
||||
files_done: 50,
|
||||
total_bytes: 1024,
|
||||
bytes_done: 512,
|
||||
current_files: ["/mnt/data/file.txt"],
|
||||
});
|
||||
|
||||
const res = await app.request(`/api/v1/backups/${schedule.shortId}/progress`, {
|
||||
headers,
|
||||
});
|
||||
|
||||
expect(res.status).toBe(200);
|
||||
const body = await res.json();
|
||||
expect(body).toMatchObject({
|
||||
scheduleId: schedule.shortId,
|
||||
percent_done: 0.5,
|
||||
files_done: 50,
|
||||
current_files: ["/mnt/data/file.txt"],
|
||||
});
|
||||
|
||||
cache.del(cacheKeys.backup.progress(schedule.id));
|
||||
});
|
||||
|
||||
test("should return a schedule when queried by short id", async () => {
|
||||
const { headers, organizationId } = await createTestSession();
|
||||
const volume = await createTestVolume({ organizationId });
|
||||
|
|
|
|||
|
|
@ -16,10 +16,9 @@ import type { ResticBackupOutputDto } from "@zerobyte/core/restic";
|
|||
import type { BackupProgressEventDto } from "~/schemas/events-dto";
|
||||
|
||||
const runningBackups = new Map<number, AbortController>();
|
||||
const progressCache = new Map<number, BackupProgressEventDto>();
|
||||
|
||||
export const getBackupProgress = (scheduleId: number): BackupProgressEventDto | undefined =>
|
||||
progressCache.get(scheduleId);
|
||||
cache.get<BackupProgressEventDto>(cacheKeys.backup.progress(scheduleId));
|
||||
|
||||
interface BackupContext {
|
||||
schedule: BackupSchedule;
|
||||
|
|
@ -123,18 +122,16 @@ const runBackupOperation = async (ctx: BackupContext, signal: AbortSignal) => {
|
|||
compressionMode: ctx.repository.compressionMode ?? "auto",
|
||||
organizationId: ctx.organizationId,
|
||||
onProgress: (progress) => {
|
||||
progressCache.set(ctx.schedule.id, {
|
||||
const progressEvent = {
|
||||
scheduleId: ctx.schedule.shortId,
|
||||
volumeName: ctx.volume.name,
|
||||
repositoryName: ctx.repository.name,
|
||||
...progress,
|
||||
});
|
||||
};
|
||||
cache.set(cacheKeys.backup.progress(ctx.schedule.id), progressEvent, 60 * 60);
|
||||
serverEvents.emit("backup:progress", {
|
||||
organizationId: ctx.organizationId,
|
||||
scheduleId: ctx.schedule.shortId,
|
||||
volumeName: ctx.volume.name,
|
||||
repositoryName: ctx.repository.name,
|
||||
...progress,
|
||||
...progressEvent,
|
||||
});
|
||||
},
|
||||
});
|
||||
|
|
@ -297,7 +294,7 @@ const executeBackup = async (scheduleId: number, manual = false): Promise<void>
|
|||
await handleBackupFailure(scheduleId, ctx.organizationId, error, ctx);
|
||||
} finally {
|
||||
runningBackups.delete(scheduleId);
|
||||
progressCache.delete(scheduleId);
|
||||
cache.del(cacheKeys.backup.progress(scheduleId));
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -106,6 +106,9 @@ export const createCache = (options: CacheOptions = {}) => {
|
|||
};
|
||||
|
||||
export const cacheKeys = {
|
||||
backup: {
|
||||
progress: (scheduleId: number) => `backup:${scheduleId}:progress`,
|
||||
},
|
||||
repository: {
|
||||
all: (repositoryId: string) => `repo:${repositoryId}:`,
|
||||
snapshots: (repositoryId: string, backupId = "all") => `repo:${repositoryId}:snapshots:${backupId}`,
|
||||
|
|
|
|||
Loading…
Reference in a new issue