actualbudget/actual · error

Failed to duplicate budget file: ${error.message}

Error message

Failed to duplicate budget file: ${error.message}

What it means

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.

Source

Thrown at packages/loot-core/src/server/budgetfiles/app.ts:373

    // write metadata for new budget
    await fs.writeFile(
      fs.join(newBudgetDir, 'metadata.json'),
      JSON.stringify(metadata),
    );

    await fs.copyFile(
      fs.join(budgetDir, 'db.sqlite'),
      fs.join(newBudgetDir, 'db.sqlite'),
    );
  } catch (error) {
    // Clean up any partially created files
    try {
      const newBudgetDir = fs.getBudgetDir(newId);
      if (await fs.exists(newBudgetDir)) {
        await fs.removeDirRecursively(newBudgetDir);
      }
    } catch {} // Ignore cleanup errors
    throw new Error(`Failed to duplicate budget file: ${error.message}`);
  }

  // load in and validate
  const { error } = await _loadBudget(newId);
  if (error) {
    logger.log('Error duplicating budget: ' + error);
    return error;
  }

  if (cloudSync) {
    try {
      await cloudStorage.upload();
    } catch (error) {
      logger.warn('Failed to sync duplicated budget to cloud:', error);
      // Ignore any errors uploading. If they are offline they should
      // still be able to create files.
    }
  }

View on GitHub (pinned to d4334cb6e6)

Solutions

  1. Inspect the wrapped `error.message` suffix — it names the underlying OS failure (ENOENT, EACCES, ENOSPC, EBUSY) and points at the actual fix.
  2. 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.
  3. Free disk space or fix permissions (chown/chmod on the data dir, correct the Docker volume uid) as indicated by the message.
  4. Manually remove any leftover target budget directory from the failed attempt, then retry `duplicateBudget`.

Example fix

// before (shell, as root-created volume)
ls -l /data/budgets  # owned by root, server runs as uid 1000

// after
chown -R 1000:1000 /data/budgets
Defensive patterns

Strategy: try-catch

Validate before calling

import fs from 'fs/promises';
// before duplicating:
const srcDir = getBudgetDir(id);
await fs.access(srcDir, fs.constants.R_OK);          // source readable
await fs.access(path.dirname(srcDir), fs.constants.W_OK); // parent writable
const stats = await fs.statfs(path.dirname(srcDir));  // check free space (Node >=18.15)

Try / catch

try {
  await duplicateBudget({ id, newName, cloudSync: false, open: 'none' });
} catch (e) {
  // e.message = 'Failed to duplicate budget file: <os error>'
  logger.error('Budget duplication failed:', e.message);
  // clean up leftover partial copy if the target dir exists, then retry once
}

Prevention

When it happens

Trigger: 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.

Common situations: 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.

Related errors


AI-assisted analysis of actualbudget/actual@d4334cb6e6 (2026-08-29). Data as JSON: /api/errors/305b5eb48bc4a0d2. Report an issue: GitHub.