{"record":{"id":"fcc154b7195f7e94","repo":"laurent22/joplin","slug":"extracting-outpath-would-overwrite","errorCode":null,"errorMessage":"Extracting ${outPath} would overwrite","messagePattern":"Extracting (.+?) would overwrite","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"packages/app-mobile/utils/fs-driver/tarExtract.ts","lineNumber":30,"sourceCode":"\tconst cwd = options.cwd;\n\n\t// resolve doesn't correctly handle file:// or content:// URLs. Thus, we don't resolve relative\n\t// to cwd if the source is a URL.\n\tconst isSourceUrl = options.file.match(/$[a-z]+:\\/\\//);\n\tconst filePath = isSourceUrl ? options.file : resolve(cwd, options.file);\n\n\tconst fsDriver = shim.fsDriver();\n\tif (!(await fsDriver.exists(filePath))) {\n\t\tthrow new Error('tarExtract: Source file does not exist');\n\t}\n\n\tconst extract = tarStreamExtract({ defaultEncoding: 'base64' });\n\n\textract.on('entry', async (header, stream, next) => {\n\t\tconst outPath = fsDriver.resolveRelativePathWithinDir(cwd, header.name);\n\n\t\tif (await fsDriver.exists(outPath)) {\n\t\t\tthrow new Error(`Extracting ${outPath} would overwrite`);\n\t\t}\n\n\t\t// Allows moving to the next item after all data for this entry has been read\n\t\t// **and** this data has been processed.\n\t\t// See https://github.com/laurent22/joplin/issues/10285\n\t\tconst streamEndPromise = new Promise<void>((resolve) => {\n\t\t\tstream.once('end', () => resolve());\n\t\t});\n\n\t\tif (header.type === 'directory') {\n\t\t\tawait fsDriver.mkdir(outPath);\n\t\t} else if (header.type === 'file') {\n\t\t\tconst parentDir = dirname(outPath);\n\t\t\tawait fsDriver.mkdir(parentDir);\n\n\t\t\tawait fsDriver.appendBinaryReadableToFile(outPath, stream);\n\t\t} else {\n\t\t\tthrow new Error(`Unsupported file system entity type: ${header.type}`);","sourceCodeStart":12,"sourceCodeEnd":48,"githubUrl":"https://github.com/laurent22/joplin/blob/2654b33620775080d1d59c552259d41e33dad3d2/packages/app-mobile/utils/fs-driver/tarExtract.ts#L12-L48","documentation":"Thrown during tar entry processing when the resolved output path for an entry already exists on disk. tarExtract treats extraction as non-destructive: it refuses to overwrite anything already present in the cwd target directory. The path is first sanitized through fsDriver.resolveRelativePathWithinDir(cwd, header.name) to prevent path traversal, then existence is checked.","triggerScenarios":"Extracting a tar into a cwd that already contains files/dirs with the same names; a previous interrupted extraction left partial files; extracting the same archive twice; the tar contains entries whose header.name collide with pre-existing user data in cwd.","commonSituations":"Re-running a backup restore into a non-empty directory; the restore target was not cleaned between attempts; a sync conflict deposited files with identical names; tar was built with absolute or duplicated entry names that collapse onto the same target.","solutions":["Clean or use a fresh empty directory as cwd before extracting: await shim.fsDriver().removeAllDir(cwd); await shim.fsDriver().mkdir(cwd).","If re-extraction is intentional, delete the specific colliding paths first.","Inspect the tar's entry list (tar -tf on desktop) to find which entry name collides with outPath in the error message.","Ensure the caller does not extract into the live resource directory without prior eviction."],"exampleFix":"// before\ntarExtract({ cwd: targetDir, file: archive });\n// after — ensure a clean target\nif (await shim.fsDriver().exists(targetDir)) {\n\tawait shim.fsDriver().removeAllDir(targetDir);\n}\nawait shim.fsDriver().mkdir(targetDir);\ntarExtract({ cwd: targetDir, file: archive });","handlingStrategy":"validation","validationCode":"const fsDriver = shim.fsDriver();\n// Ensure cwd is empty or pre-cleaned before extraction\nconst entries = await fsDriver.readDirStats(options.cwd);\nif (entries.length > 0) {\n  await fsDriver.removeAllDir(options.cwd);\n  await fsDriver.mkdir(options.cwd);\n}\nawait tarExtract(options);","typeGuard":"function isCleanDir(stats) { return stats.length === 0; }","tryCatchPattern":"try {\n  await tarExtract(options);\n} catch (e) {\n  if (/Extracting .* would overwrite/.test(e.message)) {\n    const p = e.message.match(/Extracting (.*) would overwrite/)?.[1];\n    await shim.fsDriver().remove(p);\n    await tarExtract(options); // retry once\n  } else throw e;\n}","preventionTips":["Extract into a freshly created empty directory.","Never extract into the live resource directory without eviction.","Keep the archive's entry list free of name collisions."],"tags":["mobile","filesystem","tar","backup","data-integrity"],"backgroundTag":null,"analyzedSha":"2654b33620775080d1d59c552259d41e33dad3d2","analyzedAt":"2026-08-12T14:26:46.263Z","schemaVersion":2},"datasetVersion":"2026-08-12T18:17:37.767Z"}