Validation des chemins externalisée (library-path) avec codes d'erreur explicites, support d'alias LIBRARY_PATH_ALIASES pour traduire un chemin hôte vers le montage conteneur, rejet des doublons de chemin entre bibliothèques, documentation README/.env.example et docker-compose paramétrable via READABOOK_LIBRARY_HOST_PATH. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
73 lines
2.5 KiB
TypeScript
73 lines
2.5 KiB
TypeScript
import { accessSync, constants, realpathSync, statSync } from "node:fs";
|
|
import { relative, resolve, sep } from "node:path";
|
|
|
|
export type LibraryPathAlias = {
|
|
from: string;
|
|
to: string;
|
|
};
|
|
|
|
export type LibraryPathErrorCode = "LIBRARY_PATH_NOT_FOUND" | "LIBRARY_PATH_NOT_DIRECTORY" | "LIBRARY_PATH_NOT_READABLE";
|
|
|
|
export class LibraryPathValidationError extends Error {
|
|
constructor(
|
|
public readonly code: LibraryPathErrorCode,
|
|
public readonly path: string
|
|
) {
|
|
super(messageForCode(code));
|
|
}
|
|
}
|
|
|
|
export function resolveLibraryPath(input: string, aliases: LibraryPathAlias[] = []): string {
|
|
const candidates = candidatePaths(input, aliases);
|
|
let firstError: LibraryPathValidationError | null = null;
|
|
|
|
for (const candidate of candidates) {
|
|
try {
|
|
const stats = statSync(candidate);
|
|
if (!stats.isDirectory()) {
|
|
throw new LibraryPathValidationError("LIBRARY_PATH_NOT_DIRECTORY", candidate);
|
|
}
|
|
accessSync(candidate, constants.R_OK | constants.X_OK);
|
|
return realpathSync(candidate);
|
|
} catch (error) {
|
|
firstError ??= normalizePathError(error, candidate);
|
|
}
|
|
}
|
|
|
|
throw firstError ?? new LibraryPathValidationError("LIBRARY_PATH_NOT_FOUND", resolve(input));
|
|
}
|
|
|
|
function candidatePaths(input: string, aliases: LibraryPathAlias[]): string[] {
|
|
const resolved = resolve(input);
|
|
const candidates = [resolved];
|
|
|
|
for (const alias of aliases) {
|
|
const from = resolve(alias.from);
|
|
const to = resolve(alias.to);
|
|
const remainder = relative(from, resolved);
|
|
if (remainder === "" || (!remainder.startsWith("..") && remainder !== ".." && !remainder.startsWith(`..${sep}`))) {
|
|
candidates.push(resolve(to, remainder));
|
|
}
|
|
}
|
|
|
|
return [...new Set(candidates)];
|
|
}
|
|
|
|
function normalizePathError(error: unknown, path: string): LibraryPathValidationError {
|
|
if (error instanceof LibraryPathValidationError) return error;
|
|
const code = typeof error === "object" && error && "code" in error ? String(error.code) : "";
|
|
if (code === "ENOENT" || code === "ENOTDIR") {
|
|
return new LibraryPathValidationError("LIBRARY_PATH_NOT_FOUND", path);
|
|
}
|
|
if (code === "EACCES" || code === "EPERM") {
|
|
return new LibraryPathValidationError("LIBRARY_PATH_NOT_READABLE", path);
|
|
}
|
|
return new LibraryPathValidationError("LIBRARY_PATH_NOT_READABLE", path);
|
|
}
|
|
|
|
function messageForCode(code: LibraryPathErrorCode): string {
|
|
if (code === "LIBRARY_PATH_NOT_FOUND") return "Library path does not exist";
|
|
if (code === "LIBRARY_PATH_NOT_DIRECTORY") return "Library path must be a directory";
|
|
return "Library path is not readable";
|
|
}
|