zerobyte/app/server/utils/restic/commands/ls.ts
Nico 4a812a884b
refactor(restic): split each command into its own file (#584)
refactor(restic): split each command into its own file

refactor: add missing await before promise expects

<!-- This is an auto-generated comment: release notes by coderabbit.ai -->

## Summary by CodeRabbit

## Release Notes

* **Tests**
  * Improved async assertion handling across test suites for enhanced test reliability
  * Expanded test coverage for backup, restore, and other core operations

* **Refactor**
  * Reorganized internal utility structure for improved code maintainability

* **Chores**
  * Updated linting configuration and TypeScript dependencies

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-02-26 19:51:24 +01:00

128 lines
2.9 KiB
TypeScript

import { type } from "arktype";
import type { RepositoryConfig } from "~/schemas/restic";
import { ResticError } from "~/server/utils/errors";
import { logger } from "~/server/utils/logger";
import { safeSpawn } from "~/server/utils/spawn";
import { addCommonArgs } from "../helpers/add-common-args";
import { buildEnv } from "../helpers/build-env";
import { buildRepoUrl } from "../helpers/build-repo-url";
import { cleanupTemporaryKeys } from "../helpers/cleanup-temporary-keys";
const lsNodeSchema = type({
name: "string",
type: "string",
path: "string",
uid: "number?",
gid: "number?",
size: "number?",
mode: "number?",
mtime: "string?",
atime: "string?",
ctime: "string?",
struct_type: "'node'",
});
const lsSnapshotInfoSchema = type({
time: "string",
parent: "string?",
tree: "string",
paths: "string[]",
hostname: "string",
username: "string?",
id: "string",
short_id: "string",
struct_type: "'snapshot'",
message_type: "'snapshot'",
});
export const ls = async (
config: RepositoryConfig,
snapshotId: string,
organizationId: string,
path?: string,
options?: { offset?: number; limit?: number },
) => {
const repoUrl = buildRepoUrl(config);
const env = await buildEnv(config, organizationId);
const args: string[] = ["--repo", repoUrl, "ls", snapshotId, "--long"];
if (path) {
args.push(path);
}
addCommonArgs(args, env, config);
let snapshot: typeof lsSnapshotInfoSchema.infer | null = null;
const nodes: Array<typeof lsNodeSchema.infer> = [];
let totalNodes = 0;
let isFirstLine = true;
let hasMore = false;
const offset = Math.max(options?.offset ?? 0, 0);
const limit = Math.min(Math.max(options?.limit ?? 500, 1), 500);
const res = await safeSpawn({
command: "restic",
args,
env,
onStdout: (line) => {
const trimmedLine = line.trim();
if (!trimmedLine) {
return;
}
try {
const data = JSON.parse(trimmedLine);
if (isFirstLine) {
isFirstLine = false;
const snapshotValidation = lsSnapshotInfoSchema(data);
if (!(snapshotValidation instanceof type.errors)) {
snapshot = snapshotValidation;
}
return;
}
const nodeValidation = lsNodeSchema(data);
if (nodeValidation instanceof type.errors) {
logger.warn(`Skipping invalid node: ${nodeValidation.summary}`);
return;
}
if (totalNodes >= offset && totalNodes < offset + limit) {
nodes.push(nodeValidation);
}
totalNodes++;
if (totalNodes >= offset + limit + 1) {
hasMore = true;
}
} catch {
// Ignore JSON parse errors for non-JSON lines
}
},
});
await cleanupTemporaryKeys(env);
if (res.exitCode !== 0) {
logger.error(`Restic ls failed: ${res.error}`);
throw new ResticError(res.exitCode, res.error);
}
if (totalNodes > offset + limit) {
hasMore = true;
}
return {
snapshot: snapshot as typeof lsSnapshotInfoSchema.infer | null,
nodes,
pagination: {
offset,
limit,
total: totalNodes,
hasMore,
},
};
};