zerobyte/app/server/modules/repositories/helpers/dump.ts
Nicolas Meienberger 49abcc5a12 feat: export snapshot as tar file
chore(mutext): prevent double lock release
2026-02-20 12:01:01 +01:00

73 lines
2.2 KiB
TypeScript

import { BadRequestError } from "http-errors-enhanced";
import path from "node:path";
import { findCommonAncestor } from "~/utils/common-ancestor";
const normalizeAbsolutePath = (value?: string): string => {
const trimmed = value?.trim();
if (!trimmed) return "/";
const normalizedInput = decodeURIComponent(trimmed).replace(/\\+/g, "/");
const withLeadingSlash = normalizedInput.startsWith("/") ? normalizedInput : `/${normalizedInput}`;
const normalized = path.posix.normalize(withLeadingSlash);
if (!normalized || normalized === "." || normalized.startsWith("..")) {
return "/";
}
const withoutTrailingSlash = normalized.replace(/\/+$/, "");
if (!withoutTrailingSlash) {
return "/";
}
const withSingleLeadingSlash = withoutTrailingSlash.startsWith("/")
? `/${withoutTrailingSlash.replace(/^\/+/, "")}`
: `/${withoutTrailingSlash}`;
return withSingleLeadingSlash || "/";
};
const sanitizeFilenamePart = (value: string): string => {
const sanitized = value.replace(/[^a-zA-Z0-9._-]/g, "_").replace(/^_+|_+$/g, "");
return sanitized || "snapshot";
};
export const prepareSnapshotDump = (params: {
snapshotId: string;
snapshotPaths: string[];
requestedPath?: string;
}) => {
const { snapshotId, snapshotPaths, requestedPath } = params;
const archiveFilename = `snapshot-${sanitizeFilenamePart(snapshotId)}.tar`;
const normalizedRequestedPath = normalizeAbsolutePath(requestedPath);
const basePath = normalizeAbsolutePath(findCommonAncestor(snapshotPaths));
if (basePath === "/") {
return {
snapshotRef: snapshotId,
path: normalizedRequestedPath,
filename: archiveFilename,
};
}
if (normalizedRequestedPath === "/" || normalizedRequestedPath === basePath) {
return {
snapshotRef: `${snapshotId}:${basePath}`,
path: "/",
filename: archiveFilename,
};
}
const relativeFromBase = path.posix.relative(basePath, normalizedRequestedPath);
if (relativeFromBase === ".." || relativeFromBase.startsWith("../")) {
throw new BadRequestError("Requested path is outside the snapshot base path");
}
const relativePath = relativeFromBase ? `/${relativeFromBase}` : "/";
return {
snapshotRef: `${snapshotId}:${basePath}`,
path: relativePath,
filename: archiveFilename,
};
};