spatial_features 3.11.1 → 3.11.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 41ad7ed6a989f41b717e103a1f800a60c776ecdf772d3be54dd65128ac84882c
4
- data.tar.gz: 6b741f6c962678db2edf963fdd53a87756c657476159f882af36a0bbb1685798
3
+ metadata.gz: 0d90a3143573947c3e70f74845fbd475dcddbd871df0baff29dd028d236c9833
4
+ data.tar.gz: 37bf93eb6f87120a85f2e028efe77debb91772cc3cd77289f75e59deb5d5ab4e
5
5
  SHA512:
6
- metadata.gz: da7049b48402b343a62045dd9910c334240ebfc9d02f0738c5896d3a0033330b4293badfa5e39b9837c34204d3fb221116a79810a812ffa41b3cbf05c0784367
7
- data.tar.gz: 5bf14c630adc5bf960a106e0d1ae17b4490770e62dadbcce24dfeaf0cc711ac0e77ef218266d764744a847e66e7bcd0a22cb81e153c7cf2cba1d176d2a3dc266
6
+ metadata.gz: 0fa27a478c51fccc0a7c97ecfe5214faeea64e930b8ab0643cddc2fd4e3d2570905503308c6c2f0b1c54b5384054d774e82d48a3df6044fad135ff4688f5c9ea
7
+ data.tar.gz: e07a9760f62c00fd47dbb350b4b669b067ca4a98cb8c836d7fa8d7deb0432bd6836ae1d647ee025bcf5b494d9b4dbb189174be9cfa6f90efaccbe4947e5a9cd4
data/Rakefile CHANGED
@@ -16,3 +16,5 @@ RSpec::Core::RakeTask.new(:spec) do |task|
16
16
  end
17
17
 
18
18
  task :default => :spec
19
+
20
+ Dir['tasks/*.rake'].each { |f| load f }
@@ -1,4 +1,5 @@
1
1
  require 'digest/md5'
2
+ require 'open3'
2
3
  require 'spatial_features/importers/geo_json'
3
4
 
4
5
  module SpatialFeatures
@@ -15,11 +16,9 @@ module SpatialFeatures
15
16
  private
16
17
 
17
18
  def esri_json_to_geojson(url)
18
- if URI.parse(url).relative?
19
- `ogr2ogr -t_srs EPSG:4326 -f GeoJSON /dev/stdout "#{url}"` # It is a local file path
20
- else
21
- `ogr2ogr -t_srs EPSG:4326 -f GeoJSON /dev/stdout "#{url}" OGRGeoJSON`
22
- end
19
+ args = ['ogr2ogr', '-t_srs', 'EPSG:4326', '-f', 'GeoJSON', '/dev/stdout', url]
20
+ args << 'OGRGeoJSON' unless URI.parse(url).relative? # A relative URL is a local file path
21
+ Open3.capture2(*args).first
23
22
  end
24
23
  end
25
24
  end
@@ -1,5 +1,6 @@
1
1
  require 'ostruct'
2
2
  require 'digest/md5'
3
+ require 'open3'
3
4
 
4
5
  module SpatialFeatures
5
6
  module Importers
@@ -55,8 +56,11 @@ module SpatialFeatures
55
56
  if proj4 == PROJ4_4326
56
57
  data[:geog] = wkt
57
58
  else
