Files
bookorbit/server/test/mergerfs-inode.e2e-spec.ts
neon 95a7b94184 fix(scanner): support bigint inodes to prevent folder mis-merges
Store file inodes as numeric rather than bigint on PostgreSQL, and read/process them as BigInt in Node.js. This preserves exact 64-bit inodes from filesystems like MergerFS (hybrid-hash) and avoids folder mis-merging or disabled rename detection.

Closes #618
2026-07-11 20:18:10 +00:00

61 lines
2.1 KiB
TypeScript

import { mkdir, stat, writeFile } from 'fs/promises';
import { join } from 'path';
import { eq } from 'drizzle-orm';
import { bookFiles, books } from '../src/db/schema';
import {
closeScannerE2EContext,
createScannerE2EContext,
loadLibraryBookState,
seedLibrary,
triggerAndWaitForLibraryScan,
type ScannerE2EContext,
} from './e2e/scanner/scanner-harness';
const mergerFsRoot = process.env.MERGERFS_TEST_ROOT;
describe.skipIf(!mergerFsRoot)('MergerFS inode scanning', () => {
let ctx: ScannerE2EContext;
let libraryId: number;
beforeAll(async () => {
await mkdir(join(mergerFsRoot!, 'ExistingBook'), { recursive: true });
await writeFile(join(mergerFsRoot!, 'ExistingBook', 'book.epub'), 'shared-content');
ctx = await createScannerE2EContext();
({ libraryId } = await seedLibrary(ctx.db, {
rootPath: mergerFsRoot!,
mode: 'book_per_folder',
name: 'mergerfs-inode-e2e',
}));
});
afterAll(async () => {
if (ctx) await closeScannerE2EContext(ctx);
});
it('persists exact oversized inodes and keeps identical files in distinct folders', async () => {
const existingPath = join(mergerFsRoot!, 'ExistingBook', 'book.epub');
const existingStat = await stat(existingPath, { bigint: true });
const maxSafeIno = BigInt(Number.MAX_SAFE_INTEGER);
expect(existingStat.ino < -maxSafeIno || existingStat.ino > maxSafeIno).toBe(true);
await triggerAndWaitForLibraryScan(ctx, libraryId);
const [storedExisting] = await ctx.db
.select({ ino: bookFiles.ino })
.from(bookFiles)
.innerJoin(books, eq(books.id, bookFiles.bookId))
.where(eq(books.libraryId, libraryId));
expect(storedExisting.ino).toBe(existingStat.ino);
await mkdir(join(mergerFsRoot!, 'NewBook'), { recursive: true });
await writeFile(join(mergerFsRoot!, 'NewBook', 'book.epub'), 'shared-content');
await triggerAndWaitForLibraryScan(ctx, libraryId);
const state = await loadLibraryBookState(ctx.db, libraryId);
expect(state).toHaveLength(2);
expect(state.map((book) => book.folderPath)).toEqual([join(mergerFsRoot!, 'ExistingBook'), join(mergerFsRoot!, 'NewBook')]);
});
});