feat(snapshot): backend restore api

This commit is contained in:
Nicolas Meienberger
2025-10-31 21:15:43 +01:00
parent 5846c1ff86
commit c7db88fb56
7 changed files with 251 additions and 7 deletions

View File

@@ -6,15 +6,19 @@ import {
deleteRepositoryDto,
getRepositoryDto,
listRepositoriesDto,
listSnapshotsDto,
listSnapshotsFilters,
listSnapshotFilesDto,
listSnapshotFilesQuery,
listSnapshotsDto,
listSnapshotsFilters,
restoreSnapshotBody,
restoreSnapshotDto,
type CreateRepositoryDto,
type DeleteRepositoryDto,
type GetRepositoryDto,
type ListRepositoriesDto,
type ListSnapshotsDto,
type ListSnapshotFilesDto,
type ListSnapshotsDto,
type RestoreSnapshotDto,
} from "./repositories.dto";
import { repositoriesService } from "./repositories.service";
@@ -86,4 +90,12 @@ export const repositoriesController = new Hono()
return c.json<ListSnapshotFilesDto>(result, 200);
},
);
)
.post("/:name/restore", restoreSnapshotDto, validator("json", restoreSnapshotBody), async (c) => {
const { name } = c.req.param();
const { snapshotId, targetPath, path, include, exclude } = c.req.valid("json");
const result = await repositoriesService.restoreSnapshot(name, snapshotId, targetPath, { path, include, exclude });
return c.json<RestoreSnapshotDto>(result, 200);
});

View File

@@ -213,3 +213,42 @@ export const listSnapshotFilesDto = describeRoute({
},
},
});
/**
* Restore a snapshot
*/
export const restoreSnapshotBody = type({
snapshotId: "string",
targetPath: "string",
path: "string?",
include: "string[]?",
exclude: "string[]?",
});
export type RestoreSnapshotBody = typeof restoreSnapshotBody.infer;
export const restoreSnapshotResponse = type({
success: "boolean",
message: "string",
filesRestored: "number",
filesUpdated: "number",
totalBytes: "number",
});
export type RestoreSnapshotDto = typeof restoreSnapshotResponse.infer;
export const restoreSnapshotDto = describeRoute({
description: "Restore a snapshot to a target path on the filesystem",
tags: ["Repositories"],
operationId: "restoreSnapshot",
responses: {
200: {
description: "Snapshot restored successfully",
content: {
"application/json": {
schema: resolver(restoreSnapshotResponse),
},
},
},
},
});

View File

@@ -161,6 +161,35 @@ const listSnapshotFiles = async (name: string, snapshotId: string, path?: string
};
};
const restoreSnapshot = async (
name: string,
snapshotId: string,
targetPath: string,
options?: {
path?: string;
include?: string[];
exclude?: string[];
},
) => {
const repository = await db.query.repositoriesTable.findFirst({
where: eq(repositoriesTable.name, name),
});
if (!repository) {
throw new NotFoundError("Repository not found");
}
const result = await restic.restore(repository.config, snapshotId, targetPath, options);
return {
success: true,
message: "Snapshot restored successfully",
filesRestored: result.files_restored,
filesUpdated: result.files_updated,
totalBytes: result.total_bytes,
};
};
export const repositoriesService = {
listRepositories,
createRepository,
@@ -168,4 +197,5 @@ export const repositoriesService = {
deleteRepository,
listSnapshots,
listSnapshotFiles,
restoreSnapshot,
};