actualbudget/actual · error
Error reading backup zip file
Error message
Error reading backup zip file
What it means
loadBackup extracts a backup zip via safeUnzip (which enforces zip-slip and entry-size safety). If the archive cannot be parsed at all — corrupt data, unsupported compression, or a malicious structure rejected by safeUnzip — the underlying error is logged and rethrown as 'Error reading backup zip file'.
Source
Thrown at packages/loot-core/src/server/budgetfiles/backups.ts:243
// Re-upload the new file
try {
await cloudStorage.upload();
} catch {}
prefs.unloadPrefs();
const zipContent = await fs.readFile(
fs.join(budgetDir, 'backups', backupId),
'binary',
);
let entries: Record<string, Uint8Array>;
try {
entries = safeUnzip(zipContent);
} catch (e) {
logger.log(e);
throw new Error('Error reading backup zip file');
}
if (!entries['db.sqlite'] || !entries['metadata.json']) {
throw new Error('Backup zip file is missing db.sqlite or metadata.json');
}
await fs.writeFile(fs.join(budgetDir, 'db.sqlite'), entries['db.sqlite']);
await fs.writeFile(
fs.join(budgetDir, 'metadata.json'),
entries['metadata.json'],
);
}
}
export function startBackupService(id: string) {
if (serviceInterval) {
clearInterval(serviceInterval);
}
View on GitHub (pinned to d4334cb6e6)
Solutions
- Verify the file is a real, complete zip (open it with unzip --test or check the PK header and End-of-Central-Directory record).
- Re-download or re-copy the backup and confirm the byte size matches the source.
- If restoring from cloud storage, retry the fetch — transient truncation is common.
- Check the log output (logger.log(e)) for the underlying safeUnzip reason (encryption, unsupported method) and re-create the backup without those features.
- Fall back to another backup file; the current one is unrecoverable if the central directory is corrupt.
Example fix
// before
await send('load-backup', { id: backupId }); // file was a partial download
// after
const buf = await fs.readFile(path); // verify first
if (buf.length < 4 || buf.readUInt32LE(0) !== 0x04034b50) {
throw new Error('Not a valid zip: ' + path);
}
await send('load-backup', { id: backupId }); Defensive patterns
Strategy: validation
Validate before calling
function looksLikeZip(buf: Uint8Array) {
return buf.length >= 4 && buf[0] === 0x50 && buf[1] === 0x4b; // 'PK'
}
if (!looksLikeZip(zipContent)) throw new Error('Selected file is not a valid zip backup'); Type guard
function isZipBackup(data: unknown): data is Uint8Array {
return data instanceof Uint8Array && data.length > 4 && data[0] === 0x50 && data[1] === 0x4b;
} Try / catch
try {
await send('load-backup', { id });
} catch (e) {
if (e.message === 'Error reading backup zip file') {
console.error('Backup archive is corrupt; choose another backup', e);
} else throw e;
} Prevention
- Verify backup files open with unzip --test before relying on them.
- Check byte sizes after downloads/copies to catch truncation.
- Keep multiple backup generations; never restore from a single suspect file.
- Don't rename arbitrary files to .zip and attempt restores.
When it happens
Trigger: Calling loadBackup with zipContent that is not a valid zip: truncated download, HTML error page saved as .zip, empty buffer, or an archive using an encoding safeUnzip rejects (e.g. encrypted or zip64).
Common situations: Restoring a backup copied from a failed sync; a cloud restore downloaded partially; user picks the wrong file (e.g. a .blob or metadata file) in the restore dialog; manually renamed file with .zip extension.
Related errors
- Backup zip file is missing db.sqlite or metadata.json
- Error exporting budget: ${result.error}
- zipMeta ? getUnsafeZipError(zipMeta) : error
- Could not find file: ${path}
- File name matches multiple files: ${path}
AI-assisted analysis of actualbudget/actual@d4334cb6e6 (2026-08-29).
Data as JSON: /api/errors/eb8f30ed2d88309b.
Report an issue: GitHub.