{"record":{"id":"63c7661e7ef2e776","repo":"actualbudget/actual","slug":"invalid-zip-file","errorCode":"invalid-zip-file","errorMessage":"invalid-zip-file","messagePattern":"invalid-zip-file","errorType":"error_code","errorClass":"FileDownloadError","httpStatus":null,"severity":"error","filePath":"packages/loot-core/src/server/cloud-storage.ts","lineNumber":240,"sourceCode":"  const entryNames = Object.keys(entries);\n  const dbDirs = entryNames\n    .filter(name => name === 'db.sqlite' || name.endsWith('/db.sqlite'))\n    .map(name => name.slice(0, -'db.sqlite'.length));\n  const metaDirs = entryNames\n    .filter(name => name === 'metadata.json' || name.endsWith('/metadata.json'))\n    .map(name => name.slice(0, -'metadata.json'.length));\n\n  // Both files must come from the same directory: prefer the archive root,\n  // otherwise there must be exactly one directory containing both.\n  const sharedDirs = dbDirs.filter(dir => metaDirs.includes(dir));\n  const dir = sharedDirs.includes('')\n    ? ''\n    : sharedDirs.length === 1\n      ? sharedDirs[0]\n      : null;\n\n  if (dir == null) {\n    throw FileDownloadError('invalid-zip-file');\n  }\n\n  const entryName = dir + 'db.sqlite';\n  const metaEntryName = dir + 'metadata.json';\n\n  const dbContent = Buffer.from(entries[entryName]);\n  const metaContent = Buffer.from(entries[metaEntryName]);\n\n  let meta;\n  try {\n    meta = JSON.parse(metaContent.toString('utf8'));\n  } catch {\n    throw FileDownloadError('invalid-meta-file');\n  }\n\n  // Update the metadata. The stored file on the server might be\n  // out-of-date with a few keys\n  meta = {","sourceCodeStart":222,"sourceCodeEnd":258,"githubUrl":"https://github.com/actualbudget/actual/blob/d4334cb6e6123f4d3bcea1ad6166608884c7e658/packages/loot-core/src/server/cloud-storage.ts#L222-L258","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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"],"exampleFix":"// before: importing an arbitrary zip\ncatch (e) { /* invalid-zip-file */ }\n// after: validate structure first\nconst names = Object.keys(entries);\nconst ok = ['db.sqlite','metadata.json'].every(f => names.includes(f));\nif (!ok) throw new Error('archive must contain db.sqlite and metadata.json at root');","handlingStrategy":"validation","validationCode":"async function assertImportableZip(buffer) {\n  const zip = await loadZip(buffer);\n  const names = Object.keys(zip.entries);\n  const dbDirs = names.filter(n => n === 'db.sqlite' || n.endsWith('/db.sqlite')).map(n => n.slice(0, -'db.sqlite'.length));\n  const metaDirs = names.filter(n => n === 'metadata.json' || n.endsWith('/metadata.json')).map(n => n.slice(0, -'metadata.json'.length));\n  const shared = dbDirs.filter(d => metaDirs.includes(d));\n  if (!(shared.includes('') || shared.length === 1)) throw new Error('invalid-zip-file');\n}","typeGuard":"function isImportableArchive(entries) {\n  const names = Object.keys(entries);\n  const has = f => names.some(n => n === f || n.endsWith('/' + f));\n  return has('db.sqlite') && has('metadata.json');\n}","tryCatchPattern":"try {\n  await importActual(buffer);\n} catch (e) {\n  if (e instanceof FileDownloadError && e.reason === 'invalid-zip-file') {\n    console.error('Archive lacks db.sqlite + metadata.json; use an Actual export.');\n  } else throw e;\n}","preventionTips":["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"],"tags":["zip","file-import","data-integrity","cloud-sync"],"backgroundTag":"invalid-archive-structure","analyzedSha":"d4334cb6e6123f4d3bcea1ad6166608884c7e658","analyzedAt":"2026-08-29T01:02:11.213Z","schemaVersion":2},"datasetVersion":"2026-08-29T02:17:18.158Z"}