iostreams 0.20.3 → 1.0.0.beta

Sign up to get free protection for your applications and to get access to all the features.
Files changed (90) hide show
  1. checksums.yaml +4 -4
  2. data/lib/io_streams/bzip2/reader.rb +9 -21
  3. data/lib/io_streams/bzip2/writer.rb +9 -21
  4. data/lib/io_streams/deprecated.rb +217 -0
  5. data/lib/io_streams/encode/reader.rb +12 -16
  6. data/lib/io_streams/encode/writer.rb +9 -13
  7. data/lib/io_streams/errors.rb +6 -6
  8. data/lib/io_streams/gzip/reader.rb +7 -14
  9. data/lib/io_streams/gzip/writer.rb +7 -15
  10. data/lib/io_streams/io_streams.rb +182 -524
  11. data/lib/io_streams/line/reader.rb +9 -9
  12. data/lib/io_streams/line/writer.rb +10 -11
  13. data/lib/io_streams/path.rb +190 -0
  14. data/lib/io_streams/paths/file.rb +176 -0
  15. data/lib/io_streams/paths/http.rb +92 -0
  16. data/lib/io_streams/paths/matcher.rb +61 -0
  17. data/lib/io_streams/paths/s3.rb +269 -0
  18. data/lib/io_streams/paths/sftp.rb +99 -0
  19. data/lib/io_streams/pgp.rb +47 -19
  20. data/lib/io_streams/pgp/reader.rb +20 -28
  21. data/lib/io_streams/pgp/writer.rb +24 -46
  22. data/lib/io_streams/reader.rb +28 -0
  23. data/lib/io_streams/record/reader.rb +20 -16
  24. data/lib/io_streams/record/writer.rb +28 -28
  25. data/lib/io_streams/row/reader.rb +22 -26
  26. data/lib/io_streams/row/writer.rb +29 -28
  27. data/lib/io_streams/stream.rb +400 -0
  28. data/lib/io_streams/streams.rb +125 -0
  29. data/lib/io_streams/symmetric_encryption/reader.rb +5 -13
  30. data/lib/io_streams/symmetric_encryption/writer.rb +16 -15
  31. data/lib/io_streams/tabular/header.rb +9 -3
  32. data/lib/io_streams/tabular/parser/array.rb +8 -3
  33. data/lib/io_streams/tabular/parser/csv.rb +6 -2
  34. data/lib/io_streams/tabular/parser/hash.rb +4 -1
  35. data/lib/io_streams/tabular/parser/json.rb +3 -1
  36. data/lib/io_streams/tabular/parser/psv.rb +3 -1
  37. data/lib/io_streams/tabular/utility/csv_row.rb +9 -8
  38. data/lib/io_streams/utils.rb +22 -0
  39. data/lib/io_streams/version.rb +1 -1
  40. data/lib/io_streams/writer.rb +28 -0
  41. data/lib/io_streams/xlsx/reader.rb +7 -19
  42. data/lib/io_streams/zip/reader.rb +7 -26
  43. data/lib/io_streams/zip/writer.rb +21 -38
  44. data/lib/iostreams.rb +15 -15
  45. data/test/bzip2_reader_test.rb +3 -3
  46. data/test/bzip2_writer_test.rb +3 -3
  47. data/test/deprecated_test.rb +123 -0
  48. data/test/encode_reader_test.rb +3 -3
  49. data/test/encode_writer_test.rb +6 -6
  50. data/test/gzip_reader_test.rb +2 -2
  51. data/test/gzip_writer_test.rb +3 -3
  52. data/test/io_streams_test.rb +43 -136
  53. data/test/line_reader_test.rb +20 -20
  54. data/test/line_writer_test.rb +3 -3
  55. data/test/path_test.rb +30 -28
  56. data/test/paths/file_test.rb +206 -0
  57. data/test/paths/http_test.rb +34 -0
  58. data/test/paths/matcher_test.rb +111 -0
  59. data/test/paths/s3_test.rb +207 -0
  60. data/test/pgp_reader_test.rb +8 -8
  61. data/test/pgp_writer_test.rb +13 -13
  62. data/test/record_reader_test.rb +5 -5
  63. data/test/record_writer_test.rb +4 -4
  64. data/test/row_reader_test.rb +5 -5
  65. data/test/row_writer_test.rb +6 -6
  66. data/test/stream_test.rb +116 -0
  67. data/test/streams_test.rb +255 -0
  68. data/test/utils_test.rb +20 -0
  69. data/test/xlsx_reader_test.rb +3 -3
  70. data/test/zip_reader_test.rb +12 -12
  71. data/test/zip_writer_test.rb +5 -5
  72. metadata +33 -45
  73. data/lib/io_streams/base_path.rb +0 -72
  74. data/lib/io_streams/file/path.rb +0 -58
  75. data/lib/io_streams/file/reader.rb +0 -12
  76. data/lib/io_streams/file/writer.rb +0 -22
  77. data/lib/io_streams/http/reader.rb +0 -71
  78. data/lib/io_streams/s3.rb +0 -26
  79. data/lib/io_streams/s3/path.rb +0 -40
  80. data/lib/io_streams/s3/reader.rb +0 -28
  81. data/lib/io_streams/s3/writer.rb +0 -85
  82. data/lib/io_streams/sftp/reader.rb +0 -67
  83. data/lib/io_streams/sftp/writer.rb +0 -68
  84. data/test/base_path_test.rb +0 -35
  85. data/test/file_path_test.rb +0 -97
  86. data/test/file_reader_test.rb +0 -33
  87. data/test/file_writer_test.rb +0 -50
  88. data/test/http_reader_test.rb +0 -38
  89. data/test/s3_reader_test.rb +0 -41
  90. data/test/s3_writer_test.rb +0 -41
