Skip to content

Commit c06321b

Browse files
chitcommitclaude
andauthored
fix: document upload crash, duplicate wrangler config, batch upload gaps (#90)
- Fix btoa(String.fromCharCode(...bytes)) stack overflow on files >64KB by using chunked base64 encoding (affects both single and batch uploads) - Remove duplicate wrangler.toml — wrangler.jsonc is canonical (PR #79), add SVC_STORAGE service binding to wrangler.jsonc instead - Fix batch upload: check ChittyStorage response for errors, create cc_documents DB records so batch uploads appear in UI - Add fallback warning log when single upload falls through to legacy R2 Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 9ad45ea commit c06321b

3 files changed

Lines changed: 43 additions & 96 deletions

File tree

src/routes/documents.ts

Lines changed: 38 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,16 @@ import { Hono } from 'hono';
22
import type { Env } from '../index';
33
import { getDb } from '../lib/db';
44

5+
/** Chunked base64 encoding — avoids stack overflow on files >64KB */
6+
function uint8ToBase64(bytes: Uint8Array): string {
7+
let binary = '';
8+
const chunkSize = 0x8000;
9+
for (let i = 0; i < bytes.length; i += chunkSize) {
10+
binary += String.fromCharCode(...bytes.subarray(i, i + chunkSize));
11+
}
12+
return btoa(binary);
13+
}
14+
515
export const documentRoutes = new Hono<{ Bindings: Env }>();
616

717
documentRoutes.get('/', async (c) => {
@@ -44,7 +54,7 @@ documentRoutes.post('/upload', async (c) => {
4454
// Submit to ChittyStorage via service binding
4555
if (c.env.SVC_STORAGE) {
4656
try {
47-
const content_base64 = btoa(String.fromCharCode(...bytes));
57+
const content_base64 = uint8ToBase64(bytes);
4858
const storageRes = await c.env.SVC_STORAGE.fetch('https://internal/mcp', {
4959
method: 'POST',
5060
headers: { 'Content-Type': 'application/json' },
@@ -84,6 +94,8 @@ documentRoutes.post('/upload', async (c) => {
8494
} catch (err) {
8595
console.error('[documents] ChittyStorage ingest failed, falling back to direct R2:', err);
8696
}
97+
// If we reach here, ChittyStorage either isn't bound or returned unparseable result — fall through to direct R2
98+
console.warn('[documents] Using legacy R2 fallback for:', safeName);
8799
}
88100

89101
// Fallback: direct R2 (legacy path — remove once SVC_STORAGE is confirmed stable)
@@ -108,6 +120,7 @@ documentRoutes.post('/upload/batch', async (c) => {
108120
if (!files.length) return c.json({ error: 'No files provided' }, 400);
109121
if (files.length > 20) return c.json({ error: 'Maximum 20 files per batch' }, 400);
110122

123+
const sql = getDb(c.env);
111124
const results: { filename: string; status: 'ok' | 'skipped' | 'error'; error?: string; content_hash?: string }[] = [];
112125

113126
for (const file of files) {
@@ -122,27 +135,47 @@ documentRoutes.post('/upload/batch', async (c) => {
122135
const hashBuf = await crypto.subtle.digest('SHA-256', bytes);
123136
const contentHash = Array.from(new Uint8Array(hashBuf)).map(b => b.toString(16).padStart(2, '0')).join('');
124137

138+
const chittyId = `scan-${contentHash.slice(0, 12)}`;
139+
let r2Key = `sha256/${contentHash}`;
140+
125141
if (c.env.SVC_STORAGE) {
126-
const content_base64 = btoa(String.fromCharCode(...bytes));
127-
await c.env.SVC_STORAGE.fetch('https://internal/mcp', {
142+
const content_base64 = uint8ToBase64(bytes);
143+
const storageRes = await c.env.SVC_STORAGE.fetch('https://internal/mcp', {
128144
method: 'POST',
129145
headers: { 'Content-Type': 'application/json' },
130146
body: JSON.stringify({
131147
jsonrpc: '2.0', method: 'tools/call',
132148
params: { name: 'storage_ingest', arguments: {
133-
chitty_id: `scan-${contentHash.slice(0, 12)}`, filename: safeName,
149+
chitty_id: chittyId, filename: safeName,
134150
content_base64, mime_type: file.type, source_platform: 'chittycommand',
135151
origin: 'first-party', copyright: '©2026_IT-CAN-BE-LLC_ALL-RIGHTS-RESERVED',
136152
entity_slugs: entitySlug ? [entitySlug] : [],
137153
}}, id: 1,
138154
}),
139155
});
156+
const mcp = await storageRes.json() as any;
157+
const resultText = mcp?.result?.content?.[0]?.text;
158+
if (resultText) {
159+
const parsed = JSON.parse(resultText);
160+
r2Key = parsed.r2_key ?? r2Key;
161+
} else if (!storageRes.ok || mcp?.error) {
162+
console.error(`[documents] Batch: ChittyStorage failed for ${safeName}:`, mcp?.error ?? storageRes.status);
163+
results.push({ filename: safeName, status: 'error', error: 'ChittyStorage ingest failed' });
164+
continue;
165+
}
140166
} else {
141-
await c.env.DOCUMENTS.put(`sha256/${contentHash}`, bytes, {
167+
await c.env.DOCUMENTS.put(r2Key, bytes, {
142168
httpMetadata: { contentType: file.type },
143169
customMetadata: { filename: safeName, source: 'chittycommand' },
144170
});
145171
}
172+
173+
await sql`
174+
INSERT INTO cc_documents (doc_type, source, filename, r2_key, processing_status, metadata)
175+
VALUES ('upload', 'chittycommand', ${safeName}, ${r2Key}, 'synced',
176+
${JSON.stringify({ content_hash: contentHash, storage_chitty_id: chittyId, batch: true })}::jsonb)
177+
ON CONFLICT (r2_key) DO NOTHING
178+
`;
146179
results.push({ filename: safeName, status: 'ok', content_hash: contentHash });
147180
} catch (err) {
148181
results.push({ filename: safeName, status: 'error', error: String(err) });

wrangler.jsonc

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,11 @@
7272
}
7373
},
7474

75+
// ChittyStorage service binding for document ingest
76+
"services": [
77+
{ "binding": "SVC_STORAGE", "service": "chittystorage" }
78+
],
79+
7580
// ChittyTrack observability
7681
"tail_consumers": [
7782
{ "service": "chittytrack" }

wrangler.toml

Lines changed: 0 additions & 91 deletions
This file was deleted.

0 commit comments

Comments
 (0)