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
- Re-download the budget from the sync server instead of using a locally saved zip
- Open the zip and verify db.sqlite and metadata.json are both at the root (or in one shared folder) and repack if needed
- If importing manually, use an export produced by Actual itself (Export budget) rather than a hand-built archive
- 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
- Only import archives produced by Actual's own export
- Run `unzip -t` to verify archive integrity before importing
- Keep db.sqlite and metadata.json together at the archive root when repacking
- Never hand-edit entries inside budget zips
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
- invalid-meta-file
- zip-too-large
- not-zip-file
- zipMeta ? getUnsafeZipError(zipMeta) : error
- Transfer payee with account ID ${id} not found.
AI-assisted analysis of actualbudget/actual@d4334cb6e6 (2026-08-29).
Data as JSON: /api/errors/63c7661e7ef2e776.
Report an issue: GitHub.