Press n or j to go to the next uncovered block, b, p or k for the previous block.
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 | 1x 3x 3x 3x 1x 1x 2x 2x 3x 3x 2x 2x 1x 1x 1x 3x 3x 3x 1x 3x 3x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 3x 1x 1x 1x 1x | import { access, constants, mkdir, readFile, rename, stat, unlink, writeFile } from "node:fs/promises";
import path from "node:path";
import { AppError } from "@ontrack/backend-common";
import type { GpxContent, GpxContentStore } from "./gpx-content-store.js";
function resolveSafePath(rootAbsolute: string, fileName: string): string {
const base = path.basename(fileName.trim());
if (base.length === 0 || base !== fileName.trim()) {
throw new AppError(400, "INVALID_FILE_NAME", "File name must be a single path segment");
}
const root = path.resolve(rootAbsolute);
const full = path.resolve(root, base);
const rootWithSep = root.endsWith(path.sep) ? root : root + path.sep;
if (full !== root && !full.startsWith(rootWithSep)) {
throw new AppError(400, "INVALID_FILE_NAME", "Path escapes content root");
}
return full;
}
export class LocalDirectoryGpxStore implements GpxContentStore {
constructor(private readonly rootAbsolute: string) {}
async getByFileName(fileName: string): Promise<GpxContent> {
const fullPath = resolveSafePath(this.rootAbsolute, fileName);
try {
await access(fullPath, constants.R_OK);
const st = await stat(fullPath);
if (!st.isFile()) {
throw new AppError(404, "TRACK_FILE_NOT_FOUND", "GPX object is not a file");
}
} catch (err) {
if (err instanceof AppError) {
throw err;
}
const code = (err as NodeJS.ErrnoException).code;
if (code === "ENOENT" || code === "ENOTDIR") {
throw new AppError(404, "TRACK_FILE_NOT_FOUND", "GPX file not found in storage");
}
throw err;
}
const body = await readFile(fullPath);
return {
body,
contentType: "application/gpx+xml; charset=utf-8",
};
}
async saveNew(fileName: string, body: Buffer): Promise<void> {
const fullPath = resolveSafePath(this.rootAbsolute, fileName);
try {
await writeFile(fullPath, body, { flag: "wx", mode: 0o644 });
} catch (err) {
const code = (err as NodeJS.ErrnoException).code;
if (code === "EEXIST") {
throw new AppError(409, "FILE_NAME_TAKEN", "GPX file already exists in storage");
}
throw err;
}
}
async deleteByFileName(fileName: string): Promise<void> {
const fullPath = resolveSafePath(this.rootAbsolute, fileName);
await unlink(fullPath).catch((err: NodeJS.ErrnoException) => {
if (err.code !== "ENOENT") {
throw err;
}
});
}
async archive(fileName: string): Promise<void> {
const fullPath = resolveSafePath(this.rootAbsolute, fileName);
const trashDir = path.join(this.rootAbsolute, ".trash");
const dest = path.join(trashDir, path.basename(fullPath));
try {
await mkdir(trashDir, { recursive: true });
await rename(fullPath, dest);
} catch (err) {
if ((err as NodeJS.ErrnoException).code === "ENOENT") {
// Исходника нет — перенос считаем выполненным (идемпотентность).
return;
}
throw err;
}
}
}
|