Freika/dawarich · error · ArgumentError

zip has no entries

Error message

zip has no entries

What it means

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.

Source

Thrown at app/services/archive/unzipper.rb:47

        return Result.new(kind: :not_a_zip)
      end

      return Result.new(kind: :not_a_zip) if entries.nil?

      # Single-entry-with-unsupported-extension collapses to :multi_entry so
      # the existing Imports::ZipExtractor handles the filtering rather than
      # duplicating the supported-extensions list in two places.
      if entries.size == 1 && supported_extension?(entries.first.name)
        Result.new(kind: :single_entry, entry_name: entries.first.name)
      else
        Result.new(kind: :multi_entry)
      end
    end

    def self.extract_single(path)
      ::Zip::File.open(path) do |zf|
        entry = zf.entries.reject(&:directory?).first
        raise ArgumentError, 'zip has no entries' unless entry

        ext = File.extname(entry.name)
        # Tempfile.create (not .new) returns a plain File without an
        # ObjectSpace finalizer, so the path survives GC of the File object.
        # The caller (Imports::Create) is responsible for unlinking.
        inner = Tempfile.create(['unzipped', ext], binmode: true)

        begin
          bytes_written = 0
          entry.get_input_stream do |stream|
            while (chunk = stream.read(64 * 1024))
              bytes_written += chunk.bytesize
              if bytes_written > MAX_EXTRACTED_SIZE
                inner.close
                File.unlink(inner.path) if File.exist?(inner.path)
                raise ArchiveTooLarge, "entry exceeds #{MAX_EXTRACTED_SIZE} bytes"
              end

View on GitHub (pinned to 97fad417c5)

Solutions

  1. List the archive contents before importing: unzip -l file.zip - if only directories appear, that is the cause
  2. Re-create the zip so the GPX/CSV files sit at the archive root, not inside an empty folder chain
  3. Validate client-side (file count > 0, at least one supported extension) before uploading
  4. If building archives programmatically, skip directory entries so the zip contains only files

Example fix

# before
Zip::File.open('out.zip') { |zf| zf.mkdir('gpx') } # directory-only archive

# after
Zip::File.open('out.zip') do |zf|
  zf.add('trace.gpx', 'trace.gpx') # at least one real file entry
end
Defensive patterns

Strategy: validation

Validate before calling

require 'zip'

def zip_has_file_entries?(path)
  Zip::File.open(path) { |zf| zf.entries.any? { |e| !e.directory? } }
rescue Zip::Error
  false
end

Try / catch

begin
  Archive::Unzipper.extract_single(path)
rescue ArgumentError => e
  render_error(:unprocessable_entity, 'Archive contains no files - re-zip the files without an empty folder wrapper')
end

Prevention

When it happens

Trigger: 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.

Common situations: 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.

Related errors


AI-assisted analysis of Freika/dawarich@97fad417c5 (2026-08-21). Data as JSON: /api/errors/98a8093982f7448c. Report an issue: GitHub.