actualbudget/actual · error · FileDownloadError

not-zip-file

not-zip-file

Error message

not-zip-file

What it means

If safeUnzip fails for any reason other than UnsafeZipError (i.e. the buffer is not a valid zip archive), importBuffer throws FileDownloadError with code 'not-zip-file'. It guards against importing files that merely claim to be Actual exports.

Source

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

  if (
    availableMemory != null &&
    entries['db.sqlite'].length > availableMemory
  ) {
    warnings.push('may-exceed-available-memory');
  }

  return { data: Buffer.from(zipped), warnings };
}

export async function importBuffer(fileData, buffer) {
  let entries;
  try {
    entries = safeUnzip(buffer);
  } catch (e) {
    if (e instanceof UnsafeZipError) {
      throw FileDownloadError('zip-too-large', e.meta);
    }
    throw FileDownloadError('not-zip-file');
  }
  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;

View on GitHub (pinned to d4334cb6e6)

Solutions

  1. Re-download/export the budget file and verify it is a real zip (e.g. `unzip -l file.zip`).
  2. Ensure you pass the exported zip archive, not the raw db.sqlite file.
  3. Check the download path/URL for proxies or auth redirects that return HTML; verify file size > 0.
Defensive patterns

Strategy: validation

Validate before calling

const fs = require('fs');
const buf = fs.readFileSync(zipPath);
if (buf.length < 4 || buf.readUInt32BE(0) !== 0x504b0304) {
  throw new Error('File is not a zip archive');
}

Type guard

function isZipBuffer(buf) {
  return buf.length > 4 && buf.readUInt32BE(0) === 0x504b0304;
}

Try / catch

try {
  await actual.importActual(zipPath);
} catch (e) {
  if (e.code === 'not-zip-file') {
    console.error('Not a valid zip; re-export the budget file');
  } else throw e;
}

Prevention

When it happens

Trigger: Passing a non-zip buffer to importActual/importBuffer — an HTML error page saved as .zip, a plain db.sqlite file (not zipped), an empty file, or a corrupted/truncated download.

Common situations: Download URLs returning auth/error pages instead of the file; manually zipping with a structure the importer rejects; serving the raw sqlite file instead of the zipped export; interrupted uploads.

Related errors


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