actualbudget/actual · error

Could not find file: ${path}

Error message

Could not find file: ${path}

What it means

When importing a YNAB4 (.ynab4) budget zip, parseFile unzips the archive and getFile(entries, path) looks up an entry whose name exactly equals the requested path. If no zip entry matches exactly, this error is thrown. It almost always means the archive has a different internal directory layout than the importer expects (e.g. an extra or missing root folder like 'Budget~.../*.ynab4/').

Source

Thrown at packages/loot-core/src/server/importers/ynab4.ts:420

    return null;
  }

  unixFilepath = unixFilepath.replace(/\.zip$/, '').replace(/.ynab4$/, '');

  // Most budgets are named like "Budget~51938D82.ynab4" but sometimes
  // they are only "Budget.ynab4". We only want to grab the name
  // before the ~ if it exists.
  const m = unixFilepath.match(/([^/~]+)[^/]*$/);
  if (!m) {
    return null;
  }
  return m[1];
}

function getFile(entries: string[], path: string) {
  const files = entries.filter(e => e === path);
  if (files.length === 0) {
    throw new Error('Could not find file: ' + path);
  }
  if (files.length >= 2) {
    throw new Error('File name matches multiple files: ' + path);
  }
  return files[0];
}

function join(...paths: string[]): string {
  return paths.slice(1).reduce(
    (full, path) => {
      return full + '/' + path.replace(/^\//, '');
    },
    paths[0].replace(/\/$/, ''),
  );
}

export function parseFile(buffer: Buffer): YNAB4.YFull {
  let zipped: Record<string, Uint8Array>;

View on GitHub (pinned to d4334cb6e6)

Solutions

  1. Re-zip the budget so the archive contains the original structure: a top-level folder ending in .ynab4 containing Budget.yfull and the data files, with no extra wrapper folders.
  2. Inspect the zip contents (unzip -l budget.ynab4) and confirm Budget.yfull exists at the expected path; fix casing/nesting to match exactly.
  3. Verify you are importing a YNAB4 export, not a YNAB5 (.yfull vs nYab format); use the correct importer for the format.
  4. Extract the zip, confirm the file exists and is not corrupted, then re-zip without compression changes to the inner structure.

Example fix

// before (broken zip layout)
Budget.yfull
Data/payees.yfull
// after (expected YNAB4 layout)
MyBudget~9C1A.ynab4/Budget.yfull
MyBudget~9C1A.ynab4/Data/payees.yfull
Defensive patterns

Strategy: validation

Validate before calling

import { unzipSync } from 'fflate';
function canParseYnab4(buffer) {
  try {
    const entries = Object.keys(safeUnzip(buffer));
    return entries.some(e => /[^/]*\.ynab4/.test(e)) &&
      entries.some(e => e.endsWith('Budget.yfull'));
  } catch { return false; }
}

Type guard

function isYnab4Zip(buffer) {
  return Buffer.isBuffer(buffer) &&
    buffer.length > 4 &&
    buffer.subarray(0, 2).toString('latin1') === 'PK';
}

Try / catch

try {
  const budget = parseFile(buffer);
} catch (e) {
  if (e.message.startsWith('Could not find file')) {
    // inspect zip layout, re-export/re-zip the budget
  } else throw e;
}

Prevention

When it happens

Trigger: Calling parseFile(buffer) on a YNAB4 zip whose entries, after root-directory stripping, do not contain an entry exactly equal to the computed yfullPath or other requested file (Budget.yfull, payees, etc.), e.g. because the zip lacks the top-level *.ynab4 directory or uses differing case/extra nesting.

Common situations: Re-exported or manually re-zipped YNAB4 backups where the folder structure changed; macOS/Windows zip tools adding an extra wrapper folder; renamed budget directory inside the archive; importing a YNAB5 (nYab) file through the YNAB4 importer path.

Related errors


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