Module: SpatialFeatures::Unzip

Defined in:
lib/spatial_features/unzip.rb

Defined Under Namespace

Classes: PathNotFound

Constant Summary collapse

IGNORED_ENTRY_PATHS =

paths containing '__macosx' or beginning with a '.'

/(\A|\/)(__macosx|\.)/i.freeze
NESTED_ARCHIVE_PATTERN =

Archives that may themselves hold the file we're looking for. A KMZ is a ZIP with a KML inside it, and proponents routinely forward a ZIP of per-layer ZIPs rather than the layers themselves.

/\.(zip|kmz)$/i.freeze
MAX_NESTING_DEPTH =

How many times we'll unwrap a nested archive before giving up. Two levels covers every case we've seen in practice (a ZIP of shapefile ZIPs, a ZIP holding a KMZ) while bounding the work a deeply nested archive can cost us.

2

Class Method Summary collapse

Class Method Details

.entries(file_path) ⇒ Object



79
80
81
# File 'lib/spatial_features/unzip.rb', line 79

def self.entries(file_path)
  Zip::File.open(File.path(file_path))
end

.extract(file_path, tmpdir: nil, downcase: false) ⇒ Object



54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
# File 'lib/spatial_features/unzip.rb', line 54

def self.extract(file_path, tmpdir: nil, downcase: false)
  tmpdir ||= Dir.mktmpdir
  [].tap do |paths|
    entries(file_path).each do |entry|
      next if entry.name =~ IGNORED_ENTRY_PATHS

      output_filename = entry.name
      output_filename = output_filename.downcase if downcase

      path = "#{tmpdir}/#{output_filename}"
      directory = File.dirname(path)
      basename = File.basename(path)

      FileUtils.mkdir_p(directory)
      entry.extract(basename, destination_directory: directory)

      paths << path
    end
  end
end

.is_zip?(file) ⇒ Boolean

Returns:

  • (Boolean)


83
84
85
86
87
88
89
# File 'lib/spatial_features/unzip.rb', line 83

def self.is_zip?(file)
  zip = file.readline.start_with?('PK')
  file.rewind
  return zip
rescue EOFError
  return false
end

.matching_paths(paths, find) ⇒ Object



33
34
35
# File 'lib/spatial_features/unzip.rb', line 33

def self.matching_paths(paths, find)
  paths.select {|path| find.any? {|pattern| path.index(pattern) } }
end

.names(file_path) ⇒ Object



75
76
77
# File 'lib/spatial_features/unzip.rb', line 75

def self.names(file_path)
  entries(file_path).collect(&:name)
end

.paths(file_path, find: nil, depth: 0, **extract_options) ⇒ Object



18
19
20
21
22
23
24
25
26
27
28
29
30
31
# File 'lib/spatial_features/unzip.rb', line 18

def self.paths(file_path, find: nil, depth: 0, **extract_options)
  paths = extract(file_path, **extract_options)

  if find = Array.wrap(find).presence
    matches = matching_paths(paths, find)
    # Only unwrap nested archives once the archive has yielded nothing usable, so
    # archives that already import keep their current behaviour exactly.
    matches = paths_in_nested_archives(paths, find: find, depth: depth, **extract_options) if matches.empty?
    raise(PathNotFound.new("Archive did not contain a file matching #{find}", paths)) if matches.empty?
    paths = matches
  end

  return Array(paths)
end

.paths_in_nested_archives(paths, find:, depth:, tmpdir: nil, **extract_options) ⇒ Object

Unwrap any archives the archive itself contained and search those instead. Each one extracts into its own directory so identically named entries (doc.kml in several KMZs) don't overwrite one another, and so a shapefile's sibling components stay beside it for Validation.validate_shapefile!.



41
42
43
44
45
46
47
48
49
50
51
52
# File 'lib/spatial_features/unzip.rb', line 41

def self.paths_in_nested_archives(paths, find:, depth:, tmpdir: nil, **extract_options)
  return [] unless depth < MAX_NESTING_DEPTH

  paths.grep(NESTED_ARCHIVE_PATTERN).flat_map do |archive|
    begin
      paths(archive, find: find, depth: depth + 1,
                     tmpdir: Dir.mktmpdir(nil, File.dirname(archive)), **extract_options)
    rescue PathNotFound, Zip::Error
      [] # This nested archive held nothing we can use; the others may still.
    end
  end
end