73 lines
2.2 KiB
TypeScript
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,
|
|
};
|
|
};
|