mirror of
https://github.com/nicotsx/ironmount.git
synced 2025-12-10 12:10:51 +01:00
feat(repositories): list snapshots api
This commit is contained in:
@@ -8,6 +8,8 @@ import {
|
||||
type GetRepositoryResponseDto,
|
||||
type ListRepositoriesResponseDto,
|
||||
listRepositoriesDto,
|
||||
listSnapshotsDto,
|
||||
type ListSnapshotsResponseDto,
|
||||
} from "./repositories.dto";
|
||||
import { repositoriesService } from "./repositories.service";
|
||||
|
||||
@@ -52,4 +54,26 @@ export const repositoriesController = new Hono()
|
||||
await repositoriesService.deleteRepository(name);
|
||||
|
||||
return c.json({ message: "Repository deleted" }, 200);
|
||||
})
|
||||
.get("/:name/snapshots", listSnapshotsDto, async (c) => {
|
||||
const { name } = c.req.param();
|
||||
const res = await repositoriesService.listSnapshots(name);
|
||||
|
||||
const snapshots = res.map((snapshot) => {
|
||||
const { summary } = snapshot;
|
||||
const { backup_start, backup_end } = summary;
|
||||
const duration = new Date(backup_end).getTime() - new Date(backup_start).getTime();
|
||||
|
||||
return {
|
||||
short_id: snapshot.short_id,
|
||||
duration,
|
||||
paths: snapshot.paths,
|
||||
size: summary.total_bytes_processed,
|
||||
time: new Date(snapshot.time).getTime(),
|
||||
};
|
||||
});
|
||||
|
||||
const response = { snapshots } satisfies ListSnapshotsResponseDto;
|
||||
|
||||
return c.json(response, 200);
|
||||
});
|
||||
|
||||
@@ -127,3 +127,36 @@ export const deleteRepositoryDto = describeRoute({
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* List snapshots in a repository
|
||||
*/
|
||||
export const snapshotSchema = type({
|
||||
short_id: "string",
|
||||
time: "number",
|
||||
paths: "string[]",
|
||||
size: "number",
|
||||
duration: "number",
|
||||
});
|
||||
|
||||
const listSnapshotsResponse = type({
|
||||
snapshots: snapshotSchema.array(),
|
||||
});
|
||||
|
||||
export type ListSnapshotsResponseDto = typeof listSnapshotsResponse.infer;
|
||||
|
||||
export const listSnapshotsDto = describeRoute({
|
||||
description: "List all snapshots in a repository",
|
||||
tags: ["Repositories"],
|
||||
operationId: "listSnapshots",
|
||||
responses: {
|
||||
200: {
|
||||
description: "List of snapshots",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: resolver(listSnapshotsResponse),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@@ -105,9 +105,23 @@ const deleteRepository = async (name: string) => {
|
||||
await db.delete(repositoriesTable).where(eq(repositoriesTable.name, name));
|
||||
};
|
||||
|
||||
const listSnapshots = async (name: string) => {
|
||||
const repository = await db.query.repositoriesTable.findFirst({
|
||||
where: eq(repositoriesTable.name, name),
|
||||
});
|
||||
|
||||
if (!repository) {
|
||||
throw new NotFoundError("Repository not found");
|
||||
}
|
||||
|
||||
const snapshots = await restic.snapshots(repository.config);
|
||||
return snapshots;
|
||||
};
|
||||
|
||||
export const repositoriesService = {
|
||||
listRepositories,
|
||||
createRepository,
|
||||
getRepository,
|
||||
deleteRepository,
|
||||
listSnapshots,
|
||||
};
|
||||
|
||||
@@ -25,6 +25,35 @@ const backupOutputSchema = type({
|
||||
snapshot_id: "string",
|
||||
});
|
||||
|
||||
const snapshotInfoSchema = type({
|
||||
gid: "number",
|
||||
hostname: "string",
|
||||
id: "string",
|
||||
parent: "string?",
|
||||
paths: "string[]",
|
||||
program_version: "string",
|
||||
short_id: "string",
|
||||
time: "string",
|
||||
uid: "number",
|
||||
username: "string",
|
||||
summary: type({
|
||||
backup_end: "string",
|
||||
backup_start: "string",
|
||||
data_added: "number",
|
||||
data_added_packed: "number",
|
||||
data_blobs: "number",
|
||||
dirs_changed: "number",
|
||||
dirs_new: "number",
|
||||
dirs_unmodified: "number",
|
||||
files_changed: "number",
|
||||
files_new: "number",
|
||||
files_unmodified: "number",
|
||||
total_bytes_processed: "number",
|
||||
total_files_processed: "number",
|
||||
tree_blobs: "number",
|
||||
}),
|
||||
});
|
||||
|
||||
const ensurePassfile = async () => {
|
||||
await fs.mkdir(path.dirname(RESTIC_PASS_FILE), { recursive: true });
|
||||
|
||||
@@ -115,9 +144,31 @@ const restore = async (config: RepositoryConfig, snapshotId: string, target: str
|
||||
logger.info(`Restic restore completed for snapshot ${snapshotId} to target ${target}`);
|
||||
};
|
||||
|
||||
const snapshots = async (config: RepositoryConfig) => {
|
||||
const repoUrl = buildRepoUrl(config);
|
||||
const env = await buildEnv(config);
|
||||
|
||||
const res = await $`restic --repo ${repoUrl} snapshots --json`.env(env).nothrow();
|
||||
|
||||
if (res.exitCode !== 0) {
|
||||
logger.error(`Restic snapshots retrieval failed: ${res.stderr}`);
|
||||
throw new Error(`Restic snapshots retrieval failed: ${res.stderr}`);
|
||||
}
|
||||
|
||||
const result = snapshotInfoSchema.array()(res.json());
|
||||
|
||||
if (result instanceof type.errors) {
|
||||
logger.error(`Restic snapshots output validation failed: ${result}`);
|
||||
throw new Error(`Restic snapshots output validation failed: ${result}`);
|
||||
}
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
export const restic = {
|
||||
ensurePassfile,
|
||||
init,
|
||||
backup,
|
||||
restore,
|
||||
snapshots,
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user