@@ -2,6 +2,16 @@ import { Hono } from 'hono';
22import type { Env } from '../index' ;
33import { 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+
515export const documentRoutes = new Hono < { Bindings : Env } > ( ) ;
616
717documentRoutes . 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 ) } ) ;
0 commit comments