{"record":{"id":"98a8093982f7448c","repo":"Freika/dawarich","slug":"zip-has-no-entries","errorCode":null,"errorMessage":"zip has no entries","messagePattern":"zip has no entries","errorType":"exception","errorClass":"ArgumentError","httpStatus":null,"severity":"error","filePath":"app/services/archive/unzipper.rb","lineNumber":47,"sourceCode":"        return Result.new(kind: :not_a_zip)\n      end\n\n      return Result.new(kind: :not_a_zip) if entries.nil?\n\n      # Single-entry-with-unsupported-extension collapses to :multi_entry so\n      # the existing Imports::ZipExtractor handles the filtering rather than\n      # duplicating the supported-extensions list in two places.\n      if entries.size == 1 && supported_extension?(entries.first.name)\n        Result.new(kind: :single_entry, entry_name: entries.first.name)\n      else\n        Result.new(kind: :multi_entry)\n      end\n    end\n\n    def self.extract_single(path)\n      ::Zip::File.open(path) do |zf|\n        entry = zf.entries.reject(&:directory?).first\n        raise ArgumentError, 'zip has no entries' unless entry\n\n        ext = File.extname(entry.name)\n        # Tempfile.create (not .new) returns a plain File without an\n        # ObjectSpace finalizer, so the path survives GC of the File object.\n        # The caller (Imports::Create) is responsible for unlinking.\n        inner = Tempfile.create(['unzipped', ext], binmode: true)\n\n        begin\n          bytes_written = 0\n          entry.get_input_stream do |stream|\n            while (chunk = stream.read(64 * 1024))\n              bytes_written += chunk.bytesize\n              if bytes_written > MAX_EXTRACTED_SIZE\n                inner.close\n                File.unlink(inner.path) if File.exist?(inner.path)\n                raise ArchiveTooLarge, \"entry exceeds #{MAX_EXTRACTED_SIZE} bytes\"\n              end\n","sourceCodeStart":29,"sourceCodeEnd":65,"githubUrl":"https://github.com/Freika/dawarich/blob/97fad417c5a11b0eb11157890635e015723a2e97/app/services/archive/unzipper.rb#L29-L65","documentation":"Archive::Unzipper.extract_single opens the uploaded zip, rejects directory entries, and takes the first real file; if none exists it raises ArgumentError 'zip has no entries'. This runs on the single-entry path chosen during classification, so the archive passed the zip-integrity checks but contains only directory entries (or is empty). The import flow surfaces this as a user-facing import failure.","triggerScenarios":"Uploading a zip that only contains folders (e.g. macOS Finder's 'Compress' on an empty folder, or zipping a directory whose files were filtered out), a zip whose single entry is a directory entry like 'gpx/', or an empty archive created by 'zip empty.zip' with no inputs.","commonSituations":"Users zipping a wrapper folder instead of its contents, automated export tools emitting directory-only archives, interrupted zip creation leaving structural entries but no files, nested zips where the outer layer was stripped and only folder markers remain.","solutions":["List the archive contents before importing: unzip -l file.zip - if only directories appear, that is the cause","Re-create the zip so the GPX/CSV files sit at the archive root, not inside an empty folder chain","Validate client-side (file count > 0, at least one supported extension) before uploading","If building archives programmatically, skip directory entries so the zip contains only files"],"exampleFix":"# before\nZip::File.open('out.zip') { |zf| zf.mkdir('gpx') } # directory-only archive\n\n# after\nZip::File.open('out.zip') do |zf|\n  zf.add('trace.gpx', 'trace.gpx') # at least one real file entry\nend","handlingStrategy":"validation","validationCode":"require 'zip'\n\ndef zip_has_file_entries?(path)\n  Zip::File.open(path) { |zf| zf.entries.any? { |e| !e.directory? } }\nrescue Zip::Error\n  false\nend","typeGuard":null,"tryCatchPattern":"begin\n  Archive::Unzipper.extract_single(path)\nrescue ArgumentError => e\n  render_error(:unprocessable_entity, 'Archive contains no files - re-zip the files without an empty folder wrapper')\nend","preventionTips":["Check entry counts client-side (JSZip or equivalent) before uploading","Tell users to zip the files themselves, not a parent folder, in import documentation","Treat directory-only archives as a user-input problem: fail with an actionable message instead of a generic import error"],"tags":["zip","import","archive","validation","ruby"],"backgroundTag":"empty-zip-archive","analyzedSha":"97fad417c5a11b0eb11157890635e015723a2e97","analyzedAt":"2026-08-21T17:04:17.778Z","schemaVersion":2},"datasetVersion":"2026-08-21T18:17:14.833Z"}