fix(api): chemins de bibliothèques — validation dédiée, alias et unicité

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>
This commit is contained in:
Git Agent
2026-08-23 16:23:57 +02:00
parent 0fa2f99289
commit 7b72cc0d83
8 changed files with 194 additions and 16 deletions

View File

@ -0,0 +1,49 @@
import { closeSync, existsSync, mkdtempSync, openSync, rmSync } from "node:fs";
import { tmpdir } from "node:os";
import { join, resolve } from "node:path";
import { afterEach, describe, expect, it } from "vitest";
import { LibraryPathValidationError, resolveLibraryPath } from "./library-path.js";
const tempDirs: string[] = [];
const realBooksPath = "/home/anthony/Documents/Projects/ReadaBook/Books";
afterEach(() => {
for (const dir of tempDirs.splice(0)) {
rmSync(dir, { recursive: true, force: true });
}
});
describe("library path resolution", () => {
it("resolves a readable directory", () => {
const dir = mkdtempSync(join(tmpdir(), "readabook-library-"));
tempDirs.push(dir);
expect(resolveLibraryPath(dir)).toBe(resolve(dir));
});
it("maps a host path alias to the mounted container path", () => {
const hostRoot = "/host/project/Books";
const mountedRoot = mkdtempSync(join(tmpdir(), "readabook-mounted-books-"));
tempDirs.push(mountedRoot);
expect(resolveLibraryPath(hostRoot, [{ from: hostRoot, to: mountedRoot }])).toBe(resolve(mountedRoot));
});
it("rejects regular files with a stable code", () => {
const dir = mkdtempSync(join(tmpdir(), "readabook-library-"));
tempDirs.push(dir);
const file = join(dir, "book.epub");
closeSync(openSync(file, "w"));
expect(() => resolveLibraryPath(file)).toThrowError(LibraryPathValidationError);
try {
resolveLibraryPath(file);
} catch (error) {
expect(error).toMatchObject({ code: "LIBRARY_PATH_NOT_DIRECTORY" });
}
});
it.runIf(existsSync(realBooksPath))("accepts the real Books corpus path used by QA", () => {
expect(resolveLibraryPath(realBooksPath)).toBe(resolve(realBooksPath));
});
});