Skip to content

Commit f4f4f11

Browse files
feat(api): stream attachments with Content-Length
gitsheets 2.9 gives every BlobHandle a size and a real stream() (gitsheets#306), so a 200 no longer buffers the whole blob. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011Q2tgjZEUrtvVk6k8SpdXJ
1 parent fecb25e commit f4f4f11

2 files changed

Lines changed: 20 additions & 2 deletions

File tree

‎apps/api/src/routes/attachments.ts‎

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
* (gitsheets auto-refresh) and after a hot reload (`store.public.refresh()`).
1212
*
1313
* The blob hash is the `ETag`, so a revalidation is answered with 304 without
14-
* reading the blob at all.
14+
* reading the blob at all; a 200 streams the blob with its `Content-Length`.
1515
*/
1616
import type { FastifyInstance, FastifyRequest } from 'fastify';
1717

@@ -120,7 +120,12 @@ export async function attachmentRoutes(fastify: FastifyInstance): Promise<void>
120120
return reply.code(304).send();
121121
}
122122

123-
return reply.type(inferContentType(key)).send(await blob.read());
123+
// Stream rather than buffer: gitsheets serves small blobs from one
124+
// in-core read and pipes larger ones from git.
125+
return reply
126+
.type(inferContentType(key))
127+
.header('Content-Length', blob.size)
128+
.send(blob.stream());
124129
},
125130
);
126131
}

‎apps/api/tests/attachments.test.ts‎

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -186,4 +186,17 @@ describe('GET /api/attachments/*', () => {
186186
const res = await app.inject({ method: 'GET', url: '/api/attachments/not-a-sheet/x/y.png' });
187187
expect(res.statusCode).toBe(404);
188188
});
189+
190+
it('streams a blob larger than 1 MiB intact, with Content-Length', async () => {
191+
const big = Buffer.alloc(3 * 1024 * 1024 + 17);
192+
for (let i = 0; i < big.length; i++) big[i] = (i * 31 + 7) & 0xff;
193+
await seedRawBlob(dataRepo.path, 'people/big/scan.pdf', big, 'seed a large attachment');
194+
app = await bootApp();
195+
196+
const res = await app.inject({ method: 'GET', url: '/api/attachments/people/big/scan.pdf' });
197+
expect(res.statusCode).toBe(200);
198+
expect(res.headers['content-length']).toBe(String(big.length));
199+
expect(res.rawPayload.equals(big)).toBe(true);
200+
});
189201
});
202+

0 commit comments

Comments
 (0)