{"record":{"id":"ef17a86d1909f5f4","repo":"actualbudget/actual","slug":"not-zip-file","errorCode":"not-zip-file","errorMessage":"not-zip-file","messagePattern":"not-zip-file","errorType":"error_code","errorClass":"FileDownloadError","httpStatus":null,"severity":"error","filePath":"packages/loot-core/src/server/cloud-storage.ts","lineNumber":220,"sourceCode":"  if (\n    availableMemory != null &&\n    entries['db.sqlite'].length > availableMemory\n  ) {\n    warnings.push('may-exceed-available-memory');\n  }\n\n  return { data: Buffer.from(zipped), warnings };\n}\n\nexport async function importBuffer(fileData, buffer) {\n  let entries;\n  try {\n    entries = safeUnzip(buffer);\n  } catch (e) {\n    if (e instanceof UnsafeZipError) {\n      throw FileDownloadError('zip-too-large', e.meta);\n    }\n    throw FileDownloadError('not-zip-file');\n  }\n  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","sourceCodeStart":202,"sourceCodeEnd":238,"githubUrl":"https://github.com/actualbudget/actual/blob/d4334cb6e6123f4d3bcea1ad6166608884c7e658/packages/loot-core/src/server/cloud-storage.ts#L202-L238","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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."],"exampleFix":null,"handlingStrategy":"validation","validationCode":"const fs = require('fs');\nconst buf = fs.readFileSync(zipPath);\nif (buf.length < 4 || buf.readUInt32BE(0) !== 0x504b0304) {\n  throw new Error('File is not a zip archive');\n}","typeGuard":"function isZipBuffer(buf) {\n  return buf.length > 4 && buf.readUInt32BE(0) === 0x504b0304;\n}","tryCatchPattern":"try {\n  await actual.importActual(zipPath);\n} catch (e) {\n  if (e.code === 'not-zip-file') {\n    console.error('Not a valid zip; re-export the budget file');\n  } else throw e;\n}","preventionTips":["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"],"tags":["file-import","zip","format","validation"],"backgroundTag":"invalid-file-format","analyzedSha":"d4334cb6e6123f4d3bcea1ad6166608884c7e658","analyzedAt":"2026-08-29T01:02:11.213Z","schemaVersion":2},"datasetVersion":"2026-08-29T02:17:18.158Z"}