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

  1. Verify the file is a real, complete zip (open it with unzip --test or check the PK header and End-of-Central-Directory record).
  2. Re-download or re-copy the backup and confirm the byte size matches the source.
  3. If restoring from cloud storage, retry the fetch — transient truncation is common.
  4. Check the log output (logger.log(e)) for the underlying safeUnzip reason (encryption, unsupported method) and re-create the backup without those features.
  5. 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

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


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