Skip to content

Commit 292ef36

Browse files
committed
Fix folder_not_found errors for imported projects
- Add --probe-folder flag to push command to find correct root folder ID - Store discovered rootFolderId in .olcli.json for future pushes - Expand probing range to handle projects with non-standard folder offsets - Show helpful tip when folder_not_found errors occur Fixes upload failures for projects imported from external sources (ZIP, Git) where the root folder ID doesn't follow the standard projectId - 1 pattern.
1 parent 051f8ea commit 292ef36

4 files changed

Lines changed: 149 additions & 29 deletions

File tree

‎package-lock.json‎

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎package.json‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@aloth/olcli",
3-
"version": "0.1.2",
3+
"version": "0.1.3",
44
"description": "Overleaf CLI - sync and manage LaTeX projects from the command line",
55
"type": "module",
66
"bin": {

‎src/cli.ts‎

Lines changed: 33 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -662,6 +662,7 @@ program
662662
.option('--project <name>', 'Project name or ID (overrides .olcli.json)')
663663
.option('--all', 'Upload all files (not just changed)')
664664
.option('--dry-run', 'Show what would be uploaded without uploading')
665+
.option('--probe-folder', 'Probe for correct folder ID (use if uploads fail with folder_not_found)')
665666
.option('--cookie <session>', 'Session cookie override')
666667
.action(async (dir, options) => {
667668
const targetDir = dir || '.';
@@ -671,12 +672,14 @@ program
671672
let projectId: string | undefined;
672673
let projectName: string | undefined;
673674
let lastPull: Date | undefined;
675+
let rootFolderId: string | undefined;
674676

675677
if (existsSync(metaPath)) {
676678
const meta = JSON.parse(readFileSync(metaPath, 'utf-8'));
677679
projectId = meta.projectId;
678680
projectName = meta.projectName;
679681
lastPull = meta.lastPull ? new Date(meta.lastPull) : undefined;
682+
rootFolderId = meta.rootFolderId;
680683
}
681684

682685
if (options.project) {
@@ -757,32 +760,59 @@ program
757760
return;
758761
}
759762

763+
// If --probe-folder is set, or if we don't have a cached rootFolderId, try probing
764+
if (options.probeFolder && !rootFolderId) {
765+
spinner.text = 'Probing for correct folder ID...';
766+
rootFolderId = await client.probeRootFolderId(projectId!) ?? undefined;
767+
if (rootFolderId) {
768+
// Save the discovered folder ID
769+
if (existsSync(metaPath)) {
770+
const meta = JSON.parse(readFileSync(metaPath, 'utf-8'));
771+
meta.rootFolderId = rootFolderId;
772+
writeFileSync(metaPath, JSON.stringify(meta, null, 2));
773+
}
774+
spinner.succeed(`Found root folder ID: ${rootFolderId}`);
775+
spinner.start(`Uploading ${filesToUpload.length} file(s)...`);
776+
} else {
777+
spinner.fail('Could not find valid root folder ID');
778+
console.log(chalk.yellow('Try manually specifying rootFolderId in .olcli.json'));
779+
process.exit(1);
780+
}
781+
}
782+
760783
spinner.text = `Uploading ${filesToUpload.length} file(s)...`;
761784

762785
let uploaded = 0;
763786
let failed = 0;
787+
let folderNotFoundCount = 0;
764788

765789
for (const file of filesToUpload) {
766790
try {
767791
const content = readFileSync(file.path);
768-
await client.uploadFile(projectId!, null, file.relativePath, content);
792+
await client.uploadFile(projectId!, rootFolderId || null, file.relativePath, content);
769793
uploaded++;
770794
spinner.text = `Uploading... (${uploaded}/${filesToUpload.length})`;
771795
} catch (error: any) {
772796
console.error(chalk.yellow(`\n Warning: Failed to upload ${file.relativePath}: ${error.message}`));
773797
failed++;
798+
if (error.message.includes('folder_not_found')) {
799+
folderNotFoundCount++;
800+
}
774801
}
775802
}
776803

777-
// Update last pull time
804+
// Update last push time
778805
if (existsSync(metaPath)) {
779806
const meta = JSON.parse(readFileSync(metaPath, 'utf-8'));
780807
meta.lastPush = new Date().toISOString();
781808
writeFileSync(metaPath, JSON.stringify(meta, null, 2));
782809
}
783810

784811
if (failed > 0) {
785-
spinner.warn(`Uploaded ${uploaded} files, ${failed} failed`);
812+
spinner.warn(`Uploaded ${uploaded} file(s), ${failed} failed`);
813+
if (folderNotFoundCount > 0 && !rootFolderId) {
814+
console.log(chalk.yellow(' Tip: Try running with --probe-folder to find the correct folder ID'));
815+
}
786816
} else {
787817
spinner.succeed(`Uploaded ${uploaded} file(s) to "${projectName}"`);
788818
}

‎src/client.ts‎

Lines changed: 113 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)