{"record":{"id":"305b5eb48bc4a0d2","repo":"actualbudget/actual","slug":"failed-to-duplicate-budget-file-error-message","errorCode":null,"errorMessage":"Failed to duplicate budget file: ${error.message}","messagePattern":"Failed to duplicate budget file: (.+?)","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"packages/loot-core/src/server/budgetfiles/app.ts","lineNumber":373,"sourceCode":"    // write metadata for new budget\n    await fs.writeFile(\n      fs.join(newBudgetDir, 'metadata.json'),\n      JSON.stringify(metadata),\n    );\n\n    await fs.copyFile(\n      fs.join(budgetDir, 'db.sqlite'),\n      fs.join(newBudgetDir, 'db.sqlite'),\n    );\n  } catch (error) {\n    // Clean up any partially created files\n    try {\n      const newBudgetDir = fs.getBudgetDir(newId);\n      if (await fs.exists(newBudgetDir)) {\n        await fs.removeDirRecursively(newBudgetDir);\n      }\n    } catch {} // Ignore cleanup errors\n    throw new Error(`Failed to duplicate budget file: ${error.message}`);\n  }\n\n  // load in and validate\n  const { error } = await _loadBudget(newId);\n  if (error) {\n    logger.log('Error duplicating budget: ' + error);\n    return error;\n  }\n\n  if (cloudSync) {\n    try {\n      await cloudStorage.upload();\n    } catch (error) {\n      logger.warn('Failed to sync duplicated budget to cloud:', error);\n      // Ignore any errors uploading. If they are offline they should\n      // still be able to create files.\n    }\n  }","sourceCodeStart":355,"sourceCodeEnd":391,"githubUrl":"https://github.com/actualbudget/actual/blob/d4334cb6e6123f4d3bcea1ad6166608884c7e658/packages/loot-core/src/server/budgetfiles/app.ts#L355-L391","documentation":"Inside `duplicateBudget`, after validation the budget directory is copied to a new id. If any filesystem operation during the copy throws, the code attempts a best-effort cleanup of the partially created target directory (removing it recursively, ignoring cleanup errors) and then re-throws wrapped as `Failed to duplicate budget file: <original message>`. This indicates an I/O-level failure, not a naming problem — by the time this fires the name was already validated.","triggerScenarios":"Calling `duplicateBudget` when the source budget directory is unreadable/missing on disk, the disk is full, permissions deny writing to the budgets folder, the target directory exists and cannot be removed, or an OS-level error interrupts the recursive copy.","commonSituations":"Running the sync server against a read-only or quota-exhausted volume; budgets stored on a network mount that dropped mid-copy; permission changes after moving the data directory (e.g. Docker volume owned by another uid); a stale/corrupt target directory left by a previous failed duplication.","solutions":["Inspect the wrapped `error.message` suffix — it names the underlying OS failure (ENOENT, EACCES, ENOSPC, EBUSY) and points at the actual fix.","Verify the source budget directory exists and is readable at `fs.getBudgetDir(id)`, and that the parent budgets directory is writable by the server process.","Free disk space or fix permissions (chown/chmod on the data dir, correct the Docker volume uid) as indicated by the message.","Manually remove any leftover target budget directory from the failed attempt, then retry `duplicateBudget`."],"exampleFix":"// before (shell, as root-created volume)\nls -l /data/budgets  # owned by root, server runs as uid 1000\n\n// after\nchown -R 1000:1000 /data/budgets","handlingStrategy":"try-catch","validationCode":"import fs from 'fs/promises';\n// before duplicating:\nconst srcDir = getBudgetDir(id);\nawait fs.access(srcDir, fs.constants.R_OK);          // source readable\nawait fs.access(path.dirname(srcDir), fs.constants.W_OK); // parent writable\nconst stats = await fs.statfs(path.dirname(srcDir));  // check free space (Node >=18.15)","typeGuard":null,"tryCatchPattern":"try {\n  await duplicateBudget({ id, newName, cloudSync: false, open: 'none' });\n} catch (e) {\n  // e.message = 'Failed to duplicate budget file: <os error>'\n  logger.error('Budget duplication failed:', e.message);\n  // clean up leftover partial copy if the target dir exists, then retry once\n}","preventionTips":["Keep the Actual data directory on a local, writable volume with ample free space","Fix ownership/permissions when running in Docker (match the server process uid)","Watch for wrapped OS codes: ENOSPC (disk full), EACCES (permissions), ENOENT (missing source)","After a failed duplication, remove the leftover target budget directory before retrying"],"tags":["filesystem","io","budget"],"backgroundTag":"file-copy-failure","analyzedSha":"d4334cb6e6123f4d3bcea1ad6166608884c7e658","analyzedAt":"2026-08-29T01:02:11.213Z","schemaVersion":2},"datasetVersion":"2026-08-29T02:17:18.158Z"}