fix(import): route image pages by source (#2718)

This commit is contained in:
Ziyang Guo
2026-07-16 16:31:50 -07:00
committed by GitHub
parent 7a275bf0b5
commit ec3910afc4
2 changed files with 41 additions and 7 deletions
+18 -7
View File
@@ -1315,6 +1315,8 @@ const NEEDS_DECODE = new Set(['.heic', '.heif', '.avif']);
export interface ImportTransactionSpec {
slug: string;
hadExisting: boolean;
/** Source containing the page, chunks, file row, and type-specific writes. */
sourceId?: string;
page: PageInput;
/** When undefined, no chunk write happens. When [], deletes any prior chunks. */
chunks?: ChunkInput[];
@@ -1328,23 +1330,26 @@ export async function withImportTransaction(
engine: BrainEngine,
spec: ImportTransactionSpec,
): Promise<void> {
const sourceId = spec.sourceId ?? 'default';
const txOpts = spec.sourceId ? { sourceId: spec.sourceId } : undefined;
await engine.transaction(async (tx) => {
if (spec.hadExisting) await tx.createVersion(spec.slug);
await tx.putPage(spec.slug, spec.page);
if (spec.hadExisting) await tx.createVersion(spec.slug, txOpts);
await tx.putPage(spec.slug, spec.page, txOpts);
if (spec.file) {
// page_id resolution after putPage so the new row's id is available.
const stored = await tx.getPage(spec.slug);
const stored = await tx.getPage(spec.slug, txOpts);
await tx.upsertFile({
...spec.file,
source_id: sourceId,
page_slug: spec.slug,
page_id: stored?.id ?? null,
});
}
if (spec.chunks !== undefined) {
if (spec.chunks.length > 0) {
await tx.upsertChunks(spec.slug, spec.chunks);
await tx.upsertChunks(spec.slug, spec.chunks, txOpts);
} else {
await tx.deleteChunks(spec.slug);
await tx.deleteChunks(spec.slug, txOpts);
}
}
if (spec.after) await spec.after(tx);
@@ -1574,10 +1579,14 @@ export async function importImageFile(
// and slugifyPath would already preserve it). Recompute with the file
// extension preserved so the page slug is stable + collision-free.
const imageSlug = relativePath.replace(/[\\\/]/g, '/').toLowerCase();
const sourceOpts = opts.sourceId ? { sourceId: opts.sourceId } : undefined;
const linkOpts = opts.sourceId
? { fromSourceId: opts.sourceId, toSourceId: opts.sourceId, originSourceId: opts.sourceId }
: undefined;
const buf = readFileSync(filePath);
const hash = createHash('sha256').update(buf).digest('hex');
const existing = await engine.getPage(imageSlug);
const existing = await engine.getPage(imageSlug, sourceOpts);
if (existing?.content_hash === hash) {
return { slug: imageSlug, status: 'skipped', chunks: 0 };
}
@@ -1653,6 +1662,7 @@ export async function importImageFile(
await withImportTransaction(engine, {
slug: imageSlug,
hadExisting: !!existing,
sourceId: opts.sourceId,
page: {
type: 'image',
page_kind: 'image',
@@ -1670,13 +1680,14 @@ export async function importImageFile(
// throws when the target doesn't exist; we silently skip for now and
// let `gbrain reconcile-links` pick up later additions.
for (const candidate of imageOfCandidates(imageSlug)) {
const sibling = await tx.getPage(candidate);
const sibling = await tx.getPage(candidate, sourceOpts);
if (sibling) {
try {
await tx.addLink(
imageSlug, candidate,
filename,
'image_of', 'manual', imageSlug, 'frontmatter',
linkOpts,
);
} catch { /* sibling vanished mid-tx; skip */ }
break; // one canonical link per image
+23
View File
@@ -120,6 +120,29 @@ describe('importImageFile happy path (noEmbed)', () => {
expect(r2.status).toBe('skipped');
});
test('routes page, chunks, and file metadata to the requested source (#2706)', async () => {
await engine.executeRaw(
`INSERT INTO sources (id, name) VALUES ($1, $2) ON CONFLICT (id) DO NOTHING`,
['image-source', 'Image Source'],
);
const target = join(tmpDir, 'source-photo.png');
writeFileSync(target, Buffer.from('fake-png-bytes-source-scoped'));
const result = await importImageFile(engine, target, 'photos/source-photo.png', {
noEmbed: true,
sourceId: 'image-source',
});
expect(result.status).toBe('imported');
expect(await engine.getPage('photos/source-photo.png', { sourceId: 'default' })).toBeNull();
const page = await engine.getPage('photos/source-photo.png', { sourceId: 'image-source' });
expect(page).not.toBeNull();
expect(await engine.getChunks('photos/source-photo.png', { sourceId: 'default' })).toHaveLength(0);
expect(await engine.getChunks('photos/source-photo.png', { sourceId: 'image-source' })).toHaveLength(1);
expect(await engine.getFile('default', 'photos/source-photo.png')).toBeNull();
expect(await engine.getFile('image-source', 'photos/source-photo.png')).not.toBeNull();
});
test('refuses oversized files (>20MB)', async () => {
const target = join(tmpDir, 'huge.png');
// Write a 21MB file. Buffer.alloc is fast.