actualbudget/actual · error · FileDownloadError

invalid-zip-file

invalid-zip-file

Error message

invalid-zip-file

What it means

FileDownloadError('invalid-zip-file') is thrown by importBuffer when the downloaded budget archive is a valid ZIP but does not contain db.sqlite and metadata.json in a usable layout. The two entries must both exist at the archive root, or both in exactly one shared subdirectory; otherwise the import cannot locate the database. This guards against corrupted or structurally wrong backup archives.

Source

Thrown at packages/loot-core/src/server/cloud-storage.ts:240

  const entryNames = Object.keys(entries);
  const dbDirs = entryNames
    .filter(name => name === 'db.sqlite' || name.endsWith('/db.sqlite'))
    .map(name => name.slice(0, -'db.sqlite'.length));
  const metaDirs = entryNames
    .filter(name => name === 'metadata.json' || name.endsWith('/metadata.json'))
    .map(name => name.slice(0, -'metadata.json'.length));

  // Both files must come from the same directory: prefer the archive root,
  // otherwise there must be exactly one directory containing both.
  const sharedDirs = dbDirs.filter(dir => metaDirs.includes(dir));
  const dir = sharedDirs.includes('')
    ? ''
    : sharedDirs.length === 1
      ? sharedDirs[0]
      : null;

  if (dir == null) {
    throw FileDownloadError('invalid-zip-file');
  }

  const entryName = dir + 'db.sqlite';
  const metaEntryName = dir + 'metadata.json';

  const dbContent = Buffer.from(entries[entryName]);
  const metaContent = Buffer.from(entries[metaEntryName]);

  let meta;
  try {
    meta = JSON.parse(metaContent.toString('utf8'));
  } catch {
    throw FileDownloadError('invalid-meta-file');
  }

  // Update the metadata. The stored file on the server might be
  // out-of-date with a few keys
  meta = {

View on GitHub (pinned to d4334cb6e6)

Solutions

  1. Re-download the budget from the sync server instead of using a locally saved zip
  2. Open the zip and verify db.sqlite and metadata.json are both at the root (or in one shared folder) and repack if needed
  3. If importing manually, use an export produced by Actual itself (Export budget) rather than a hand-built archive
  4. Check the archive is not truncated; unzip -t on the file to test integrity

Example fix

// before: importing an arbitrary zip
catch (e) { /* invalid-zip-file */ }
// after: validate structure first
const names = Object.keys(entries);
const ok = ['db.sqlite','metadata.json'].every(f => names.includes(f));
if (!ok) throw new Error('archive must contain db.sqlite and metadata.json at root');
Defensive patterns

Strategy: validation

Validate before calling

async function assertImportableZip(buffer) {
  const zip = await loadZip(buffer);
  const names = Object.keys(zip.entries);
  const dbDirs = names.filter(n => n === 'db.sqlite' || n.endsWith('/db.sqlite')).map(n => n.slice(0, -'db.sqlite'.length));
  const metaDirs = names.filter(n => n === 'metadata.json' || n.endsWith('/metadata.json')).map(n => n.slice(0, -'metadata.json'.length));
  const shared = dbDirs.filter(d => metaDirs.includes(d));
  if (!(shared.includes('') || shared.length === 1)) throw new Error('invalid-zip-file');
}

Type guard

function isImportableArchive(entries) {
  const names = Object.keys(entries);
  const has = f => names.some(n => n === f || n.endsWith('/' + f));
  return has('db.sqlite') && has('metadata.json');
}

Try / catch

try {
  await importActual(buffer);
} catch (e) {
  if (e instanceof FileDownloadError && e.reason === 'invalid-zip-file') {
    console.error('Archive lacks db.sqlite + metadata.json; use an Actual export.');
  } else throw e;
}

Prevention

When it happens

Trigger: calling download() or importActual() with an archive whose entries are split across different directories, contain db.sqlite/metadata.json in multiple conflicting subdirectories, or are missing one of the two required files entirely.

Common situations: manually zipped budgets where only db.sqlite was included, archives repacked by third-party tools that nest files inconsistently, corrupted partial uploads, or an old export format lacking metadata.json.

Related errors


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