58
- data[:geog] = ActiveRecord::Base.connection.select_value <<-SQL
59
- SELECT ST_Transform(ST_GeomFromText('#{wkt}'), '#{proj4}', 4326) AS geog
59
+ # `proj4` is read out of the uploaded archive's .prj, so it is quoted before it
60
+ # reaches the statement.
61
+ conn = ActiveRecord::Base.connection
62
+ data[:geog] = conn.select_value <<-SQL
63
+ SELECT ST_Transform(ST_GeomFromText(#{conn.quote(wkt)}), #{conn.quote(proj4)}, 4326) AS geog
60
64
  SQL
61
65
  end
62
66
 
@@ -90,18 +94,27 @@ module SpatialFeatures
90
94
  end
91
95
 
92
96
  # Use OGR2OGR to reproject into EPSG:4326 so we can skip the reprojection step per-feature
97
+ #
98
+ # @note Both the projection and the path come from the uploaded archive, so they are
99
+ # passed as an argv list and no shell parses them. Assembling a command string here
100
+ # would let an uploader run commands of their choosing.
93
101
  def project_to_4326(file_path)
94
102
  output_path = Tempfile.create([::File.basename(file_path, '.shp') + '_epsg_4326_', '.shp']) { |file| file.path }
95
103
  return unless (proj4 = proj4_from_file(file_path))
96
- return unless system("ogr2ogr -s_srs '#{proj4}' -t_srs EPSG:4326 '#{output_path}' '#{file_path}'")
104
+ return unless system('ogr2ogr', '-s_srs', proj4, '-t_srs', 'EPSG:4326', output_path, file_path)
97
105
  return ::File.open(output_path)
98
106
  end
99
107
 
108
+ # Returns the PROJ.4 projection string GDAL reads out of the file's .prj, or nil when
109
+ # it can't determine one. Returns nil when `gdalsrsinfo` is not installed, which
110
+ # `proj4_projection` reports.
100
111
  def proj4_from_file(file_path)
101
112
  # Sanitize: "'+proj=utm +zone=11 +datum=NAD83 +units=m +no_defs '\n" and lately
102
113
  # "+proj=utm +zone=11 +datum=NAD83 +units=m +no_defs \n" to
103
114
  # "+proj=utm +zone=11 +datum=NAD83 +units=m +no_defs"
104
- `gdalsrsinfo "#{file_path}" -o proj4`.strip.remove(/^'|'$/).presence
115
+ Open3.capture2('gdalsrsinfo', file_path, '-o', 'proj4').first.strip.remove(/^'|'$/).presence
116
+ rescue Errno::ENOENT
117
+ nil
105
118
  end
106
119
 
107
120
  # a zip archive may contain multiple SHP files
@@ -1,4 +1,5 @@
1
1
  require 'fileutils'
2
+ require 'pathname'
2
3
 
3
4
  module SpatialFeatures
4
5
  module Unzip
@@ -51,16 +52,26 @@ module SpatialFeatures
51
52
  end
52
53
  end
53
54
 
55
+ # Extracts the archive's entries and returns their paths, skipping entries that a name
56
+ # cannot place inside `tmpdir`.
57
+ #
58
+ # @param tmpdir [String] where to extract to. Must already exist, since the destination
59
+ # is resolved before anything is written. Defaults to a fresh temporary directory.
54
60
  def self.extract(file_path, tmpdir: nil, downcase: false)
55
61
  tmpdir ||= Dir.mktmpdir
62
+ root = Pathname.new(tmpdir).realpath
63
+
56
64
  [].tap do |paths|
57
65
  entries(file_path).each do |entry|
58
66
  next if entry.name =~ IGNORED_ENTRY_PATHS
67
+ next if entry.symlink?
59
68
 
60
69
  output_filename = entry.name
61
70
  output_filename = output_filename.downcase if downcase
62
71
 
63
- path = "#{tmpdir}/#{output_filename}"
72
+ path = contained_path(root, output_filename)
73
+ next unless path
74
+
64
75
  directory = File.dirname(path)
65
76
  basename = File.basename(path)
66
77
 
@@ -72,6 +83,24 @@ module SpatialFeatures
72
83
  end
73
84
  end
74
85
 
86
+ # Returns where an entry of this name lands under `root`, or nil when the name would place
87
+ # it outside. Entry names are stored in the archive verbatim, so they can carry `..`
88
+ # segments that climb out of the directory we extract into.
89
+ #
90
+ # @note `root` must already be a real path, and `::extract` skips symlink entries, so
91
+ # nothing beneath it is a symlink. `cleanpath` resolves `..` lexically, so a symlink
92
+ # under `root` would let an entry name walk back out of the directory unnoticed.
93
+ def self.contained_path(root, output_filename)
94
+ path = root.join(output_filename).cleanpath
95
+
96
+ return unless path.to_s.start_with?("#{root}#{File::SEPARATOR}")
97
+
98
+ # `cleanpath` drops the trailing separator that marks a directory entry, which
99
+ # `PathNotFound#extensions` reads to tell directories from the files it reports.
100
+ output_filename.end_with?(File::SEPARATOR) ? "#{path}#{File::SEPARATOR}" : path.to_s
101
+ end
102
+ private_class_method :contained_path
103
+
75
104
  def self.names(file_path)
76
105
  entries(file_path).collect(&:name)
77
106
  end
@@ -1,3 +1,3 @@
1
1
  module SpatialFeatures
2
- VERSION = "3.11.1"
2
+ VERSION = "3.11.2"
3
3
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: spatial_features
3
3
  version: !ruby/object:Gem::Version
4
- version: 3.11.1
4
+ version: 3.11.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ryan Wallace