@@ -0,0 +1,61 @@
1
+ module IOStreams
2
+ module Paths
3
+ # Implement fnmatch logic for any path iterator
4
+ class Matcher
5
+ # Characters indicating that pattern matching is required
6
+ MATCH_START_CHARS = /[*?\[{]/
7
+
8
+ attr_reader :path, :pattern, :flags
9
+
10
+ # If the supplied pattern contains sub-directories without wildcards, navigate down to that directory
11
+ # first before applying wildcard lookups from that point on.
12
+ #
13
+ # Examples: If the current path is "/path/work"
14
+ # "a/b/c/**/*" => "/path/work/a/b/c"
15
+ # "a/b/c?/**/*" => "/path/work/a/b"
16
+ # "**/*" => "/path/work"
17
+ #
18
+ # Note: Absolute paths in the pattern are not supported.
19
+ def initialize(path, pattern, case_sensitive: false, hidden: false)
20
+ extract_optimized_path(path, pattern)
21
+
22
+ @flags = ::File::FNM_EXTGLOB
23
+ @flags |= ::File::FNM_CASEFOLD unless case_sensitive
24
+ @flags |= ::File::FNM_DOTMATCH if hidden
25
+ end
26
+
27
+ # Returns whether the relative `file_name` matches
28
+ def match?(file_name)
29
+ relative_file_name = file_name.sub(path.to_s, '').sub(%r{\A/}, '')
30
+ ::File.fnmatch?(pattern, relative_file_name, flags)
31
+ end
32
+
33
+ # Whether this pattern includes a recursive match.
34
+ # I.e. Includes `**` anywhere in the path
35
+ def recursive?
36
+ @recursive ||= pattern.nil? ? false : pattern.include?("**")
37
+ end
38
+
39
+ private
40
+
41
+ def extract_optimized_path(path, pattern)
42
+ elements = pattern.split('/')
43
+ index = elements.find_index { |e| e.match(MATCH_START_CHARS) }
44
+ if index == 0
45
+ # Cannot optimize path since the very first entry contains a wildcard
46
+ @path = path || IOStreams.path
47
+ @pattern = pattern
48
+ elsif index.nil?
49
+ # No index means it has no pattern.
50
+ @path = path.nil? ? IOStreams.path(pattern) : path.join(pattern)
51
+ @pattern = nil
52
+ else
53
+ new_path = elements[0..index - 1].join('/')
54
+ @path = path.nil? ? IOStreams.path(new_path) : path.join(new_path)
55
+ @pattern = elements[index..-1].join('/')
56
+ end
57
+ end
58
+
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,269 @@
1
+ require "uri"
2
+
3
+ module IOStreams
4
+ module Paths
5
+ class S3 < IOStreams::Path
6
+ attr_reader :bucket_name, :key, :client
7
+
8
+ # Arguments:
9
+ #
10
+ # url: [String]
11
+ # Prefix must be: `s3://`
12
+ # followed by bucket name,
13
+ # followed by path and file_name (key).
14
+ # Examples:
15
+ # s3://my-bucket-name/file_name.txt
16
+ # s3://my-bucket-name/some_path/file_name.csv
17
+ #
18
+ # Writer specific options:
19
+ #
20
+ # @option params [String] :acl
21
+ # The canned ACL to apply to the object.
22
+ #
23
+ # @option params [String] :cache_control
24
+ # Specifies caching behavior along the request/reply chain.
25
+ #
26
+ # @option params [String] :content_disposition
27
+ # Specifies presentational information for the object.
28
+ #
29
+ # @option params [String] :content_encoding
30
+ # Specifies what content encodings have been applied to the object and
31
+ # thus what decoding mechanisms must be applied to obtain the media-type
32
+ # referenced by the Content-Type header field.
33
+ #
34
+ # @option params [String] :content_language
35
+ # The language the content is in.
36
+ #
37
+ # @option params [Integer] :content_length
38
+ # Size of the body in bytes. This parameter is useful when the size of
39
+ # the body cannot be determined automatically.
40
+ #
41
+ # @option params [String] :content_md5
42
+ # The base64-encoded 128-bit MD5 digest of the part data. This parameter
43
+ # is auto-populated when using the command from the CLI. This parameted
44
+ # is required if object lock parameters are specified.
45
+ #
46
+ # @option params [String] :content_type
47
+ # A standard MIME type describing the format of the object data.
48
+ #
49
+ # @option params [Time,DateTime,Date,Integer,String] :expires
50
+ # The date and time at which the object is no longer cacheable.
51
+ #
52
+ # @option params [String] :grant_full_control
53
+ # Gives the grantee READ, READ\_ACP, and WRITE\_ACP permissions on the
54
+ # object.
55
+ #
56
+ # @option params [String] :grant_read
57
+ # Allows grantee to read the object data and its metadata.
58
+ #
59
+ # @option params [String] :grant_read_acp
60
+ # Allows grantee to read the object ACL.
61
+ #
62
+ # @option params [String] :grant_write_acp
63
+ # Allows grantee to write the ACL for the applicable object.
64
+ #
65
+ # @option params [required, String] :key
66
+ # Object key for which the PUT operation was initiated.
67
+ #
68
+ # @option params [Hash<String,String>] :metadata
69
+ # A map of metadata to store with the object in S3.
70
+ #
71
+ # @option params [String] :server_side_encryption
72
+ # The Server-side encryption algorithm used when storing this object in
73
+ # S3 (e.g., AES256, aws:kms).
74
+ #
75
+ # @option params [String] :storage_class
76
+ # The type of storage to use for the object. Defaults to 'STANDARD'.
77
+ #
78
+ # @option params [String] :website_redirect_location
79
+ # If the bucket is configured as a website, redirects requests for this
80
+ # object to another object in the same bucket or to an external URL.
81
+ # Amazon S3 stores the value of this header in the object metadata.
82
+ #
83
+ # @option params [String] :sse_customer_algorithm
84
+ # Specifies the algorithm to use to when encrypting the object (e.g.,
85
+ # AES256).
86
+ #
87
+ # @option params [String] :sse_customer_key
88
+ # Specifies the customer-provided encryption key for Amazon S3 to use in
89
+ # encrypting data. This value is used to store the object and then it is
90
+ # discarded; Amazon does not store the encryption key. The key must be
91
+ # appropriate for use with the algorithm specified in the
92
+ # x-amz-server-side​-encryption​-customer-algorithm header.
93
+ #
94
+ # @option params [String] :sse_customer_key_md5
95
+ # Specifies the 128-bit MD5 digest of the encryption key according to
96
+ # RFC 1321. Amazon S3 uses this header for a message integrity check to
97
+ # ensure the encryption key was transmitted without error.
98
+ #
99
+ # @option params [String] :ssekms_key_id
100
+ # Specifies the AWS KMS key ID to use for object encryption. All GET and
101
+ # PUT requests for an object protected by AWS KMS will fail if not made
102
+ # via SSL or using SigV4. Documentation on configuring any of the
103
+ # officially supported AWS SDKs and CLI can be found at
104
+ # http://docs.aws.amazon.com/AmazonS3/latest/dev/UsingAWSSDK.html#specify-signature-version
105
+ #
106
+ # @option params [String] :ssekms_encryption_context
107
+ # Specifies the AWS KMS Encryption Context to use for object encryption.
108
+ # The value of this header is a base64-encoded UTF-8 string holding JSON
109
+ # with the encryption context key-value pairs.
110
+ #
111
+ # @option params [String] :request_payer
112
+ # Confirms that the requester knows that she or he will be charged for
113
+ # the request. Bucket owners need not specify this parameter in their
114
+ # requests. Documentation on downloading objects from requester pays
115
+ # buckets can be found at
116
+ # http://docs.aws.amazon.com/AmazonS3/latest/dev/ObjectsinRequesterPaysBuckets.html
117
+ #
118
+ # @option params [String] :tagging
119
+ # The tag-set for the object. The tag-set must be encoded as URL Query
120
+ # parameters. (For example, "Key1=Value1")
121
+ #
122
+ # @option params [String] :object_lock_mode
123
+ # The object lock mode that you want to apply to this object.
124
+ #
125
+ # @option params [Time,DateTime,Date,Integer,String] :object_lock_retain_until_date
126
+ # The date and time when you want this object's object lock to expire.
127
+ #
128
+ # @option params [String] :object_lock_legal_hold_status
129
+ # The Legal Hold status that you want to apply to the specified object.
130
+ def initialize(url, client: nil, **args)
131
+ Utils.load_dependency('aws-sdk-s3', 'AWS S3') unless defined?(::Aws::S3::Client)
132
+
133
+ uri = URI.parse(url)
134
+ raise "Invalid URI. Required Format: 's3://<bucket_name>/<key>'" unless uri.scheme == 's3'
135
+
136
+ @bucket_name = uri.host
137
+ @key = uri.path.sub(%r{\A/}, '')
138
+ @client = client || ::Aws::S3::Client.new
139
+ @options = args
140
+ super(url)
141
+ end
142
+
143
+ def delete
144
+ client.delete_object(bucket: bucket_name, key: key)
145
+ self
146
+ rescue Aws::S3::Errors::NotFound
147
+ self
148
+ end
149
+
150
+ def exist?
151
+ client.head_object(bucket: bucket_name, key: key)
152
+ true
153
+ rescue Aws::S3::Errors::NotFound
154
+ false
155
+ end
156
+
157
+ # Moves this file to the `target_path` by copying it to the new name and then deleting the current file.
158
+ #
159
+ # Notes:
160
+ # - Can copy across buckets.
161
+ def move_to(target_path)
162
+ target = IOStreams.new(target_path)
163
+ return super(target) unless target.is_a?(self.class)
164
+
165
+ source_name = ::File.join(bucket_name, key)
166
+ # TODO: Does/should it also copy metadata?
167
+ client.copy_object(bucket: target.bucket_name, key: target.key, copy_source: source_name)
168
+ delete
169
+ target
170
+ end
171
+
172
+ # S3 logically creates paths when a key is set.
173
+ def mkpath
174
+ self
175
+ end
176
+
177
+ def mkdir
178
+ self
179
+ end
180
+
181
+ def size
182
+ client.head_object(bucket: bucket_name, key: key).content_length
183
+ rescue Aws::S3::Errors::NotFound
184
+ nil
185
+ end
186
+
187
+ # TODO: delete_all
188
+
189
+ # Read from AWS S3 file.
190
+ def reader(&block)
191
+ # Since S3 download only supports a push stream, write it to a tempfile first.
192
+ Utils.temp_file_name("iostreams_s3") do |file_name|
193
+ read_file(file_name)
194
+
195
+ ::File.open(file_name, 'rb') { |io| io.read }
196
+ end
197
+ end
198
+
199
+ # Shortcut method if caller has a filename already with no other streams applied:
200
+ def read_file(file_name)
201
+ ::File.open(file_name, 'wb') do |file|
202
+ client.get_object(@options.merge(response_target: file, bucket: bucket_name, key: key))
203
+ end
204
+ end
205
+
206
+ # Write to AWS S3
207
+ #
208
+ # Raises [MultipartUploadError] If an object is being uploaded in
209
+ # parts, and the upload can not be completed, then the upload is
210
+ # aborted and this error is raised. The raised error has a `#errors`
211
+ # method that returns the failures that caused the upload to be
212
+ # aborted.
213
+ def writer(&block)
214
+ # Since S3 upload only supports a pull stream, write it to a tempfile first.
215
+ Utils.temp_file_name("iostreams_s3") do |file_name|
216
+ result = ::File.open(file_name, "wb", &block)
217
+
218
+ # Upload file only once all data has been written to it
219
+ write_file(file_name)
220
+ result
221
+ end
222
+ end
223
+
224
+ # Shortcut method if caller has a filename already with no other streams applied:
225
+ def write_file(file_name)
226
+ if ::File.size(file_name) > 5 * 1024 * 1024
227
+ # Use multipart file upload
228
+ s3 = Aws::S3::Resource.new(client: client)
229
+ obj = s3.bucket(bucket_name).object(key)
230
+ obj.upload_file(file_name)
231
+ else
232
+ ::File.open(file_name, 'rb') do |file|
233
+ client.put_object(@options.merge(bucket: bucket_name, key: key, body: file))
234
+ end
235
+ end
236
+ end
237
+
238
+ # Notes:
239
+ # - Currently all S3 lookups are recursive as of the pattern regardless of whether the pattern includes `**`.
240
+ def each_child(pattern = "*", case_sensitive: false, directories: false, hidden: false)
241
+ raise(NotImplementedError, "AWS S3 #each_child does not yet return directories") if directories
242
+
243
+ matcher = Matcher.new(self, pattern, case_sensitive: case_sensitive, hidden: hidden)
244
+
245
+ # When the pattern includes an exact file name without any pattern characters
246
+ if matcher.pattern.nil?
247
+ yield(matcher.path) if matcher.path.exist?
248
+ return
249
+ end
250
+
251
+ prefix = URI.parse(matcher.path.to_s).path.sub(%r{\A/}, '')
252
+ token = nil
253
+ loop do
254
+ # Fetches upto 1,000 entries at a time
255
+ resp = client.list_objects_v2(bucket: bucket_name, prefix: prefix, continuation_token: token)
256
+ resp.contents.each do |object|
257
+ file_name = ::File.join("s3://", resp.name, object.key)
258
+ next unless matcher.match?(file_name)
259
+
260
+ yield self.class.new(file_name)
261
+ end
262
+ token = resp.next_continuation_token
263
+ break if token.nil?
264
+ end
265
+ nil
266
+ end
267
+ end
268
+ end
269
+ end
@@ -0,0 +1,99 @@
1
+ module IOStreams
2
+ module Paths
3
+ class SFTP < IOStreams::Path
4
+ include SemanticLogger::Loggable if defined?(SemanticLogger)
5
+
6
+ attr_reader :hostname, :username, :file_name, :create_path, :options
7
+
8
+ # Stream to a remote file over sftp.
9
+ #
10
+ # file_name: [String]
11
+ # Name of file to write to.
12
+ #
13
+ # username: [String]
14
+ # Name of user to login with.
15
+ #
16
+ # password: [String]
17
+ # Password for the user.
18
+ #
19
+ # host: [String]
20
+ # Name of the host to connect to.
21
+ #
22
+ # port: [Integer]
23
+ # Port to connect to at the above host.
24
+ #
25
+ # **args
26
+ # Any other options supported by Net::SSH.start
27
+ #
28
+ # Examples:
29
+ #
30
+ # # Sample URL
31
+ # sftp://hostname/path/file_name
32
+ #
33
+ # # Full url showing all the optional elements that can be set via the url:
34
+ # sftp://username:password@hostname:22/path/file_name
35
+ def initialize(url, username:, password:, port: nil, max_pkt_size: 65_536, logger: nil, create_path: false, **args)
36
+ Utils.load_dependency('net-sftp', 'net/sftp') unless defined?(Net::SFTP)
37
+
38
+ uri = URI.parse(url)
39
+ raise(ArgumentError, "Invalid URL. Required Format: 'sftp://<host_name>/<file_name>'") unless uri.scheme == 'sftp'
40
+
41
+ @hostname = uri.hostname
42
+ @file_name = uri.path
43
+ @mkdir = false
44
+ @username = username || uri.user
45
+ @create_path = create_path
46
+
47
+ logger ||= self.logger if defined?(SemanticLogger)
48
+ options = args.dup
49
+ options[:logger] = logger
50
+ options[:port] = port || uri.port || 22
51
+ options[:max_pkt_size] = max_pkt_size
52
+ options[:password] = password || uri.password
53
+ @options = options
54
+ super(file_name)
55
+ end
56
+
57
+ def mkdir
58
+ @mkdir = true
59
+ self
60
+ end
61
+
62
+ # Read a file from a remote sftp server.
63
+ #
64
+ # Example:
65
+ # IOStreams.
66
+ # path("sftp://example.org/path/file.txt", username: "jbloggs", password: "secret", compression: false).
67
+ # reader do |input|
68
+ # puts input.read
69
+ # end
70
+ #
71
+ # Note:
72
+ # - raises Net::SFTP::StatusException when the file could not be read.
73
+ def reader(&block)
74
+ result = nil
75
+ Net::SFTP.start(hostname, username, options) do |sftp|
76
+ result = sftp.file.open(file_name, 'rb', &block)
77
+ end
78
+ result
79
+ end
80
+
81
+ # Write to a file on a remote sftp server.
82
+ #
83
+ # Example:
84
+ # IOStreams.
85
+ # path("sftp://example.org/path/file.txt", username: "jbloggs", password: "secret", compression: false).
86
+ # writer do |output|
87
+ # output.write('Hello World')
88
+ # end
89
+ def writer(&block)
90
+ result = nil
91
+ Net::SFTP.start(hostname, username, options) do |sftp|
92
+ sftp.session.exec!("mkdir -p '#{::File.dirname(file_name)}'") if create_path
93
+ result = sftp.file.open(file_name, 'wb', &block)
94
+ end
95
+ result
96
+ end
97
+ end
98
+ end
99
+ end
@@ -216,7 +216,7 @@ module IOStreams
216
216
  # email: [String]
217
217
  def self.key_info(key:)
218
218
  version_check
219
- command = "#{executable}"
219
+ command = executable.to_s
220
220
 
221
221
  out, err, status = Open3.capture3(command, binmode: true, stdin_data: key)
222
222
  logger.debug { "IOStreams::Pgp.key_info: #{command}\n#{err}#{out}" } if logger
@@ -275,8 +275,8 @@ module IOStreams
275
275
  command = "#{executable} --import"
276
276
 
277
277
  out, err, status = Open3.capture3(command, binmode: true, stdin_data: key)
278
- logger.debug { "IOStreams::Pgp.import: #{command}\n#{err}#{out}" } if logger
279
- if status.success? && err.length > 0
278
+ logger&.debug { "IOStreams::Pgp.import: #{command}\n#{err}#{out}" }
279
+ if status.success? && !err.empty?
280
280
  # Sample output
281
281
  #
282
282
  # gpg: key C16500E3: secret key imported\n"
@@ -306,10 +306,26 @@ module IOStreams
306
306
  results
307
307
  else
308
308
  return [] if err =~ /already in secret keyring/
309
+
309
310
  raise(Pgp::Failure, "GPG Failed importing key: #{err}#{out}")
310
311
  end
311
312
  end
312
313
 
314
+ # Returns [String] email for the supplied after importing and trusting the key
315
+ #
316
+ # Notes:
317
+ # - If the same email address has multiple keys then only the first is currently trusted.
318
+ def self.import_and_trust(key:)
319
+ raise(ArgumentError, "Key cannot be empty") if key.nil? || (key == '')
320
+
321
+ email = key_info(key: key).first.fetch(:email)
322
+ raise(ArgumentError, "Recipient email cannot be extracted from supplied key") unless email
323
+
324
+ import(key: key)
325
+ set_trust(email: email)
326
+ email
327
+ end
328
+
313
329
  # Set the trust level for an existing key.
314
330
  #
315
331
  # Returns [String] output if the trust was successfully updated
@@ -325,7 +341,7 @@ module IOStreams
325
341
  command = "#{executable} --import-ownertrust"
326
342
  trust = "#{fingerprint}:#{level + 1}:\n"
327
343
  out, err, status = Open3.capture3(command, stdin_data: trust)
328
- logger.debug { "IOStreams::Pgp.set_trust: #{command}\n#{err}#{out}" } if logger
344
+ logger&.debug { "IOStreams::Pgp.set_trust: #{command}\n#{err}#{out}" }
329
345
  if status.success?
330
346
  err
331
347
  else
@@ -336,7 +352,7 @@ module IOStreams
336
352
  # DEPRECATED - Use key_ids instead of fingerprints
337
353
  def self.fingerprint(email:)
338
354
  version_check
339
- Open3.popen2e("#{executable} --list-keys --fingerprint --with-colons #{email}") do |stdin, out, waith_thr|
355
+ Open3.popen2e("#{executable} --list-keys --fingerprint --with-colons #{email}") do |_stdin, out, waith_thr|
340
356
  output = out.read.chomp
341
357
  if waith_thr.value.success?
342
358
  output.each_line do |line|
@@ -347,6 +363,7 @@ module IOStreams
347
363
  nil
348
364
  else
349
365
  return if output =~ /(public key not found|No public key)/i
366
+
350
367
  raise(Pgp::Failure, "GPG Failed calling #{executable} to list keys for #{email}: #{output}")
351
368
  end
352
369
  end
@@ -361,7 +378,7 @@ module IOStreams
361
378
  @pgp_version ||= begin
362
379
  command = "#{executable} --version"
363
380
  out, err, status = Open3.capture3(command)
364
- logger.debug { "IOStreams::Pgp.version: #{command}\n#{err}#{out}" } if logger
381
+ logger&.debug { "IOStreams::Pgp.version: #{command}\n#{err}#{out}" }
365
382
  if status.success?
366
383
  # Sample output
367
384
  # #{executable} (GnuPG) 2.0.30
@@ -383,6 +400,7 @@ module IOStreams
383
400
  end
384
401
  else
385
402
  return [] if err =~ /(key not found|No (public|secret) key)/i
403
+
386
404
  raise(Pgp::Failure, "GPG Failed calling #{executable} to list keys for #{email || key_id}: #{err}#{out}")
387
405
  end
388
406
  end
@@ -397,7 +415,9 @@ module IOStreams
397
415
  end
398
416
 
399
417
  def self.version_check
400
- raise(Pgp::UnsupportedVersion, "Version #{pgp_version} of #{executable} is not yet supported. You are welcome to submit a Pull Request.") if pgp_version.to_f >= 2.3
418
+ if pgp_version.to_f >= 2.3
419
+ raise(Pgp::UnsupportedVersion, "Version #{pgp_version} of #{executable} is not yet supported. You are welcome to submit a Pull Request.")
420
+ end
401
421
  end
402
422
 
403
423
  # v2.2.1 output:
@@ -425,7 +445,7 @@ module IOStreams
425
445
  key_type: match[2],
426
446
  date: (Date.parse(match[4].to_s) rescue match[4])
427
447
  }
428
- elsif match = line.match(/(pub|sec)\s+(\d+)(.*)\/(\w+)\s+(\d+-\d+-\d+)(\s+(.+)<(.+)>)?/)
448
+ elsif match = line.match(%r{(pub|sec)\s+(\d+)(.*)/(\w+)\s+(\d+-\d+-\d+)(\s+(.+)<(.+)>)?})
429
449
  # Matches: pub 2048R/C7F9D9CB 2016-10-26
430
450
  # Or: pub 2048R/C7F9D9CB 2016-10-26 Receiver <receiver@example.org>
431
451
  hash = {
@@ -455,7 +475,6 @@ module IOStreams
455
475
  # v2.2 18A0FC1C09C0D8AE34CE659257DC4AE323C7368C
456
476
  hash[:key_id] ||= match[1]
457
477
  end
458
-
459
478
  end
460
479
  results
461
480
  end
@@ -467,13 +486,18 @@ module IOStreams
467
486
  return false if list.empty?
468
487
 
469
488
  list.each do |key_info|
470
- if key_id = key_info[:key_id]
471
- command = "#{executable} --batch --no-tty --yes --delete-#{keys} #{key_id}"
472
- out, err, status = Open3.capture3(command, binmode: true)
473
- logger.debug { "IOStreams::Pgp.delete_keys: #{command}\n#{err}#{out}" } if logger
489
+ key_id = key_info[:key_id]
490
+ next unless key_id
474
491
 
475
- raise(Pgp::Failure, "GPG Failed calling #{executable} to delete #{keys} for #{email}: #{err}: #{out}") unless status.success?
476
- raise(Pgp::Failure, "GPG Failed to delete #{keys} for #{email} #{err.strip}:#{out}") if out.include?('error')
492
+ command = "#{executable} --batch --no-tty --yes --delete-#{keys} #{key_id}"
493
+ out, err, status = Open3.capture3(command, binmode: true)
494
+ logger&.debug { "IOStreams::Pgp.delete_keys: #{command}\n#{err}#{out}" }
495
+
496
+ unless status.success?
497
+ raise(Pgp::Failure, "GPG Failed calling #{executable} to delete #{keys} for #{email}: #{err}: #{out}")
498
+ end
499
+ if out.include?('error')
500
+ raise(Pgp::Failure, "GPG Failed to delete #{keys} for #{email} #{err.strip}:#{out}")
477
501
  end
478
502
  end
479
503
  true
@@ -487,13 +511,17 @@ module IOStreams
487
511
  command << 'done'
488
512
 
489
513
  out, err, status = Open3.capture3(command, binmode: true)
490
- logger.debug { "IOStreams::Pgp.delete_keys: #{command}\n#{err}: #{out}" } if logger
514
+ logger&.debug { "IOStreams::Pgp.delete_keys: #{command}\n#{err}: #{out}" }
491
515
 
492
516
  return false if err =~ /(not found|no public key)/i
493
- raise(Pgp::Failure, "GPG Failed calling #{executable} to delete #{keys} for #{email}: #{err}: #{out}") unless status.success?
494
- raise(Pgp::Failure, "GPG Failed to delete #{keys} for #{email} #{err.strip}: #{out}") if out.include?('error')
517
+ unless status.success?
518
+ raise(Pgp::Failure, "GPG Failed calling #{executable} to delete #{keys} for #{email}: #{err}: #{out}")
519
+ end
520
+ if out.include?('error')
521
+ raise(Pgp::Failure, "GPG Failed to delete #{keys} for #{email} #{err.strip}: #{out}")
522
+ end
523
+
495
524
  true
496
525
  end
497
-
498
526
  end
499
527
  end