{"record":{"id":"2a804c1346b5e803","repo":"jackwener/OpenCLI","slug":"failed-to-write-pixiv-novel-path-basename-plan-d","errorCode":null,"errorMessage":"Failed to write Pixiv novel ${path.basename(plan.destPath)}: ${error?.message || error}","messagePattern":"Failed to write Pixiv novel (.+?): (.+?)","errorType":"exception","errorClass":"CommandExecutionError","httpStatus":null,"severity":"error","filePath":"clis/pixiv/novel-download-utils.js","lineNumber":198,"sourceCode":"export function commitNovelFile(plan) {\n  let descriptor;\n  try {\n    fs.mkdirSync(plan.outputDir, { recursive: true });\n    descriptor = fs.openSync(plan.destPath, 'wx');\n    fs.writeFileSync(descriptor, plan.content, 'utf8');\n    fs.closeSync(descriptor);\n    descriptor = undefined;\n    return plan.destPath;\n  } catch (error) {\n    if (descriptor !== undefined) {\n      try { fs.closeSync(descriptor); } catch {}\n      try { fs.rmSync(plan.destPath, { force: true }); } catch {}\n    }\n    for (const directory of plan.createdDirs) {\n      try { fs.rmdirSync(directory); } catch {}\n    }\n    if (error instanceof CommandExecutionError) throw error;\n    throw new CommandExecutionError(`Failed to write Pixiv novel ${path.basename(plan.destPath)}: ${error?.message || error}`);\n  }\n}\n\nexport function cleanupNovelFile(plan) {\n  try { fs.rmSync(plan.destPath, { force: true }); } catch {}\n  for (const directory of plan.createdDirs) {\n    try { fs.rmdirSync(directory); } catch {}\n  }\n}\n","sourceCodeStart":180,"sourceCodeEnd":208,"githubUrl":"https://github.com/jackwener/OpenCLI/blob/49907e53dc3ade5c223ff0c4c2c2785687cec4e6/clis/pixiv/novel-download-utils.js#L180-L208","documentation":"commitNovelFile performs the actual mkdir/write (mkdirSync + openSync with 'wx' flag) and rolls back on failure. Any error that is not already a CommandExecutionError (e.g. ENOSPC disk full, EACCES, EMFILE, EIO) is wrapped in a CommandExecutionError prefixed with 'Failed to write Pixiv novel <basename>' plus the underlying OS message.","triggerScenarios":"fs.mkdirSync fails (permission denied on outputDir), openSync 'wx' fails (file appeared between prepare and commit — EEXIST; disk full — ENOSPC; too many open files — EMFILE), or writeFileSync fails with EIO mid-write.","commonSituations":"Disk quota exceeded or full disk when downloading large batches; another process creating the same destination between the exists-check and the write (TOCTOU with 'wx'); running out of file descriptors in large parallel batches; output folder made read-only mid-run.","solutions":["Read error.message in the thrown CommandExecutionError to identify the OS cause (ENOSPC -> free disk space; EACCES -> fix permissions; EMFILE -> raise ulimit -n or reduce concurrency).","Free disk space / raise quota, then re-run after running cleanupNovelFile to remove partial artifacts.","Ensure only one process downloads into the same output directory to avoid 'wx' EEXIST races.","Reduce parallelism for large batches; retry the download once the transient cause (mount, space, fds) is fixed."],"exampleFix":"// before\nawait Promise.all(ids.map(id => writeNovelFile(bodies[id], out))); // EMFILE\n// after\nimport pLimit from 'p-limit';\nconst limit = pLimit(4);\nawait Promise.all(ids.map(id => limit(() => writeNovelFile(bodies[id], out))));","handlingStrategy":"try-catch","validationCode":"import fs from 'node:fs';\nfunction assertWritable(dir) {\n  fs.mkdirSync(dir, { recursive: true });\n  fs.accessSync(dir, fs.constants.W_OK);\n  const stat = fs.statfsSync(dir);\n  if (stat.bavail * stat.bsize < 10 * 1024 * 1024) throw new Error('Less than 10MB free');\n}","typeGuard":null,"tryCatchPattern":"try {\n  await downloadNovel(id, { output });\n} catch (e) {\n  if (e.message.startsWith('Failed to write Pixiv novel')) {\n    console.error(`Write failed: ${e.message}`);\n    if (e.message.includes('ENOSPC')) freeDiskSpace();\n    if (e.message.includes('EACCES')) fixPermissions();\n  } else throw e;\n}","preventionTips":["Check free disk space before large batch downloads.","Verify the output directory is writable (accessSync W_OK) before starting.","Run one writer per output directory to avoid 'wx'-flag EEXIST races.","Limit concurrency (p-limit) to avoid EMFILE from too many open files.","Call cleanupNovelFile(plan) on failure so partial writes are removed."],"tags":["filesystem","disk-space","permissions","file-write","race-condition"],"backgroundTag":"file-write-failed","analyzedSha":"49907e53dc3ade5c223ff0c4c2c2785687cec4e6","analyzedAt":"2026-08-29T08:14:47.543Z","schemaVersion":2},"datasetVersion":"2026-08-29T12:17:43.993Z"}