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
- Re-download/export the budget file and verify it is a real zip (e.g. `unzip -l file.zip`).
- Ensure you pass the exported zip archive, not the raw db.sqlite file.
- 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
- Verify the zip magic bytes (PK) before import
- Ensure downloads aren't HTML error pages (check content-type/size)
- Import the exported zip, never the raw db.sqlite
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
- Invalid date:
- Unsafe zip entry name: ${name}
- Zip archive contains a duplicate entry: ${file.name}
- zip-too-large
- invalid-zip-file
AI-assisted analysis of actualbudget/actual@d4334cb6e6 (2026-08-29).
Data as JSON: /api/errors/ef17a86d1909f5f4.
Report an issue: GitHub.