@@ -420,6 +420,69 @@ export class OverleafClient {
420420 return this . computeRootFolderId ( projectId ) ;
421421 }
422422
423+ /**
424+ * Find root folder ID by probing multiple candidates
425+ * This handles cases where projectId - 1 doesn't work
426+ */
427+ async probeRootFolderId ( projectId : string ) : Promise < string | null > {
428+ const candidates : string [ ] = [ ] ;
429+
430+ // Method 1: Try projectId - 1 (most common)
431+ candidates . push ( this . computeRootFolderId ( projectId ) ) ;
432+
433+ const prefix = projectId . slice ( 0 , 16 ) ;
434+ const suffix = parseInt ( projectId . slice ( 16 ) , 16 ) ;
435+
436+ // Method 2: Try a wide range around the project ID
437+ // Some projects have root folder created with different offsets
438+ for ( let i = 2 ; i <= 50 ; i ++ ) {
439+ if ( suffix - i >= 0 ) {
440+ candidates . push ( prefix + ( suffix - i ) . toString ( 16 ) . padStart ( 8 , '0' ) ) ;
441+ }
442+ }
443+ for ( let i = 1 ; i <= 50 ; i ++ ) {
444+ candidates . push ( prefix + ( suffix + i ) . toString ( 16 ) . padStart ( 8 , '0' ) ) ;
445+ }
446+
447+ // Test each candidate with a minimal probe request
448+ for ( const folderId of candidates ) {
449+ try {
450+ // Try to create a temp file to probe the folder
451+ const testFileName = `.olcli-probe-${ Date . now ( ) } .tmp` ;
452+ const formData = new FormData ( ) ;
453+ formData . append ( 'targetFolderId' , folderId ) ;
454+ formData . append ( 'name' , testFileName ) ;
455+ formData . append ( 'type' , 'text/plain' ) ;
456+ formData . append ( 'qqfile' , new Blob ( [ 'probe' ] ) , testFileName ) ;
457+
458+ const response = await fetch ( `${ UPLOAD_URL . replace ( '{id}' , projectId ) } ?folder_id=${ folderId } ` , {
459+ method : 'POST' ,
460+ headers : {
461+ 'Cookie' : this . getCookieHeader ( ) ,
462+ 'User-Agent' : USER_AGENT ,
463+ 'X-Csrf-Token' : this . csrf
464+ } ,
465+ body : formData
466+ } ) ;
467+
468+ const data = await response . json ( ) as any ;
469+ if ( data . success !== false && data . entity_id ) {
470+ // Success! Delete the probe file and return this folder ID
471+ try {
472+ await this . deleteEntity ( projectId , data . entity_id , 'doc' ) ;
473+ } catch ( e ) {
474+ // Ignore delete errors for probe file
475+ }
476+ return folderId ;
477+ }
478+ } catch ( e ) {
479+ // Continue to next candidate
480+ }
481+ }
482+
483+ return null ;
484+ }
485+
423486 /**
424487 * Upload a file to a project
425488 */
@@ -430,7 +493,7 @@ export class OverleafClient {
430493 content : Buffer
431494 ) : Promise < { success : boolean ; entityId ?: string ; entityType ?: string } > {
432495 // If no folder ID provided, get the root folder
433- const targetFolderId = folderId || await this . getRootFolderId ( projectId ) ;
496+ let targetFolderId = folderId || await this . getRootFolderId ( projectId ) ;
434497
435498 // Extract just the filename without path (PR #73 fix)
436499 const baseName = fileName . split ( '/' ) . pop ( ) || fileName ;
@@ -452,33 +515,60 @@ export class OverleafClient {
452515 } ;
453516 const mimeType = mimeTypes [ ext ] || 'application/octet-stream' ;
454517
455- const formData = new FormData ( ) ;
456- // Match Overleaf-Workshop: include targetFolderId in form data
457- formData . append ( 'targetFolderId' , targetFolderId ) ;
458- formData . append ( 'name' , baseName ) ;
459- formData . append ( 'type' , mimeType ) ;
460- formData . append ( 'qqfile' , new Blob ( [ content ] ) , baseName ) ;
518+ // Helper function to attempt upload with a specific folder ID
519+ const tryUpload = async ( fid : string ) : Promise < { success : boolean ; entityId ?: string ; entityType ?: string ; error ?: string } > => {
520+ const formData = new FormData ( ) ;
521+ formData . append ( 'targetFolderId' , fid ) ;
522+ formData . append ( 'name' , baseName ) ;
523+ formData . append ( 'type' , mimeType ) ;
524+ formData . append ( 'qqfile' , new Blob ( [ content ] ) , baseName ) ;
525+
526+ const response = await fetch ( `${ UPLOAD_URL . replace ( '{id}' , projectId ) } ?folder_id=${ fid } ` , {
527+ method : 'POST' ,
528+ headers : {
529+ 'Cookie' : this . getCookieHeader ( ) ,
530+ 'User-Agent' : USER_AGENT ,
531+ 'X-Csrf-Token' : this . csrf
532+ } ,
533+ body : formData
534+ } ) ;
535+
536+ if ( ! response . ok ) {
537+ const text = await response . text ( ) ;
538+ return { success : false , error : `${ response . status } - ${ text } ` } ;
539+ }
461540
462- const response = await fetch ( `${ UPLOAD_URL . replace ( '{id}' , projectId ) } ?folder_id=${ targetFolderId } ` , {
463- method : 'POST' ,
464- headers : {
465- 'Cookie' : this . getCookieHeader ( ) ,
466- 'User-Agent' : USER_AGENT ,
467- 'X-Csrf-Token' : this . csrf
468- } ,
469- body : formData
470- } ) ;
541+ const data = await response . json ( ) as any ;
542+ if ( data . success === false && data . error === 'folder_not_found' ) {
543+ return { success : false , error : 'folder_not_found' } ;
544+ }
545+ return {
546+ success : data . success !== false ,
547+ entityId : data . entity_id ,
548+ entityType : data . entity_type
549+ } ;
550+ } ;
471551
472- if ( ! response . ok ) {
473- const text = await response . text ( ) ;
474- throw new Error ( `Failed to upload file: ${ response . status } - ${ text } ` ) ;
552+ // First attempt with computed/cached folder ID
553+ let result = await tryUpload ( targetFolderId ) ;
554+
555+ // If folder not found, probe for the correct folder ID
556+ if ( ! result . success && result . error === 'folder_not_found' ) {
557+ const probedFolderId = await this . probeRootFolderId ( projectId ) ;
558+ if ( probedFolderId && probedFolderId !== targetFolderId ) {
559+ targetFolderId = probedFolderId ;
560+ result = await tryUpload ( targetFolderId ) ;
561+ }
562+ }
563+
564+ if ( ! result . success ) {
565+ throw new Error ( `Failed to upload file: ${ result . error || 'unknown error' } ` ) ;
475566 }
476567
477- const data = await response . json ( ) as any ;
478568 return {
479- success : data . success !== false ,
480- entityId : data . entity_id ,
481- entityType : data . entity_type
569+ success : result . success ,
570+ entityId : result . entityId ,
571+ entityType : result . entityType
482572 } ;
483573 }
484574
0 commit comments