iostreams 2.1.0 → 2.2.0
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 +4 -4
- data/docs/config.md +13 -9
- data/docs/copy_files.md +20 -2
- data/docs/extensions.md +18 -4
- data/docs/formats.md +644 -16
- data/docs/path.md +18 -2
- data/docs/streams.md +54 -9
- data/docs/tutorial.md +14 -2
- data/docs/upgrading.md +470 -23
- data/lib/io_streams/builder/deprecations.rb +250 -0
- data/lib/io_streams/builder.rb +256 -39
- data/lib/io_streams/bzip2/reader.rb +18 -2
- data/lib/io_streams/bzip2/writer.rb +11 -2
- data/lib/io_streams/deprecation/methods.rb +51 -0
- data/lib/io_streams/deprecation/once.rb +27 -0
- data/lib/io_streams/deprecation/option_names.rb +26 -0
- data/lib/io_streams/deprecation/words.rb +42 -0
- data/lib/io_streams/deprecation.rb +259 -0
- data/lib/io_streams/encode/reader.rb +54 -3
- data/lib/io_streams/encode/writer.rb +19 -2
- data/lib/io_streams/encode.rb +99 -0
- data/lib/io_streams/errors.rb +51 -0
- data/lib/io_streams/gzip/reader.rb +11 -0
- data/lib/io_streams/io_streams.rb +365 -35
- data/lib/io_streams/line/reader.rb +2 -1
- data/lib/io_streams/line/utf8_reader.rb +23 -0
- data/lib/io_streams/path.rb +125 -35
- data/lib/io_streams/paths/file.rb +72 -9
- data/lib/io_streams/paths/http.rb +48 -2
- data/lib/io_streams/paths/matcher.rb +6 -3
- data/lib/io_streams/paths/s3.rb +180 -9
- data/lib/io_streams/paths/sftp.rb +47 -3
- data/lib/io_streams/paths.rb +23 -0
- data/lib/io_streams/pgp/reader.rb +18 -0
- data/lib/io_streams/pgp/writer.rb +16 -0
- data/lib/io_streams/pgp.rb +16 -2
- data/lib/io_streams/reader.rb +44 -0
- data/lib/io_streams/record/reader.rb +59 -68
- data/lib/io_streams/record/writer.rb +17 -10
- data/lib/io_streams/row/header_row.rb +21 -0
- data/lib/io_streams/row/reader.rb +52 -15
- data/lib/io_streams/row/writer.rb +64 -13
- data/lib/io_streams/schema/columns.rb +456 -0
- data/lib/io_streams/schema/constraints.rb +99 -0
- data/lib/io_streams/schema/field.rb +967 -0
- data/lib/io_streams/schema/json_number.rb +56 -0
- data/lib/io_streams/schema/keys.rb +78 -0
- data/lib/io_streams/schema/pattern.rb +257 -0
- data/lib/io_streams/schema/source.rb +31 -0
- data/lib/io_streams/schema.rb +616 -0
- data/lib/io_streams/stream.rb +836 -67
- data/lib/io_streams/symmetric_encryption/reader.rb +8 -0
- data/lib/io_streams/tabular/header.rb +29 -0
- data/lib/io_streams/tabular/header_mapping.rb +332 -0
- data/lib/io_streams/tabular/mapping.rb +165 -0
- data/lib/io_streams/tabular/parser/array.rb +5 -0
- data/lib/io_streams/tabular/parser/base.rb +24 -0
- data/lib/io_streams/tabular/parser/fixed.rb +197 -0
- data/lib/io_streams/tabular/parser/fixed_fields.rb +160 -0
- data/lib/io_streams/tabular/parser/hash.rb +10 -0
- data/lib/io_streams/tabular/parser/json.rb +17 -0
- data/lib/io_streams/tabular/parser/json_fields.rb +132 -0
- data/lib/io_streams/tabular/schema_mapping.rb +361 -0
- data/lib/io_streams/tabular.rb +344 -30
- data/lib/io_streams/utils.rb +441 -6
- data/lib/io_streams/version.rb +1 -1
- data/lib/io_streams/writer.rb +27 -0
- data/lib/io_streams/xlsx/cells.rb +127 -0
- data/lib/io_streams/xlsx/reader.rb +56 -0
- data/lib/io_streams/xlsx/schema_reader.rb +90 -0
- data/lib/io_streams/xlsx/v3_writer.rb +16 -0
- data/lib/io_streams/zip/reader.rb +7 -0
- data/lib/io_streams/zip/writer.rb +24 -2
- data/lib/iostreams.rb +21 -17
- metadata +28 -2
data/lib/io_streams/path.rb
CHANGED
|
@@ -1,10 +1,6 @@
|
|
|
1
1
|
module IOStreams
|
|
2
2
|
class Path < IOStreams::Stream
|
|
3
|
-
|
|
4
|
-
SENSITIVE_OPTION = /pass(phrase|word)|secret/i
|
|
5
|
-
private_constant :SENSITIVE_OPTION
|
|
6
|
-
|
|
7
|
-
attr_accessor :path
|
|
3
|
+
attr_reader :path
|
|
8
4
|
|
|
9
5
|
def initialize(path)
|
|
10
6
|
raise(ArgumentError, "Path cannot be nil") if path.nil?
|
|
@@ -13,20 +9,30 @@ module IOStreams
|
|
|
13
9
|
@path = path.frozen? ? path : path.dup.freeze
|
|
14
10
|
@io_stream = nil
|
|
15
11
|
@builder = nil
|
|
12
|
+
@root_name = nil
|
|
16
13
|
end
|
|
17
14
|
|
|
15
|
+
# A copy of a root path is not a root path, see #root!, so that it can be changed.
|
|
16
|
+
def initialize_copy(source)
|
|
17
|
+
super
|
|
18
|
+
@root_name = nil
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# Returns [IOStreams::Path] a new path with the elements joined to this path, created with the same arguments as
|
|
22
|
+
# this path, such as the credentials of an SFTP path, but without the streams, options, format and schema set on
|
|
23
|
+
# this one. Without any elements it returns this path itself.
|
|
24
|
+
#
|
|
18
25
|
# If elements already contains the current path then it is used as is without
|
|
19
26
|
# adding the current path for a second time
|
|
20
27
|
def join(*elements)
|
|
21
28
|
return self if elements.empty?
|
|
22
29
|
|
|
23
30
|
elements = elements.collect(&:to_s)
|
|
24
|
-
relative = ::
|
|
31
|
+
relative = Utils::PathName.join(*elements)
|
|
25
32
|
|
|
26
|
-
new_path
|
|
27
|
-
new_path.
|
|
28
|
-
new_path.
|
|
29
|
-
new_path
|
|
33
|
+
new_path = dup
|
|
34
|
+
new_path.reset_builder
|
|
35
|
+
new_path.replace_path(contains?(relative) ? relative : Utils::PathName.join(path, relative))
|
|
30
36
|
end
|
|
31
37
|
|
|
32
38
|
def relative?
|
|
@@ -103,18 +109,6 @@ module IOStreams
|
|
|
103
109
|
raise NotImplementedError
|
|
104
110
|
end
|
|
105
111
|
|
|
106
|
-
# Cleanup an incomplete write to the target "file" if the copy fails.
|
|
107
|
-
# rubocop:disable-next Lint/SuppressedException
|
|
108
|
-
def copy_from(source, **args)
|
|
109
|
-
super
|
|
110
|
-
rescue StandardError => e
|
|
111
|
-
begin
|
|
112
|
-
delete
|
|
113
|
-
rescue NotImplementedError
|
|
114
|
-
end
|
|
115
|
-
raise(e)
|
|
116
|
-
end
|
|
117
|
-
|
|
118
112
|
# Moves the file by copying it to the new path and then deleting the current path.
|
|
119
113
|
# Returns [IOStreams::Path] the target path.
|
|
120
114
|
#
|
|
@@ -128,7 +122,8 @@ module IOStreams
|
|
|
128
122
|
target
|
|
129
123
|
end
|
|
130
124
|
|
|
131
|
-
# Returns [IOStreams::Path] the directory for this file
|
|
125
|
+
# Returns [IOStreams::Path] the directory for this file, a new path created with the same arguments as this path,
|
|
126
|
+
# but without the streams, options, format and schema set on this one, see #join.
|
|
132
127
|
#
|
|
133
128
|
# If `path` does not include a directory name then "." is returned.
|
|
134
129
|
#
|
|
@@ -139,10 +134,9 @@ module IOStreams
|
|
|
139
134
|
# IOStreams.path("test").directory #=> "."
|
|
140
135
|
# IOStreams.path(".profile").directory #=> "."
|
|
141
136
|
def directory
|
|
142
|
-
new_path
|
|
143
|
-
new_path.
|
|
144
|
-
new_path.
|
|
145
|
-
new_path
|
|
137
|
+
new_path = dup
|
|
138
|
+
new_path.reset_builder
|
|
139
|
+
new_path.replace_path(Utils::PathName.dirname(path))
|
|
146
140
|
end
|
|
147
141
|
|
|
148
142
|
# When path is a file, deletes this file.
|
|
@@ -209,17 +203,85 @@ module IOStreams
|
|
|
209
203
|
path == other.path
|
|
210
204
|
end
|
|
211
205
|
|
|
206
|
+
# Returns [Array<Symbol>] the options for creating a path of this class whose values must not be displayed,
|
|
207
|
+
# such as a password, see .redact_options. A path class with sensitive options overrides it.
|
|
208
|
+
def self.sensitive_option_names
|
|
209
|
+
[]
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
# Returns [Hash] the supplied options for creating a path of this class, such as `password:`, with the value of
|
|
213
|
+
# each sensitive option replaced with "[FILTERED]", so that they can be displayed, for example by an application
|
|
214
|
+
# that stores them to create the path later. See .sensitive_option_names and `IOStreams::Utils.redact_options`.
|
|
215
|
+
def self.redact_options(options)
|
|
216
|
+
Utils.redact_options(options, sensitive_option_names)
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
# Returns [String|nil] the deprecation warning for the supplied name or url of a path of this class, as the
|
|
220
|
+
# application supplies it to `IOStreams.path`, when IOStreams v3.0 reads it as another file or url, which says what
|
|
221
|
+
# to supply instead, so that it names the same one on both, or nil when v3.0 reads it the same way. A path class
|
|
222
|
+
# whose names v3.0 reads differently overrides it.
|
|
223
|
+
def self.name_deprecation(_name)
|
|
224
|
+
nil
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
# Returns [String|nil] the deprecation warning for the supplied name or url, see .name_deprecation, only when
|
|
228
|
+
# IOStreams v3.0 reads it as another location, which the allowed paths compare, see `IOStreams.add_allowed_path`,
|
|
229
|
+
# rather than only spelling its url another way. A path class whose names v3.0 reads as another location overrides
|
|
230
|
+
# it.
|
|
231
|
+
def self.location_deprecation(_name)
|
|
232
|
+
nil
|
|
233
|
+
end
|
|
234
|
+
|
|
235
|
+
# Returns [true|false] whether the url has no path, such as `sftp://host`, or `https://host?a=1`, which IOStreams
|
|
236
|
+
# v3.0 reads with the path `/`, see `IOStreams::Paths::SFTP.name_deprecation`. Its text is checked rather than
|
|
237
|
+
# parsed, so that a pattern with characters that a url cannot hold, such as braces, can be checked too.
|
|
238
|
+
def self.url_without_path?(url)
|
|
239
|
+
url.match?(%r{\A[a-z][a-z0-9+.-]*://[^/?#]*(?:[?#]|\z)}i)
|
|
240
|
+
end
|
|
241
|
+
private_class_method :url_without_path?
|
|
242
|
+
|
|
243
|
+
# Shows the full name of the path, see #display_name, without any credentials, and the options of its streams
|
|
244
|
+
# without the values of sensitive options, such as a passphrase, see `IOStreams.redact_stream_options`.
|
|
212
245
|
def inspect
|
|
213
|
-
str = "#<#{self.class.name}:#{
|
|
246
|
+
str = "#<#{self.class.name}:#{display_name}"
|
|
214
247
|
str << " @builder=#{redact(builder.streams).inspect}" if builder.streams
|
|
215
248
|
str << " @options=#{redact(builder.options).inspect}" if builder.options
|
|
216
249
|
str << " pipeline=#{redact(pipeline).inspect}>"
|
|
217
250
|
end
|
|
218
251
|
|
|
252
|
+
# Returns [String] the full name of this path, see #to_s, without any credentials, so that it can be logged
|
|
253
|
+
# or displayed. A user name, password or query in an SFTP or HTTP url is removed, since it can hold
|
|
254
|
+
# credentials, so unlike #to_s it cannot be used to create the path again. Each byte of a name that is not
|
|
255
|
+
# valid UTF-8 is shown as `\xHH`, see `IOStreams::Utils.display_text`.
|
|
256
|
+
#
|
|
257
|
+
# Example:
|
|
258
|
+
# IOStreams.path("sftp://jack:secret@example.org/data/a.csv").display_name
|
|
259
|
+
# # => "sftp://example.org/data/a.csv"
|
|
260
|
+
def display_name
|
|
261
|
+
Utils.display_text(to_s)
|
|
262
|
+
end
|
|
263
|
+
|
|
264
|
+
# Deprecated: a path can be used by many objects at once, such as a root path that file names are joined to, so
|
|
265
|
+
# changing it in place changes it for all of them. Use #join or #directory, which return a new path, without the
|
|
266
|
+
# streams, options, format and schema set on this one, or `IOStreams.path` with the new name. Reports a
|
|
267
|
+
# deprecation warning, see `IOStreams::Deprecation`.
|
|
268
|
+
def path=(path)
|
|
269
|
+
Deprecation.warn("#path= is deprecated, use #join or #directory, which return a new path, or IOStreams.path " \
|
|
270
|
+
"with the new name")
|
|
271
|
+
@path = path
|
|
272
|
+
end
|
|
273
|
+
|
|
219
274
|
protected
|
|
220
275
|
|
|
276
|
+
# Returns [self] after setting its path in place, for IOStreams itself, see #path=.
|
|
277
|
+
def replace_path(path)
|
|
278
|
+
@path = path
|
|
279
|
+
self
|
|
280
|
+
end
|
|
281
|
+
|
|
221
282
|
# Raises [IOStreams::Errors::AccessDenied] when allowed paths have been added, see `IOStreams.add_allowed_path`,
|
|
222
|
-
# and this path is not within any of them
|
|
283
|
+
# and this path is not within any of them, before #reader, #writer, `Stream#internal_reader` and
|
|
284
|
+
# `Stream#internal_writer` open it.
|
|
223
285
|
def authorize!
|
|
224
286
|
return if IOStreams.allowed_paths.empty? || (@permitted_path && @permitted_path == path)
|
|
225
287
|
|
|
@@ -275,7 +337,7 @@ module IOStreams
|
|
|
275
337
|
def allowed_child?(child)
|
|
276
338
|
return true if child.allowed?
|
|
277
339
|
|
|
278
|
-
IOStreams.logger&.warn("Skipping #{child} since it is not within any of the allowed paths")
|
|
340
|
+
IOStreams.logger&.warn("Skipping #{child.display_name} since it is not within any of the allowed paths")
|
|
279
341
|
false
|
|
280
342
|
end
|
|
281
343
|
|
|
@@ -286,13 +348,41 @@ module IOStreams
|
|
|
286
348
|
self
|
|
287
349
|
end
|
|
288
350
|
|
|
351
|
+
# Makes this path the root path of the supplied name, which `IOStreams.add_root` adds, and the whole process shares,
|
|
352
|
+
# so that changing it in place reports a deprecation warning, see #warn_change. A copy of it is not a root path, see
|
|
353
|
+
# #initialize_copy.
|
|
354
|
+
# Returns self
|
|
355
|
+
def root!(name)
|
|
356
|
+
@root_name = name
|
|
357
|
+
self
|
|
358
|
+
end
|
|
359
|
+
|
|
360
|
+
# Returns [true|false] whether the streams of this path read a regular local file in v3.0: the file itself, or the
|
|
361
|
+
# temp file that it is downloaded into, as v2.2 reads, unless v3.0 reads it from another IO, see #v3_source, such as
|
|
362
|
+
# an S3 object, see `IOStreams::Stream#v3_reads_file?`.
|
|
363
|
+
def v3_reads_file?
|
|
364
|
+
v3_source.nil?
|
|
365
|
+
end
|
|
366
|
+
|
|
367
|
+
# Changing a root path in place, see #root!, such as with #stream, is deprecated, since it changes the root for the
|
|
368
|
+
# whole process. v3.0 keeps its own frozen copy of the root instead, so that changing a path no longer changes the
|
|
369
|
+
# root: the path that `IOStreams.add_root` returns is that copy, so changing it raises FrozenError, while a path
|
|
370
|
+
# supplied to `IOStreams.add_root` is not, and `IOStreams.root`, `IOStreams.roots` and `IOStreams.join` without
|
|
371
|
+
# elements return copies of it, so changing them changes only the copy. A copy of the root, or a path joined to it,
|
|
372
|
+
# can be changed in either version, which the warning names for the root that this path is.
|
|
373
|
+
def warn_change(method_name)
|
|
374
|
+
return unless @root_name
|
|
375
|
+
|
|
376
|
+
root, join = @root_name == :default ? ["", ""] : ["(#{@root_name.inspect})", ", root: #{@root_name.inspect}"]
|
|
377
|
+
Deprecation.warn("#{method_name} is deprecated on a root path, which the whole process shares, since v3.0 " \
|
|
378
|
+
"keeps its own frozen copy of the root, and returns copies of it, so changing this path no " \
|
|
379
|
+
"longer changes the root, change a copy of it instead, such as IOStreams.root#{root}.dup, or a " \
|
|
380
|
+
"path joined to it, such as IOStreams.join(\"a.csv\"#{join})")
|
|
381
|
+
end
|
|
382
|
+
|
|
289
383
|
# Returns [Hash<Symbol:Hash>] the streams with the values of sensitive options replaced.
|
|
290
384
|
def redact(streams)
|
|
291
|
-
streams
|
|
292
|
-
next options unless options.is_a?(Hash)
|
|
293
|
-
|
|
294
|
-
options.to_h { |name, value| [name, name.to_s.match?(SENSITIVE_OPTION) ? "[FILTERED]" : value] }
|
|
295
|
-
end
|
|
385
|
+
IOStreams.redact_stream_options(streams)
|
|
296
386
|
end
|
|
297
387
|
|
|
298
388
|
def builder
|
|
@@ -3,13 +3,64 @@ require "fileutils"
|
|
|
3
3
|
module IOStreams
|
|
4
4
|
module Paths
|
|
5
5
|
class File < IOStreams::Path
|
|
6
|
-
|
|
6
|
+
# A pattern supplied to #each_child that is not a String, such as a Pathname, is deprecated.
|
|
7
|
+
PATTERN_DEPRECATION = "a pattern that is not a String is deprecated for #each_child and #children, since v3.0 " \
|
|
8
|
+
"raises ArgumentError for it, supply a String instead, such as pattern.to_s".freeze
|
|
9
|
+
private_constant :PATTERN_DEPRECATION
|
|
10
|
+
|
|
11
|
+
# Moving a file onto an existing directory is deprecated, see #move_to.
|
|
12
|
+
MOVE_DEPRECATION = "#move_to onto an existing directory is deprecated, since v3.0 raises ArgumentError for it, " \
|
|
13
|
+
"supply the file within the directory instead, such as " \
|
|
14
|
+
"move_to(IOStreams.path(directory).join(path.basename))".freeze
|
|
15
|
+
private_constant :MOVE_DEPRECATION
|
|
16
|
+
|
|
17
|
+
attr_reader :create_path
|
|
7
18
|
|
|
8
19
|
def initialize(file_name, create_path: true)
|
|
9
20
|
@create_path = create_path
|
|
10
21
|
super(file_name)
|
|
11
22
|
end
|
|
12
23
|
|
|
24
|
+
# Sets whether writing the file creates its directory first, see #mkpath.
|
|
25
|
+
def create_path=(create_path)
|
|
26
|
+
warn_change("#create_path=")
|
|
27
|
+
@create_path = create_path
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# Returns [String|nil] the deprecation warning for a name that IOStreams v3.0 reads as another file, see
|
|
31
|
+
# `IOStreams::Path.name_deprecation`, or nil for one that it reads the same way:
|
|
32
|
+
# - A name of `~`, or starting with `~/`, which is within a directory called `~` in the current directory, while
|
|
33
|
+
# v3.0 reads it within the home directory. A name such as `~user/a.csv` or `~$Book1.xlsx` is the same on both.
|
|
34
|
+
# - A `file://` url, such as `file:///data/a.csv` or `file://~/a.csv`, which is a file name relative to the current
|
|
35
|
+
# directory, within a directory called `file:`, while v3.0 reads it as the local file that it names, such as
|
|
36
|
+
# `/data/a.csv`, or within the home directory.
|
|
37
|
+
def self.name_deprecation(name)
|
|
38
|
+
if file_url?(name)
|
|
39
|
+
"a file:// url is deprecated, since v3.0 reads it as the local file that it names, such as /data/a.csv for " \
|
|
40
|
+
"file:///data/a.csv, or a file within the home directory for file://~/a.csv, rather than as a file name " \
|
|
41
|
+
"relative to the current directory: supply the name of the file instead, such as /data/a.csv, or " \
|
|
42
|
+
"File.expand_path(\"~/a.csv\") for the home directory"
|
|
43
|
+
elsif name == "~" || name.start_with?("~/")
|
|
44
|
+
"a local file name of ~, or starting with ~/, is deprecated, since v3.0 reads it within the home directory, " \
|
|
45
|
+
"rather than within a directory called ~ in the current directory: expand it with File.expand_path for " \
|
|
46
|
+
"the home directory, or start it with ./~/ for the directory called ~"
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# Returns [String|nil] the deprecation warning for a name that IOStreams v3.0 reads as another file, see
|
|
51
|
+
# .name_deprecation, which is in another directory, and so another allowed path.
|
|
52
|
+
def self.location_deprecation(name)
|
|
53
|
+
name_deprecation(name)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# Returns [true|false] whether the name is a `file://` url, in any case, as IOStreams v3.0 takes it, see
|
|
57
|
+
# .name_deprecation. Its first bytes are compared, so that a name that is not valid in its encoding, such as a
|
|
58
|
+
# Latin-1 name, can be checked too.
|
|
59
|
+
def self.file_url?(name)
|
|
60
|
+
name.include?("://") && name.byteslice(0, 7).b.casecmp?("file://")
|
|
61
|
+
end
|
|
62
|
+
private_class_method :file_url?
|
|
63
|
+
|
|
13
64
|
# Yields Paths within the current path.
|
|
14
65
|
#
|
|
15
66
|
# Examples:
|
|
@@ -88,20 +139,28 @@ module IOStreams
|
|
|
88
139
|
# "**.rb" "./main.rb" false
|
|
89
140
|
# "**.rb" "lib/song.rb" true
|
|
90
141
|
# "*" "dave/.profile" true
|
|
142
|
+
#
|
|
143
|
+
# A pattern that is not a String, such as a Pathname, is deprecated, and reports a deprecation warning, see
|
|
144
|
+
# `IOStreams::Deprecation`.
|
|
91
145
|
def each_child(pattern = "*", case_sensitive: false, directories: false, hidden: false)
|
|
92
146
|
unless block_given?
|
|
93
147
|
return to_enum(__method__, pattern,
|
|
94
148
|
case_sensitive: case_sensitive, directories: directories, hidden: hidden)
|
|
95
149
|
end
|
|
96
150
|
|
|
151
|
+
Deprecation.warn(PATTERN_DEPRECATION) unless pattern.is_a?(String)
|
|
97
152
|
authorize!
|
|
98
153
|
|
|
154
|
+
# A pattern that is a Pathname is taken by its #to_path, as `::File.join` takes it, which v2.1 joined the pattern
|
|
155
|
+
# with: `IOStreams::Utils::PathName.join` does not take it on JRuby, where it joins the names by their bytes, and
|
|
156
|
+
# the case-insensitive retry below matches and downcases the pattern as a String.
|
|
157
|
+
pattern = pattern.to_path if pattern.respond_to?(:to_path)
|
|
158
|
+
|
|
99
159
|
flags = 0
|
|
100
160
|
flags |= ::File::FNM_CASEFOLD unless case_sensitive
|
|
101
161
|
flags |= ::File::FNM_DOTMATCH if hidden
|
|
102
162
|
|
|
103
|
-
|
|
104
|
-
full_pattern = ::File.join(path, pattern)
|
|
163
|
+
full_pattern = Utils::PathName.join(path, pattern)
|
|
105
164
|
|
|
106
165
|
results = Dir.glob(full_pattern, flags)
|
|
107
166
|
|
|
@@ -111,7 +170,7 @@ module IOStreams
|
|
|
111
170
|
if results.empty? && !case_sensitive && pattern.match?(/[A-Z]/)
|
|
112
171
|
# Try converting the pattern to lowercase and re-matching
|
|
113
172
|
lowercase_pattern = pattern.downcase
|
|
114
|
-
lowercase_full_pattern = ::
|
|
173
|
+
lowercase_full_pattern = Utils::PathName.join(path, lowercase_pattern)
|
|
115
174
|
results = Dir.glob(lowercase_full_pattern, flags)
|
|
116
175
|
end
|
|
117
176
|
|
|
@@ -127,12 +186,16 @@ module IOStreams
|
|
|
127
186
|
#
|
|
128
187
|
# Notes:
|
|
129
188
|
# - Can copy across buckets.
|
|
189
|
+
# - A target that is an existing directory, which the file is moved into, as `mv` does, returning the directory, is
|
|
190
|
+
# deprecated, and reports a deprecation warning, see `IOStreams::Deprecation`: supply the file within it instead,
|
|
191
|
+
# such as `move_to(IOStreams.path("/archive").join(path.basename))`.
|
|
130
192
|
def move_to(target_path)
|
|
131
193
|
target = IOStreams.new(target_path)
|
|
132
194
|
return super(target) unless target.is_a?(self.class)
|
|
133
195
|
|
|
134
196
|
authorize!
|
|
135
197
|
target.authorize!
|
|
198
|
+
Deprecation.warn(MOVE_DEPRECATION) if ::File.directory?(target.path)
|
|
136
199
|
target.mkpath
|
|
137
200
|
# In case the file is being moved across partitions
|
|
138
201
|
FileUtils.move(path, target.to_s)
|
|
@@ -141,7 +204,7 @@ module IOStreams
|
|
|
141
204
|
|
|
142
205
|
def mkpath
|
|
143
206
|
authorize!
|
|
144
|
-
dir = ::
|
|
207
|
+
dir = Utils::PathName.dirname(path)
|
|
145
208
|
FileUtils.mkdir_p(dir)
|
|
146
209
|
self
|
|
147
210
|
end
|
|
@@ -193,13 +256,13 @@ module IOStreams
|
|
|
193
256
|
# is appended to the real path of the part that does exist. It cannot contain `.` or `..`, since
|
|
194
257
|
# what they refer to depends on directories that have not been created yet.
|
|
195
258
|
def allowed_location
|
|
196
|
-
existing = ::File.absolute_path?(path) ? path : ::
|
|
259
|
+
existing = ::File.absolute_path?(path) ? path : Utils::PathName.join(Dir.pwd, path)
|
|
197
260
|
missing = []
|
|
198
261
|
until present?(existing)
|
|
199
|
-
parent = ::
|
|
262
|
+
parent = Utils::PathName.dirname(existing)
|
|
200
263
|
break if parent == existing
|
|
201
264
|
|
|
202
|
-
missing.unshift(::
|
|
265
|
+
missing.unshift(Utils::PathName.basename(existing))
|
|
203
266
|
existing = parent
|
|
204
267
|
end
|
|
205
268
|
|
|
@@ -207,7 +270,7 @@ module IOStreams
|
|
|
207
270
|
raise(Errors::AccessDenied, "Access denied to #{path}: '.' and '..' are not allowed after a missing directory")
|
|
208
271
|
end
|
|
209
272
|
|
|
210
|
-
::
|
|
273
|
+
Utils::PathName.join(::File.realpath(existing), *missing)
|
|
211
274
|
rescue SystemCallError => e
|
|
212
275
|
raise(Errors::AccessDenied, "Access denied to #{path}: #{e.message}")
|
|
213
276
|
end
|
|
@@ -6,6 +6,19 @@ module IOStreams
|
|
|
6
6
|
class HTTP < IOStreams::Path
|
|
7
7
|
attr_reader :username, :password, :http_redirect_count, :url
|
|
8
8
|
|
|
9
|
+
# The password, and the query `parameters`, which can hold an access key, see IOStreams::Path.redact_options.
|
|
10
|
+
def self.sensitive_option_names = %i[password parameters]
|
|
11
|
+
|
|
12
|
+
# Returns [String|nil] the deprecation warning for an HTTP(S) url without a path, such as `https://host`, or
|
|
13
|
+
# `https://host?a=1`, which IOStreams v3.0 reads with the path `/`, as `https://host/?a=1`, see
|
|
14
|
+
# `IOStreams::Path.name_deprecation`.
|
|
15
|
+
def self.name_deprecation(url)
|
|
16
|
+
return unless url_without_path?(url)
|
|
17
|
+
|
|
18
|
+
"an HTTP url without a path, such as https://host or https://host?a=1, is deprecated, since v3.0 adds a / " \
|
|
19
|
+
"after the host, https://host/?a=1, which changes its #to_s and #path: supply the url with the /"
|
|
20
|
+
end
|
|
21
|
+
|
|
9
22
|
# Stream to/from a remote file over http(s).
|
|
10
23
|
#
|
|
11
24
|
# Parameters:
|
|
@@ -21,7 +34,8 @@ module IOStreams
|
|
|
21
34
|
# SECURITY WARNING:
|
|
22
35
|
# A username and password supplied in the url remain part of it, so `#to_s` and `#url`
|
|
23
36
|
# return them, as does any log or error message that includes the path.
|
|
24
|
-
# Supply them with the `username:` and `password:` arguments instead
|
|
37
|
+
# Supply them with the `username:` and `password:` arguments instead,
|
|
38
|
+
# and log `#display_name`, which never includes them.
|
|
25
39
|
#
|
|
26
40
|
# username: [String]
|
|
27
41
|
# When supplied, basic authentication is used with the username and password.
|
|
@@ -72,6 +86,7 @@ module IOStreams
|
|
|
72
86
|
@maximum_file_size = maximum_file_size
|
|
73
87
|
@url = parameters ? "#{url}?#{URI.encode_www_form(parameters)}" : url
|
|
74
88
|
super(uri.path)
|
|
89
|
+
warn_url_without_host(uri)
|
|
75
90
|
end
|
|
76
91
|
|
|
77
92
|
# Does not support relative file names since there is no concept of current working directory
|
|
@@ -83,17 +98,48 @@ module IOStreams
|
|
|
83
98
|
url
|
|
84
99
|
end
|
|
85
100
|
|
|
101
|
+
# Returns [String] the url without the user name, password, query or fragment, which can hold credentials,
|
|
102
|
+
# such as the signature of a pre-signed url, or the `parameters:` supplied to this path.
|
|
103
|
+
def display_name
|
|
104
|
+
uri = URI.parse(url)
|
|
105
|
+
uri.user = nil
|
|
106
|
+
uri.query = nil
|
|
107
|
+
uri.fragment = nil
|
|
108
|
+
uri.to_s
|
|
109
|
+
end
|
|
110
|
+
|
|
86
111
|
private
|
|
87
112
|
|
|
88
113
|
attr_reader :allow_hosts, :maximum_file_size
|
|
89
114
|
|
|
115
|
+
# Reports a deprecation warning for a url without a host, such as `http:///a.csv`, which IOStreams 3.0 raises
|
|
116
|
+
# ArgumentError for when the path is created, so that a mistake in a configured url is reported wherever the
|
|
117
|
+
# code runs.
|
|
118
|
+
def warn_url_without_host(uri)
|
|
119
|
+
return unless uri.host.to_s.empty?
|
|
120
|
+
|
|
121
|
+
Deprecation.warn("the HTTP url #{display_name.inspect} is deprecated, since it names no host, name one, such " \
|
|
122
|
+
"as 'https://example.org/a.csv'")
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
# The methods of the IO that v3.0 reads the response from, see #v3_source: those of an IO that read, and #close.
|
|
126
|
+
V3_SOURCE_METHODS = [*IOStreams::Reader::V3_READ_METHODS, :close].freeze
|
|
127
|
+
private_constant :V3_SOURCE_METHODS
|
|
128
|
+
|
|
129
|
+
# Returns [Array(Array<Symbol>, String)] the methods of the IO that v3.0 reads the response from, in place of the
|
|
130
|
+
# temp File that v2.2 downloads it into, and its name, see `IOStreams::Stream#v3_source`: v3.0 reads the response as
|
|
131
|
+
# the application reads it, with an IO that only reads.
|
|
132
|
+
def v3_source
|
|
133
|
+
[V3_SOURCE_METHODS, "the IO of an HTTP path"]
|
|
134
|
+
end
|
|
135
|
+
|
|
90
136
|
# Read a file using an http get.
|
|
91
137
|
#
|
|
92
138
|
# For example:
|
|
93
139
|
# IOStreams.path('https://www5.fdic.gov/idasp/Offices2.zip').reader {|file| puts file.read}
|
|
94
140
|
#
|
|
95
141
|
# Read the file without unzipping and streaming the first file in the zip:
|
|
96
|
-
# IOStreams.path('https://www5.fdic.gov/idasp/Offices2.zip').
|
|
142
|
+
# IOStreams.path('https://www5.fdic.gov/idasp/Offices2.zip').raw.reader {|file| puts file.read}
|
|
97
143
|
#
|
|
98
144
|
# Notes:
|
|
99
145
|
# * Since Net::HTTP download only supports a push stream, the data is streamed into a tempfile first.
|
|
@@ -38,20 +38,23 @@ module IOStreams
|
|
|
38
38
|
|
|
39
39
|
private
|
|
40
40
|
|
|
41
|
+
# The directory is created without reporting a deprecation warning for its name, see `IOStreams::Paths.build`,
|
|
42
|
+
# since it is derived from the pattern, whose name `IOStreams.each_child` reports instead, and since other gems
|
|
43
|
+
# create a matcher themselves, such as Rocket Job, whose users cannot change its lines that a warning would name.
|
|
41
44
|
def extract_optimized_path(path, pattern)
|
|
42
45
|
elements = pattern.split("/")
|
|
43
46
|
index = elements.find_index { |e| e.match(MATCH_START_CHARS) }
|
|
44
47
|
if index.nil?
|
|
45
48
|
# No index means it has no pattern.
|
|
46
|
-
@path = path.nil? ?
|
|
49
|
+
@path = path.nil? ? Paths.build(pattern) : path.join(pattern)
|
|
47
50
|
@pattern = nil
|
|
48
51
|
elsif index.zero?
|
|
49
52
|
# Cannot optimize path since the very first entry contains a wildcard
|
|
50
|
-
@path = path ||
|
|
53
|
+
@path = path || Paths.build("")
|
|
51
54
|
@pattern = pattern
|
|
52
55
|
else
|
|
53
56
|
new_path = elements[0..(index - 1)].join("/")
|
|
54
|
-
@path = path.nil? ?
|
|
57
|
+
@path = path.nil? ? Paths.build(new_path) : path.join(new_path)
|
|
55
58
|
@pattern = elements[index..].join("/")
|
|
56
59
|
end
|
|
57
60
|
end
|