refactor: use backend cache in restic operations (#295)

This commit is contained in:
Nico
2026-01-04 16:37:42 +01:00
committed by GitHub
parent 79d5f47b19
commit a2ee223ee7
3 changed files with 64 additions and 11 deletions

View File

@@ -6,6 +6,7 @@ import { db } from "../../db/db";
import { backupSchedulesTable, backupScheduleMirrorsTable, repositoriesTable, volumesTable } from "../../db/schema";
import { restic } from "../../utils/restic";
import { logger } from "../../utils/logger";
import { cache } from "../../utils/cache";
import { getVolumePath } from "../volumes/helpers";
import type { CreateBackupScheduleBody, UpdateBackupScheduleBody, UpdateScheduleMirrorsBody } from "./backups.dto";
import { toMessage } from "../../utils/errors";
@@ -329,6 +330,8 @@ const executeBackup = async (scheduleId: number, manual = false) => {
const finalStatus = exitCode === 0 ? "success" : "warning";
cache.delByPrefix(`snapshots:${repository.id}:`);
const nextBackupAt = calculateNextRun(schedule.cronExpression);
await db
.update(backupSchedulesTable)
@@ -488,6 +491,7 @@ const runForget = async (scheduleId: number, repositoryId?: string) => {
const releaseLock = await repoMutex.acquireExclusive(repository.id, `forget:${scheduleId}`);
try {
await restic.forget(repository.config, schedule.retentionPolicy, { tag: schedule.shortId });
cache.delByPrefix(`snapshots:${repository.id}:`);
} finally {
releaseLock();
}
@@ -619,6 +623,7 @@ const copyToMirrors = async (
try {
await restic.copy(sourceRepository.config, mirror.repository.config, { tag: schedule.shortId });
cache.delByPrefix(`snapshots:${mirror.repository.id}:`);
} finally {
releaseSource();
releaseMirror();

View File

@@ -7,6 +7,7 @@ import { toMessage } from "../../utils/errors";
import { generateShortId } from "../../utils/id";
import { restic } from "../../utils/restic";
import { cryptoUtils } from "../../utils/crypto";
import { cache } from "../../utils/cache";
import { repoMutex } from "../../core/repository-mutex";
import {
repositoryConfigSchema,
@@ -143,6 +144,9 @@ const deleteRepository = async (id: string) => {
// TODO: Add cleanup logic for the actual restic repository files
await db.delete(repositoriesTable).where(eq(repositoriesTable.id, repository.id));
cache.delByPrefix(`snapshots:${repository.id}:`);
cache.delByPrefix(`ls:${repository.id}:`);
};
/**
@@ -160,6 +164,12 @@ const listSnapshots = async (id: string, backupId?: string) => {
throw new NotFoundError("Repository not found");
}
const cacheKey = `snapshots:${repository.id}:${backupId || "all"}`;
const cached = cache.get<Awaited<ReturnType<typeof restic.snapshots>>>(cacheKey);
if (cached) {
return cached;
}
const releaseLock = await repoMutex.acquireShared(repository.id, "snapshots");
try {
let snapshots = [];
@@ -170,6 +180,8 @@ const listSnapshots = async (id: string, backupId?: string) => {
snapshots = await restic.snapshots(repository.config);
}
cache.set(cacheKey, snapshots);
return snapshots;
} finally {
releaseLock();
@@ -183,6 +195,15 @@ const listSnapshotFiles = async (id: string, snapshotId: string, path?: string)
throw new NotFoundError("Repository not found");
}
const cacheKey = `ls:${repository.id}:${snapshotId}:${path || "root"}`;
const cached = cache.get<Awaited<ReturnType<typeof restic.ls>>>(cacheKey);
if (cached?.snapshot) {
return {
snapshot: cached.snapshot,
files: cached.nodes,
};
}
const releaseLock = await repoMutex.acquireShared(repository.id, `ls:${snapshotId}`);
try {
const result = await restic.ls(repository.config, snapshotId, path);
@@ -191,7 +212,7 @@ const listSnapshotFiles = async (id: string, snapshotId: string, path?: string)
throw new NotFoundError("Snapshot not found or empty");
}
return {
const response = {
snapshot: {
id: result.snapshot.id,
short_id: result.snapshot.short_id,
@@ -201,6 +222,10 @@ const listSnapshotFiles = async (id: string, snapshotId: string, path?: string)
},
files: result.nodes,
};
cache.set(cacheKey, result);
return response;
} finally {
releaseLock();
}
@@ -248,19 +273,26 @@ const getSnapshotDetails = async (id: string, snapshotId: string) => {
throw new NotFoundError("Repository not found");
}
const releaseLock = await repoMutex.acquireShared(repository.id, `snapshot_details:${snapshotId}`);
try {
const snapshots = await restic.snapshots(repository.config);
const snapshot = snapshots.find((snap) => snap.id === snapshotId || snap.short_id === snapshotId);
const cacheKey = `snapshots:${repository.id}:all`;
let snapshots = cache.get<Awaited<ReturnType<typeof restic.snapshots>>>(cacheKey);
if (!snapshot) {
throw new NotFoundError("Snapshot not found");
if (!snapshots) {
const releaseLock = await repoMutex.acquireShared(repository.id, `snapshot_details:${snapshotId}`);
try {
snapshots = await restic.snapshots(repository.config);
cache.set(cacheKey, snapshots);
} finally {
releaseLock();
}
return snapshot;
} finally {
releaseLock();
}
const snapshot = snapshots.find((snap) => snap.id === snapshotId || snap.short_id === snapshotId);
if (!snapshot) {
throw new NotFoundError("Snapshot not found");
}
return snapshot;
};
const checkHealth = async (repositoryId: string) => {
@@ -388,6 +420,8 @@ const deleteSnapshot = async (id: string, snapshotId: string) => {
const releaseLock = await repoMutex.acquireExclusive(repository.id, `delete:${snapshotId}`);
try {
await restic.deleteSnapshot(repository.config, snapshotId);
cache.delByPrefix(`snapshots:${repository.id}:`);
cache.delByPrefix(`ls:${repository.id}:${snapshotId}:`);
} finally {
releaseLock();
}
@@ -403,6 +437,10 @@ const deleteSnapshots = async (id: string, snapshotIds: string[]) => {
const releaseLock = await repoMutex.acquireExclusive(repository.id, `delete:bulk`);
try {
await restic.deleteSnapshots(repository.config, snapshotIds);
cache.delByPrefix(`snapshots:${repository.id}:`);
for (const snapshotId of snapshotIds) {
cache.delByPrefix(`ls:${repository.id}:${snapshotId}:`);
}
} finally {
releaseLock();
}
@@ -422,6 +460,10 @@ const tagSnapshots = async (
const releaseLock = await repoMutex.acquireExclusive(repository.id, `tag:bulk`);
try {
await restic.tagSnapshots(repository.config, snapshotIds, tags);
cache.delByPrefix(`snapshots:${repository.id}:`);
for (const snapshotId of snapshotIds) {
cache.delByPrefix(`ls:${repository.id}:${snapshotId}:`);
}
} finally {
releaseLock();
}

View File

@@ -54,6 +54,11 @@ export const createCache = (options: CacheOptions = {}) => {
stmt.run(key);
};
const delByPrefix = (prefix: string) => {
const stmt = db.prepare("DELETE FROM cache WHERE key LIKE ?");
stmt.run(`${prefix}%`);
};
const getByPrefix = <T>(prefix: string): { key: string; value: T }[] => {
const stmt = db.prepare("SELECT key, value, expiration FROM cache WHERE key LIKE ?");
const rows = stmt.all(`${prefix}%`) as { key: string; value: string; expiration: number }[];
@@ -88,6 +93,7 @@ export const createCache = (options: CacheOptions = {}) => {
set,
get,
del,
delByPrefix,
getByPrefix,
clear,
};