iostreams 0.9.1 → 0.10.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 4c229181098f8b4aedf3ce542477dc44554f94f4
4
- data.tar.gz: 866c35496433dbd255865b322a61bd50272b5ea5
3
+ metadata.gz: e653b5b68990dd8c8c6442c160a109a072e33a14
4
+ data.tar.gz: f30e9b16508e24ed1040f36bf16192cc2c5e7e1a
5
5
  SHA512:
6
- metadata.gz: d91a975d92b3ab08d2d0185a927f6403ccdd362b63dd07871f7322219d07ea088e84ff401c9bd6ec3cb8227e29a7d7702aec6d35dbe863a2764ebda83e5e3dc9
7
- data.tar.gz: 7204cf47a99466b5818b462f3cffbc303f129e277793c3fda1ec8ef1c5f697145bcda34d69ea859d674789cb3f500afcd29fcacad76963c173bdd9768757f057
6
+ metadata.gz: 63d58edaa9e53bc878bfca723772f0373791cea7411fe5be74b37db6e56d5a79003f06ccd0a95962a2e6d754c55f0a8af6031a68c0a70aef71ed83328e50000b
7
+ data.tar.gz: 28be995afd5b6627758599c6c50d2e21293bef90e1df9babb413a82152953237e5cf25191bb8d15a7aedf782c162e635a4d822878dde183a40c61a2f621ad37d
data/Rakefile CHANGED
@@ -1,6 +1,4 @@
1
- require 'rake/clean'
2
1
  require 'rake/testtask'
3
-
4
2
  require_relative 'lib/io_streams/version'
5
3
 
6
4
  task :gem do
@@ -14,14 +12,10 @@ task :publish => :gem do
14
12
  system "rm iostreams-#{IOStreams::VERSION}.gem"
15
13
  end
16
14
 
17
- desc 'Run Test Suite'
18
- task :test do
19
- Rake::TestTask.new(:functional) do |t|
20
- t.test_files = FileList['test/**/*_test.rb']
21
- t.verbose = true
22
- end
23
-
24
- Rake::Task['functional'].invoke
15
+ Rake::TestTask.new(:test) do |t|
16
+ t.pattern = 'test/**/*_test.rb'
17
+ t.verbose = true
18
+ t.warning = true
25
19
  end
26
20
 
27
21
  task :default => :test
@@ -4,12 +4,12 @@ module IOStreams
4
4
  attr_accessor :delimiter, :buffer_size, :encoding, :strip_non_printable
5
5
 
6
6
  # Read from a file or stream
7
- def self.open(file_name_or_io, options={}, &block)
7
+ def self.open(file_name_or_io, delimiter: nil, buffer_size: 65536, encoding: UTF8_ENCODING, strip_non_printable: false)
8
8
  if IOStreams.reader_stream?(file_name_or_io)
9
- block.call(new(file_name_or_io, options))
9
+ yield new(file_name_or_io, delimiter: delimiter, buffer_size: buffer_size, encoding: encoding, strip_non_printable: strip_non_printable)
10
10
  else
11
11
  ::File.open(file_name_or_io, 'rb') do |io|
12
- block.call(new(io, options))
12
+ yield new(io, delimiter: delimiter, buffer_size: buffer_size, encoding: encoding, strip_non_printable: strip_non_printable)
13
13
  end
14
14
  end
15
15
  end
@@ -24,38 +24,35 @@ module IOStreams
24
24
  # input_stream
25
25
  # The input stream that implements #read
26
26
  #
27
- # options
28
- # :delimiter[String]
29
- # Line / Record delimiter to use to break the stream up into records
30
- # Any string to break the stream up by
31
- # The records when saved will not include this delimiter
32
- # Default: nil
33
- # Automatically detect line endings and break up by line
34
- # Searches for the first "\r\n" or "\n" and then uses that as the
35
- # delimiter for all subsequent records
27
+ # delimiter: [String]
28
+ # Line / Record delimiter to use to break the stream up into records
29
+ # Any string to break the stream up by
30
+ # The records when saved will not include this delimiter
31
+ # Default: nil
32
+ # Automatically detect line endings and break up by line
33
+ # Searches for the first "\r\n" or "\n" and then uses that as the
34
+ # delimiter for all subsequent records
36
35
  #
37
- # :buffer_size [Integer]
38
- # Maximum size of the buffer into which to read the stream into for
39
- # processing.
40
- # Must be large enough to hold the entire first line and its delimiter(s)
41
- # Default: 65536 ( 64K )
36
+ # buffer_size: [Integer]
37
+ # Maximum size of the buffer into which to read the stream into for
38
+ # processing.
39
+ # Must be large enough to hold the entire first line and its delimiter(s)
40
+ # Default: 65536 ( 64K )
42
41
  #
43
- # :strip_non_printable [true|false]
44
- # Strip all non-printable characters read from the file
45
- # Default: false
42
+ # strip_non_printable: [true|false]
43
+ # Strip all non-printable characters read from the file
44
+ # Default: false
46
45
  #
47
- # :encoding
48
- # Force encoding to this encoding for all data being read
49
- # Default: UTF8_ENCODING
50
- # Set to nil to disable encoding
51
- def initialize(input_stream, options={})
46
+ # encoding:
47
+ # Force encoding to this encoding for all data being read
48
+ # Default: UTF8_ENCODING
49
+ # Set to nil to disable encoding
50
+ def initialize(input_stream, delimiter: nil, buffer_size: 65536, encoding: UTF8_ENCODING, strip_non_printable: false)
52
51
  @input_stream = input_stream
53
- options = options.dup
54
- @delimiter = options.delete(:delimiter)
55
- @buffer_size = options.delete(:buffer_size) || 65536
56
- @encoding = options.has_key?(:encoding) ? options.delete(:encoding) : UTF8_ENCODING
57
- @strip_non_printable = options.delete(:strip_non_printable) || false
58
- raise ArgumentError.new("Unknown IOStreams::Delimited::Reader#initialize options: #{options.inspect}") if options.size > 0
52
+ @delimiter = delimiter
53
+ @buffer_size = buffer_size
54
+ @encoding = encoding
55
+ @strip_non_printable = strip_non_printable
59
56
 
60
57
  @delimiter.force_encoding(UTF8_ENCODING) if @delimiter && @encoding
61
58
  @buffer = ''
@@ -4,12 +4,12 @@ module IOStreams
4
4
  attr_accessor :delimiter
5
5
 
6
6
  # Write delimited records/lines to a file or stream
7
- def self.open(file_name_or_io, options={}, &block)
7
+ def self.open(file_name_or_io, delimiter: $/, encoding: UTF8_ENCODING, strip_non_printable: false)
8
8
  if IOStreams.writer_stream?(file_name_or_io)
9
- block.call(new(file_name_or_io, options))
9
+ yield new(file_name_or_io, delimiter: delimiter, encoding: encoding, strip_non_printable: strip_non_printable)
10
10
  else
11
11
  ::File.open(file_name_or_io, 'wb') do |io|
12
- block.call(new(io, options))
12
+ yield new(io, delimiter: delimiter, encoding: encoding, strip_non_printable: strip_non_printable)
13
13
  end
14
14
  end
15
15
  end
@@ -26,28 +26,24 @@ module IOStreams
26
26
  # output_stream
27
27
  # The output stream that implements #write
28
28
  #
29
- # options
30
- # delimiter: [String]
31
- # Add the specified delimiter after every record when writing it
32
- # to the output stream
33
- # Default: OS Specific. Linux: "\n"
29
+ # delimiter: [String]
30
+ # Add the specified delimiter after every record when writing it
31
+ # to the output stream
32
+ # Default: OS Specific. Linux: "\n"
34
33
  #
35
- # :encoding
36
- # Force encoding to this encoding for all data being read
37
- # Default: UTF8_ENCODING
38
- # Set to nil to disable encoding
34
+ # encoding:
35
+ # Force encoding to this encoding for all data being read
36
+ # Default: UTF8_ENCODING
37
+ # Set to nil to disable encoding
39
38
  #
40
- # :strip_non_printable [true|false]
41
- # Strip all non-printable characters read from the file
42
- # Default: false
43
- def initialize(output_stream, options={})
39
+ # strip_non_printable: [true|false]
40
+ # Strip all non-printable characters read from the file
41
+ # Default: false
42
+ def initialize(output_stream, delimiter: $/, encoding: UTF8_ENCODING, strip_non_printable: false)
44
43
  @output_stream = output_stream
45
- options = options.dup
46
- @delimiter = options.has_key?(:delimiter) ? options.delete(:delimiter) : $/.dup
47
- @encoding = options.has_key?(:encoding) ? options.delete(:encoding) : UTF8_ENCODING
48
- @strip_non_printable = options.delete(:strip_non_printable)
49
- @strip_non_printable = @strip_non_printable.nil? && (@encoding == UTF8_ENCODING)
50
- raise ArgumentError.new("Unknown IOStreams::Delimited::Writer#initialize options: #{options.inspect}") if options.size > 0
44
+ @delimiter = delimiter.dup
45
+ @encoding = encoding
46
+ @strip_non_printable = strip_non_printable
51
47
  @delimiter.force_encoding(UTF8_ENCODING) if @delimiter
52
48
  end
53
49
 
@@ -51,7 +51,7 @@ module IOStreams
51
51
  # # MyXls::Reader and MyXls::Writer must implement .open
52
52
  # register_extension(:xls, MyXls::Reader, MyXls::Writer)
53
53
  def self.register_extension(extension, reader_class, writer_class)
54
- raise "Invalid extension #{extension.inspect}" unless extension.to_s =~ /\A\w+\Z/
54
+ raise(ArgumentError, "Invalid extension #{extension.inspect}") unless extension.to_s =~ /\A\w+\Z/
55
55
  @@extensions[extension.to_sym] = Extension.new(reader_class, writer_class)
56
56
  end
57
57
 
@@ -62,7 +62,7 @@ module IOStreams
62
62
  # Example:
63
63
  # register_extension(:xls)
64
64
  def self.deregister_extension(extension)
65
- raise "Invalid extension #{extension.inspect}" unless extension.to_s =~ /\A\w+\Z/
65
+ raise(ArgumentError, "Invalid extension #{extension.inspect}") unless extension.to_s =~ /\A\w+\Z/
66
66
  @@extensions.delete(extension.to_sym)
67
67
  end
68
68
 
@@ -239,7 +239,7 @@ module IOStreams
239
239
  stream_struct.klass.open(file_name_or_io, stream_struct.options, &block)
240
240
  else
241
241
  # Daisy chain multiple streams together
242
- last = stream_structs.inject(block) { |inner, stream_struct| -> io { stream_struct.klass.open(io, stream_struct.options, &inner) } }
242
+ last = stream_structs.inject(block) { |inner, ss| -> io { ss.klass.open(io, ss.options, &inner) } }
243
243
  last.call(file_name_or_io)
244
244
  end
245
245
  end
@@ -283,5 +283,7 @@ module IOStreams
283
283
  register_extension(:delimited, IOStreams::Delimited::Reader, IOStreams::Delimited::Writer)
284
284
  register_extension(:xlsx, IOStreams::Xlsx::Reader, nil)
285
285
  register_extension(:xlsm, IOStreams::Xlsx::Reader, nil)
286
+ register_extension(:pgp, IOStreams::Pgp::Reader, IOStreams::Pgp::Writer)
287
+ register_extension(:gpg, IOStreams::Pgp::Reader, IOStreams::Pgp::Writer)
286
288
  #register_extension(:csv, IOStreams::CSV::Reader, IOStreams::CSV::Writer)
287
289
  end
@@ -0,0 +1,213 @@
1
+ require 'open3'
2
+ module IOStreams
3
+ # Read/Write PGP/GPG file or stream.
4
+ #
5
+ # Example Setup:
6
+ #
7
+ # 1. Install OpenPGP
8
+ # Mac OSX (homebrew) : `brew install gpg2`
9
+ # Redhat Linux: `rpm install gpg2`
10
+ #
11
+ # 2. # Generate senders private and public key
12
+ # IOStreams::Pgp.generate_key(name: 'Sender', email: 'sender@example.org', passphrase: 'sender_passphrase')
13
+ #
14
+ # 3. # Generate receivers private and public key
15
+ # IOStreams::Pgp.generate_key(name: 'Receiver', email: 'receiver@example.org', passphrase: 'receiver_passphrase')
16
+ #
17
+ # Example 1:
18
+ #
19
+ # # Generate encrypted file for a specific recipient and sign it with senders credentials
20
+ # data = %w(this is some data that should be encrypted using pgp)
21
+ # IOStreams::Pgp::Writer.open('secure.gpg', recipient: 'receiver@example.org', signer: 'sender@example.org', signer_passphrase: 'sender_passphrase') do |output|
22
+ # data.each { |word| output.puts(word) }
23
+ # end
24
+ #
25
+ # # Decrypt the file sent to `receiver@example.org` using its private key
26
+ # # Recipient must also have the senders public key to verify the signature
27
+ # IOStreams::Pgp::Reader.open('secure.gpg', passphrase: 'receiver_passphrase') do |stream|
28
+ # while !stream.eof?
29
+ # ap stream.read(10)
30
+ # puts
31
+ # end
32
+ # end
33
+ #
34
+ # Example 2:
35
+ #
36
+ # # Default user and passphrase to sign the output file:
37
+ # IOStreams::Pgp::Writer.default_signer = 'sender@example.org'
38
+ # IOStreams::Pgp::Writer.default_signer_passphrase = 'sender_passphrase'
39
+ #
40
+ # # Default passphrase for decrypting recipients files.
41
+ # # Note: Usually this would be the senders passphrase, but in this example
42
+ # # it is decrypting the file intended for the recipient.
43
+ # IOStreams::Pgp::Reader.default_passphrase = 'receiver_passphrase'
44
+ #
45
+ # # Generate encrypted file for a specific recipient and sign it with senders credentials
46
+ # data = %w(this is some data that should be encrypted using pgp)
47
+ # IOStreams.writer('secure.gpg', pgp: {recipient: 'receiver@example.org'}) do |output|
48
+ # data.each { |word| output.puts(word) }
49
+ # end
50
+ #
51
+ # # Decrypt the file sent to `receiver@example.org` using its private key
52
+ # # Recipient must also have the senders public key to verify the signature
53
+ # IOStreams.reader('secure.gpg') do |stream|
54
+ # while data = stream.read(10)
55
+ # ap data
56
+ # end
57
+ # end
58
+ #
59
+ # FAQ:
60
+ # - If you get not trusted errors
61
+ # gpg --edit-key sender@example.org
62
+ # Select highest level: 5
63
+ #
64
+ # Delete test keys:
65
+ # IOStreams::Pgp.delete_keys(email: 'sender@example.org', secret: true)
66
+ # IOStreams::Pgp.delete_keys(email: 'receiver@example.org', secret: true)
67
+ #
68
+ # Limitations
69
+ # - Designed for processing larger files since a process is spawned for each file processed.
70
+ # - For small in memory files or individual emails, use the 'opengpgme' library.
71
+ #
72
+ # Compression Performance:
73
+ # Running tests on an Early 2015 Macbook Pro Dual Core with Ruby v2.3.1
74
+ #
75
+ # Input file: test.log 3.6GB
76
+ # :none: size: 3.6GB write: 52s read: 45s
77
+ # :zip: size: 411MB write: 75s read: 31s
78
+ # :zlib: size: 241MB write: 66s read: 23s ( 756KB Memory )
79
+ # :bzip2: size: 129MB write: 430s read: 130s ( 5MB Memory )
80
+ module Pgp
81
+ autoload :Reader, 'io_streams/pgp/reader'
82
+ autoload :Writer, 'io_streams/pgp/writer'
83
+
84
+ class Failure < StandardError
85
+ end
86
+
87
+ # Generate a new ultimate trusted local public and private key
88
+ # Returns [String] the key id for the generated key
89
+ # Raises an exception if it fails to generate the key
90
+ #
91
+ # name: [String]
92
+ # Name of who owns the key, such as organization
93
+ #
94
+ # email: [String]
95
+ # Email address for the key
96
+ #
97
+ # comment: [String]
98
+ # Optional comment to add to the generated key
99
+ #
100
+ # passphrase [String]
101
+ # Optional passphrase to secure the key with.
102
+ # Highly Recommended.
103
+ # To generate a good passphrase:
104
+ # `SecureRandom.urlsafe_base64(128)`
105
+ #
106
+ # See `man gpg` for the remaining options
107
+ def self.generate_key(name:, email:, comment: nil, passphrase: nil, key_type: 'RSA', key_length: 4096, subkey_type: 'RSA', subkey_length: key_length, expire_date: nil)
108
+ Open3.popen2e('gpg --batch --gen-key') do |stdin, out, waith_thr|
109
+ stdin.puts "Key-Type: #{key_type}" if key_type
110
+ stdin.puts "Key-Length: #{key_length}" if key_length
111
+ stdin.puts "Subkey-Type: #{subkey_type}" if subkey_type
112
+ stdin.puts "Subkey-Length: #{subkey_length}" if subkey_length
113
+ stdin.puts "Name-Real: #{name}" if name
114
+ stdin.puts "Name-Comment: #{comment}" if comment
115
+ stdin.puts "Name-Email: #{email}" if email
116
+ stdin.puts "Expire-Date: #{expire_date}" if expire_date
117
+ stdin.puts "Passphrase: #{passphrase}" if passphrase
118
+ stdin.puts '%commit'
119
+ stdin.close
120
+ if waith_thr.value.success?
121
+ key_id = nil
122
+ out.each_line do |line|
123
+ if (line = line.chomp) =~ /^gpg: key ([0-9A-F]+) marked as ultimately trusted/
124
+ key_id = $1.to_i(16)
125
+ end
126
+ end
127
+ key_id
128
+ else
129
+ raise(Pgp::Failure, "GPG Failed to generate key: #{out.read.chomp}")
130
+ end
131
+ end
132
+ end
133
+
134
+ # Delete a secret and public keys using its email
135
+ # Returns false if no key was found
136
+ # Raises an exception if it fails to delete the key
137
+ #
138
+ # email: [String] Email address for the key
139
+ #
140
+ # public: [true|false]
141
+ # Whether to delete the public key
142
+ # Default: true
143
+ #
144
+ # secret: [true|false]
145
+ # Whether to delete the secret key
146
+ # Default: false
147
+ def self.delete_keys(email:, public: true, secret: false)
148
+ cmd = "for i in `gpg --with-colons --fingerprint #{email} | grep \"^fpr\" | cut -d: -f10`; do\n"
149
+ cmd << "gpg --batch --delete-secret-keys \"$i\" ;\n" if secret
150
+ cmd << "gpg --batch --delete-keys \"$i\" ;\n" if public
151
+ cmd << 'done'
152
+ Open3.popen2e(cmd) do |stdin, out, waith_thr|
153
+ output = out.read.chomp
154
+ if waith_thr.value.success?
155
+ return false if output =~ /(public key not found|No public key)/i
156
+ raise(Pgp::Failure, "GPG Failed to delete keys for #{email}: #{output}") if output.include?('error')
157
+ true
158
+ else
159
+ raise(Pgp::Failure, "GPG Failed calling gpg to delete secret keys for #{email}: #{output}")
160
+ end
161
+ end
162
+ end
163
+
164
+ def self.has_key?(email:)
165
+ Open3.popen2e("gpg --list-keys --with-colons #{email}") do |stdin, out, waith_thr|
166
+ output = out.read.chomp
167
+ if waith_thr.value.success?
168
+ output.each_line do |line|
169
+ return true if line.match(/\Auid.*::([^\:]*):\Z/)
170
+ end
171
+ false
172
+ else
173
+ return false if output =~ /(public key not found|No public key)/i
174
+ raise(Pgp::Failure, "GPG Failed calling gpg to list keys for #{email}: #{output}")
175
+ end
176
+ end
177
+ end
178
+
179
+ # Returns [String] the key for the supplied email address
180
+ #
181
+ # email: [String] Email address for requested key
182
+ #
183
+ # ascii: [true|false]
184
+ # Whether to export as ASCII text instead of binary format
185
+ # Default: true
186
+ #
187
+ # secret: [true|false]
188
+ # Whether to export the private key
189
+ # Default: false
190
+ def self.export(email:, ascii: true, secret: false)
191
+ armor = ascii ? ' --armor' : nil
192
+ cmd = secret ? '--export-secret-keys' : '--export'
193
+ out, err, status = Open3.capture3("gpg#{armor} #{cmd} #{email}", binmode: true)
194
+ if status.success? && out.length > 0
195
+ out
196
+ else
197
+ raise(Pgp::Failure, "GPG Failed reading key: #{email}: #{err} #{out}")
198
+ end
199
+ end
200
+
201
+ # Imports the supplied public/private key
202
+ # Returns [String] the output returned from the import command
203
+ def self.import(key)
204
+ out, err, status = Open3.capture3('gpg --import', binmode: true, stdin_data: key)
205
+ if status.success? && out.length > 0
206
+ out
207
+ else
208
+ raise(Pgp::Failure, "GPG Failed importing key: #{err} #{out}")
209
+ end
210
+ end
211
+
212
+ end
213
+ end
@@ -0,0 +1,50 @@
1
+ require 'open3'
2
+
3
+ module IOStreams
4
+ module Pgp
5
+ class Reader
6
+ # Passphrase to use to open the private key to decrypt the received file
7
+ def self.default_passphrase=(default_passphrase)
8
+ @default_passphrase = default_passphrase
9
+ end
10
+
11
+ # Read from a PGP / GPG file or stream, decompressing the contents as it is read
12
+ # file_name_or_io: [String|IO]
13
+ # Name of file to read from
14
+ # Or, the IO stream to receive the decrypted contents
15
+ # passphrase: [String]
16
+ # Pass phrase for private key to decrypt the file with
17
+ def self.open(file_name_or_io, passphrase: self.default_passphrase, binary: true)
18
+ raise(ArgumentError, 'Missing both passphrase and IOStreams::Pgp::Reader.default_passphrase') unless passphrase
19
+
20
+ if IOStreams.reader_stream?(file_name_or_io)
21
+ raise(NotImplementedError, 'Can only PGP Decrypt directly from a file name. Input streams are not yet supported.')
22
+ else
23
+ # Read decrypted contents from stdout
24
+ Open3.popen3("gpg --batch --no-tty --yes --decrypt --passphrase-fd 0 #{file_name_or_io}") do |stdin, stdout, stderr, waith_thr|
25
+ stdin.puts(passphrase) if passphrase
26
+ stdin.close
27
+ result =
28
+ begin
29
+ stdout.binmode if binary
30
+ yield(stdout)
31
+ rescue Errno::EPIPE
32
+ # Ignore broken pipe because gpg terminates early due to an error
33
+ raise(Pgp::Failure, "GPG Failed reading from encrypted file: #{file_name_or_io}: #{stderr.read.chomp}")
34
+ end
35
+ raise(Pgp::Failure, "GPG Failed to decrypt file: #{file_name_or_io}: #{stderr.read.chomp}") unless waith_thr.value.success?
36
+ result
37
+ end
38
+ end
39
+ end
40
+
41
+ private
42
+
43
+ @default_passphrase = nil
44
+
45
+ def self.default_passphrase
46
+ @default_passphrase
47
+ end
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,93 @@
1
+ require 'open3'
2
+
3
+ module IOStreams
4
+ module Pgp
5
+ class Writer
6
+ # Sign all encrypted files with this users key.
7
+ # Default: Do not sign encyrpted files.
8
+ def self.default_signer=(default_signer)
9
+ @default_signer = default_signer
10
+ end
11
+
12
+ # Passphrase to use to open the private key when signing the file.
13
+ # Default: None.
14
+ def self.default_signer_passphrase=(default_signer_passphrase)
15
+ @default_signer_passphrase = default_signer_passphrase
16
+ end
17
+
18
+ # Write to a PGP / GPG file or stream, encrypting the contents as it is written
19
+ #
20
+ # file_name_or_io: [String|IO]
21
+ # Name of file to write to.
22
+ # Or, the IO stream to write the encrypted contents to.
23
+ #
24
+ # recipient: [String]
25
+ # Email of user for which to encypt the file.
26
+ #
27
+ # signer: [String]
28
+ # Name of user with which to sign the encypted file.
29
+ # Default: default_signer or do not sign.
30
+ #
31
+ # signer_passphrase: [String]
32
+ # Passphrase to use to open the private key when signing the file.
33
+ # Default: default_signer_passphrase
34
+ #
35
+ # binary: [true|false]
36
+ # Whether to write binary data.
37
+ # Default: true
38
+ #
39
+ # compression: [:none|:zip|:zlib|:bzip2]
40
+ # Note: Standard PGP only supports :zip.
41
+ # :zlib is better than zip.
42
+ # :bzip2 is best, but uses a lot of memory.
43
+ # Default: :zip
44
+ #
45
+ # compress_level: [Integer]
46
+ # Compression level
47
+ # Default: 6
48
+ def self.open(file_name_or_io, recipient:, signer: default_signer, signer_passphrase: default_signer_passphrase, binary: true, compression: :zip, compress_level: 6)
49
+ compress_level = 0 if compression == :none
50
+ if IOStreams.writer_stream?(file_name_or_io)
51
+ raise(NotImplementedError, 'Can only PGP Encrypt directly to a file name. Output to streams are not yet supported.')
52
+ else
53
+ # Write to stdin, with encrypted contents being written to the file
54
+ cmd = "gpg --batch --no-tty --yes --encrypt"
55
+ cmd << " --sign --local-user \"#{signer}\"" if signer
56
+ cmd << " --passphrase \"#{signer_passphrase}\"" if signer_passphrase
57
+ cmd << " -z #{compress_level}" if compress_level != 6
58
+ cmd << " --compress-algo #{compression}" unless compression == :none
59
+ cmd << " --recipient \"#{recipient}\" -o \"#{file_name_or_io}\""
60
+ Open3.popen2e(cmd) do |stdin, out, waith_thr|
61
+ begin
62
+ stdin.binmode if binary
63
+ yield(stdin)
64
+ stdin.close
65
+ rescue Errno::EPIPE
66
+ # Ignore broken pipe because gpg terminates early due to an error
67
+ ::File.delete(file_name_or_io)
68
+ raise(Pgp::Failure, "GPG Failed writing to encrypted file: #{file_name_or_io}: #{out.read.chomp}")
69
+ end
70
+ unless waith_thr.value.success?
71
+ ::File.delete(file_name_or_io) if ::File.exist?(file_name_or_io)
72
+ raise(Pgp::Failure, "GPG Failed to create encrypted file: #{file_name_or_io}: #{out.read.chomp}")
73
+ end
74
+ end
75
+ end
76
+ end
77
+
78
+ private
79
+
80
+ @default_signer_passphrase = nil
81
+ @default_signer = nil
82
+
83
+ def self.default_signer_passphrase
84
+ @default_signer_passphrase
85
+ end
86
+
87
+ def self.default_signer
88
+ @default_signer
89
+ end
90
+
91
+ end
92
+ end
93
+ end
@@ -0,0 +1,67 @@
1
+ module IOStreams
2
+ # Example:
3
+ # IOStreams::SFTP::Reader.open(
4
+ # 'file.txt',
5
+ # user: 'jbloggs',
6
+ # password: 'secret',
7
+ # host: 'example.org'
8
+ # ) do |input|
9
+ # puts input.read
10
+ # end
11
+ module SFTP
12
+ class Reader
13
+ include SemanticLogger::Loggable if defined?(SemanticLogger)
14
+
15
+ # Stream to a remote file over sftp.
16
+ #
17
+ # file_name: [String]
18
+ # Name of file to write to.
19
+ #
20
+ # user: [String]
21
+ # Name of user to login with.
22
+ #
23
+ # password: [String]
24
+ # Password for the user.
25
+ #
26
+ # host: [String]
27
+ # Name of the host to connect to.
28
+ #
29
+ # port: [Integer]
30
+ # Port to connect to at the above host.
31
+ #
32
+ # binary [true|false]
33
+ # Whether to write in binary mode
34
+ # Default: true
35
+ #
36
+ # options: [Hash]
37
+ # Any options supported by Net::SSH.start
38
+ #
39
+ # Note:
40
+ # - Net::SFTP::StatusException means the file could not be read
41
+ def self.open(file_name, user:, password:, host:, port: 22, binary: true, options: {}, &block)
42
+ raise(NotImplementedError, 'Can only SFTP directly to a file name, not another stream.') if IOStreams.writer_stream?(file_name)
43
+
44
+ begin
45
+ require 'net/sftp' unless defined?(Net::SFTP)
46
+ rescue LoadError => e
47
+ raise(LoadError, "Please install the 'net-sftp' gem for SFTP streaming support. #{e.message}")
48
+ end
49
+
50
+ options = options.dup
51
+ options[:logger] ||= self.logger if defined?(SemanticLogger)
52
+ options[:port] ||= 22
53
+ options[:max_pkt_size] ||= 65536
54
+ options[:password] = password
55
+ options[:port] = port
56
+ mode = binary ? 'rb' : 'r'
57
+
58
+ result = nil
59
+ Net::SFTP.start(host, user, options) do |sftp|
60
+ result = sftp.file.open(file_name, mode, &block)
61
+ end
62
+ result
63
+ end
64
+
65
+ end
66
+ end
67
+ end
@@ -0,0 +1,68 @@
1
+ module IOStreams
2
+ # Example:
3
+ # IOStreams::SFTP::Writer.open('file.txt',
4
+ # user: 'jbloggs',
5
+ # password: 'secret',
6
+ # host: 'example.org',
7
+ # options: {compression: false}
8
+ # ) do |output|
9
+ # output.write('Hello World')
10
+ # end
11
+ module SFTP
12
+ class Writer
13
+ include SemanticLogger::Loggable if defined?(SemanticLogger)
14
+
15
+ # Stream to a remote file over sftp.
16
+ #
17
+ # file_name: [String]
18
+ # Name of file to write to.
19
+ #
20
+ # user: [String]
21
+ # Name of user to login with.
22
+ #
23
+ # password: [String]
24
+ # Password for the user.
25
+ #
26
+ # host: [String]
27
+ # Name of the host to connect to.
28
+ #
29
+ # port: [Integer]
30
+ # Port to connect to at the above host.
31
+ #
32
+ # mkdir [true|false]
33
+ # Whether to create the output directory on the target system before writing the file.
34
+ # The path is created recursively if any portions of the path that are missing.
35
+ # Default: false
36
+ #
37
+ # binary [true|false]
38
+ # Whether to write in binary mode
39
+ # Default: true
40
+ #
41
+ # options: [Hash]
42
+ # Any options supported by Net::SSH.start
43
+ def self.open(file_name, user:, password:, host:, port: 22, mkdir: false, binary: true, options: {}, &block)
44
+ raise(NotImplementedError, 'Can only SFTP directly to a file name, not another stream.') if IOStreams.writer_stream?(file_name)
45
+
46
+ begin
47
+ require 'net/sftp' unless defined?(Net::SFTP)
48
+ rescue LoadError => e
49
+ raise(LoadError, "Please install the 'net-sftp' gem for SFTP streaming support. #{e.message}")
50
+ end
51
+
52
+ options = options.dup
53
+ options[:logger] ||= logger if defined?(SemanticLogger)
54
+ options[:port] ||= 22
55
+ options[:max_pkt_size] ||= 65536
56
+ options[:password] = password
57
+ options[:port] = port
58
+ mode = binary ? 'wb' : 'w'
59
+
60
+ Net::SFTP.start(host, user, options) do |sftp|
61
+ sftp.session.exec!("mkdir -p '#{File.dirname(file_name)}'") if mkdir
62
+ sftp.file.open(file_name, mode, & block)
63
+ end
64
+ end
65
+
66
+ end
67
+ end
68
+ end
@@ -92,7 +92,7 @@ module IOStreams
92
92
  # RocketJob::Formatter::Formats.streams_for_file_name('myfile.csv')
93
93
  # => [ :file ]
94
94
  def streams_for_file_name(file_name)
95
- raise ArgumentError.new("RocketJob Cannot detect file format when uploading to stream: #{file_name.inspect}") if file_name.respond_to?(:read)
95
+ raise ArgumentError.new("RocketJob Cannot detect file format when uploading to stream: #{file_name.inspect}") if reader_stream?(file_name)
96
96
 
97
97
  parts = file_name.split('.')
98
98
  extensions = []
@@ -1,3 +1,3 @@
1
1
  module IOStreams #:nodoc
2
- VERSION = '0.9.1'
2
+ VERSION = '0.10.0'
3
3
  end
@@ -13,17 +13,13 @@ module IOStreams
13
13
  # puts line
14
14
  # end
15
15
  # end
16
- def self.open(file_name_or_io, options={}, &block)
16
+ def self.open(file_name_or_io, buffer_size: 65536, &block)
17
17
  begin
18
18
  require 'creek' unless defined?(Creek::Book)
19
19
  rescue LoadError => e
20
20
  raise(LoadError, "Please install the 'creek' gem for xlsx streaming support. #{e.message}")
21
21
  end
22
22
 
23
- options = options.dup
24
- buffer_size = options.delete(:buffer_size) || 65536
25
- raise(ArgumentError, "Unknown IOStreams::Xlsx::Reader option: #{options.inspect}") if options.size > 0
26
-
27
23
  if IOStreams.reader_stream?(file_name_or_io)
28
24
  temp_file = Tempfile.new('rocket_job_xlsx')
29
25
  file_name = temp_file.to_path
@@ -12,11 +12,7 @@ module IOStreams
12
12
  # puts data
13
13
  # end
14
14
  # end
15
- def self.open(file_name_or_io, options={}, &block)
16
- options = options.dup
17
- buffer_size = options.delete(:buffer_size) || 65536
18
- raise(ArgumentError, "Unknown IOStreams::Zip::Reader option: #{options.inspect}") if options.size > 0
19
-
15
+ def self.open(file_name_or_io, buffer_size: 65536, &block)
20
16
  if !defined?(JRuby) && !defined?(::Zip)
21
17
  # MRI needs Ruby Zip, since it only has native support for GZip
22
18
  begin
@@ -7,8 +7,7 @@ module IOStreams
7
7
  # file_name_or_io [String]
8
8
  # Full path and filename for the output zip file
9
9
  #
10
- # Options
11
- # :file_name [String]
10
+ # zip_file_name: [String]
12
11
  # Name of the file within the Zip Stream
13
12
  #
14
13
  # The stream supplied to the block only responds to #write
@@ -22,14 +21,9 @@ module IOStreams
22
21
  # Notes:
23
22
  # - Since Zip cannot write to streams, if a stream is supplied, a temp file
24
23
  # is automatically created under the covers
25
- def self.open(file_name_or_io, options={}, &block)
26
- options = options.dup
27
- zip_file_name = options.delete(:file_name) || options.delete(:zip_file_name)
28
- buffer_size = options.delete(:buffer_size) || 65536
29
- raise(ArgumentError, "Unknown IOStreams::Zip::Writer option: #{options.inspect}") if options.size > 0
30
-
24
+ def self.open(file_name_or_io, zip_file_name: nil, buffer_size: 65536, &block)
31
25
  # Default the name of the file within the zip to the supplied file_name without the zip extension
32
- zip_file_name = file_name_or_io.to_s[0..-5] if zip_file_name.nil? && !file_name_or_io.respond_to?(:write) && (file_name_or_io =~ /\.(zip)\z/)
26
+ zip_file_name = file_name_or_io.to_s[0..-5] if zip_file_name.nil? && !IOStreams.writer_stream?(file_name_or_io) && (file_name_or_io =~ /\.(zip)\z/)
33
27
  zip_file_name ||= 'file'
34
28
 
35
29
  if !defined?(JRuby) && !defined?(::Zip)
@@ -12,6 +12,11 @@ module IOStreams
12
12
  autoload :Reader, 'io_streams/gzip/reader'
13
13
  autoload :Writer, 'io_streams/gzip/writer'
14
14
  end
15
+ autoload :Pgp, 'io_streams/pgp'
16
+ module SFTP
17
+ autoload :Reader, 'io_streams/sftp/reader'
18
+ autoload :Writer, 'io_streams/sftp/writer'
19
+ end
15
20
  module Zip
16
21
  autoload :Reader, 'io_streams/zip/reader'
17
22
  autoload :Writer, 'io_streams/zip/writer'
@@ -0,0 +1,44 @@
1
+ require_relative 'test_helper'
2
+
3
+ module Streams
4
+ class PgpReaderTest < Minitest::Test
5
+ describe IOStreams::Pgp::Reader do
6
+ before do
7
+ file_name = File.join(File.dirname(__FILE__), 'files', 'text.txt')
8
+ @data = File.read(file_name)
9
+ @temp_file = Tempfile.new('iostreams')
10
+ @file_name = @temp_file.to_path
11
+ end
12
+
13
+ after do
14
+ @temp_file.delete if @temp_file
15
+ end
16
+
17
+ describe '.open' do
18
+ it 'reads encrypted file' do
19
+ IOStreams::Pgp::Writer.open(@file_name, recipient: 'receiver@example.org') do |io|
20
+ io.write(@data)
21
+ end
22
+
23
+ result = IOStreams::Pgp::Reader.open(@file_name, passphrase: 'receiver_passphrase') { |file| file.read }
24
+ assert_equal @data, result
25
+ end
26
+
27
+ it 'fails with bad passphrase' do
28
+ assert_raises IOStreams::Pgp::Failure do
29
+ IOStreams::Pgp::Reader.open(@file_name, passphrase: 'BAD') { |file| file.read }
30
+ end
31
+ end
32
+
33
+ it 'fails with stream input' do
34
+ io = StringIO.new
35
+ assert_raises NotImplementedError do
36
+ IOStreams::Pgp::Reader.open(io, passphrase: 'BAD') { |file| file.read }
37
+ end
38
+ end
39
+
40
+ end
41
+
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,91 @@
1
+ require_relative 'test_helper'
2
+
3
+ module Streams
4
+ class PgpWriterTest < Minitest::Test
5
+ describe IOStreams::Pgp::Writer do
6
+ before do
7
+ file_name = File.join(File.dirname(__FILE__), 'files', 'text.txt')
8
+ @data = File.read(file_name)
9
+
10
+ @temp_file = Tempfile.new('iostreams')
11
+ @file_name = @temp_file.to_path
12
+ end
13
+
14
+ after do
15
+ @temp_file.delete if @temp_file
16
+ end
17
+
18
+ describe '.open' do
19
+ it 'writes encrypted text file' do
20
+ IOStreams::Pgp::Writer.open(@file_name, recipient: 'receiver@example.org', binary: false) do |io|
21
+ io.write(@data)
22
+ end
23
+
24
+ result = IOStreams::Pgp::Reader.open(@file_name, passphrase: 'receiver_passphrase', binary: false) { |file| file.read }
25
+ assert_equal @data, result
26
+ end
27
+
28
+ it 'writes encrypted binary file' do
29
+ binary_file_name = File.join(File.dirname(__FILE__), 'files', 'spreadsheet.xlsx')
30
+ binary_data = File.open(binary_file_name, 'rb') { |file| file.read }
31
+
32
+ File.open(binary_file_name, 'rb') do |input|
33
+ IOStreams::Pgp::Writer.open(@file_name, recipient: 'receiver@example.org') do |output|
34
+ IOStreams.copy(input, output, 65535)
35
+ end
36
+ end
37
+
38
+ result = IOStreams::Pgp::Reader.open(@file_name, passphrase: 'receiver_passphrase') { |file| file.read }
39
+ assert_equal binary_data, result
40
+ end
41
+
42
+ it 'writes and signs encrypted file' do
43
+ IOStreams::Pgp::Writer.open(@file_name, recipient: 'receiver@example.org', signer: 'sender@example.org', signer_passphrase: 'sender_passphrase') do |io|
44
+ io.write(@data)
45
+ end
46
+
47
+ result = IOStreams::Pgp::Reader.open(@file_name, passphrase: 'receiver_passphrase') { |file| file.read }
48
+ assert_equal @data, result
49
+ end
50
+
51
+ it 'fails with bad signer passphrase' do
52
+ assert_raises IOStreams::Pgp::Failure do
53
+ IOStreams::Pgp::Writer.open(@file_name, recipient: 'receiver@example.org', signer: 'sender@example.org', signer_passphrase: 'BAD') do |io|
54
+ io.write(@data)
55
+ end
56
+ end
57
+ end
58
+
59
+ it 'fails with bad recipient' do
60
+ assert_raises IOStreams::Pgp::Failure do
61
+ IOStreams::Pgp::Writer.open(@file_name, recipient: 'BAD@example.org', signer: 'sender@example.org', signer_passphrase: 'sender_passphrase') do |io|
62
+ io.write(@data)
63
+ # Allow process to terminate
64
+ sleep 1
65
+ io.write(@data)
66
+ end
67
+ end
68
+ end
69
+
70
+ it 'fails with bad signer' do
71
+ assert_raises IOStreams::Pgp::Failure do
72
+ IOStreams::Pgp::Writer.open(@file_name, recipient: 'receiver@example.org', signer: 'BAD@example.org', signer_passphrase: 'sender_passphrase') do |io|
73
+ io.write(@data)
74
+ end
75
+ end
76
+ end
77
+
78
+ it 'fails with stream output' do
79
+ io = StringIO.new
80
+ assert_raises NotImplementedError do
81
+ IOStreams::Pgp::Writer.open(io, recipient: 'receiver@example.org') do |io|
82
+ io.write(@data)
83
+ end
84
+ end
85
+ end
86
+
87
+ end
88
+
89
+ end
90
+ end
91
+ end
@@ -17,3 +17,12 @@ SymmetricEncryption.cipher = SymmetricEncryption::Cipher.new(
17
17
  encoding: :base64strict
18
18
  )
19
19
 
20
+ # Test PGP Keys
21
+ unless IOStreams::Pgp.has_key?(email: 'sender@example.org')
22
+ puts 'Generating test PGP key: sender@example.org'
23
+ IOStreams::Pgp.generate_key(name: 'Sender', email: 'sender@example.org', passphrase: 'sender_passphrase', key_length: 2048)
24
+ end
25
+ unless IOStreams::Pgp.has_key?(email: 'receiver@example.org')
26
+ puts 'Generating test PGP key: receiver@example.org'
27
+ IOStreams::Pgp.generate_key(name: 'Receiver', email: 'receiver@example.org', passphrase: 'receiver_passphrase', key_length: 2048)
28
+ end
@@ -33,7 +33,7 @@ module Streams
33
33
  result = nil
34
34
  begin
35
35
  zin = ::Zip::InputStream.new(io)
36
- entry = zin.get_next_entry
36
+ zin.get_next_entry
37
37
  result = zin.read
38
38
  ensure
39
39
  zin.close if zin
metadata CHANGED
@@ -1,29 +1,15 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: iostreams
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.9.1
4
+ version: 0.10.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Reid Morrison
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2016-02-20 00:00:00.000000000 Z
11
+ date: 2016-09-27 00:00:00.000000000 Z
12
12
  dependencies:
13
- - !ruby/object:Gem::Dependency
14
- name: symmetric-encryption
15
- requirement: !ruby/object:Gem::Requirement
16
- requirements:
17
- - - "~>"
18
- - !ruby/object:Gem::Version
19
- version: '3.0'
20
- type: :runtime
21
- prerelease: false
22
- version_requirements: !ruby/object:Gem::Requirement
23
- requirements:
24
- - - "~>"
25
- - !ruby/object:Gem::Version
26
- version: '3.0'
27
13
  - !ruby/object:Gem::Dependency
28
14
  name: concurrent-ruby
29
15
  requirement: !ruby/object:Gem::Requirement
@@ -56,6 +42,11 @@ files:
56
42
  - lib/io_streams/gzip/reader.rb
57
43
  - lib/io_streams/gzip/writer.rb
58
44
  - lib/io_streams/io_streams.rb
45
+ - lib/io_streams/pgp.rb
46
+ - lib/io_streams/pgp/reader.rb
47
+ - lib/io_streams/pgp/writer.rb
48
+ - lib/io_streams/sftp/reader.rb
49
+ - lib/io_streams/sftp/writer.rb
59
50
  - lib/io_streams/streams.rb
60
51
  - lib/io_streams/version.rb
61
52
  - lib/io_streams/xlsx/reader.rb
@@ -76,11 +67,13 @@ files:
76
67
  - test/files/text.zip
77
68
  - test/gzip_reader_test.rb
78
69
  - test/gzip_writer_test.rb
70
+ - test/pgp_reader_test.rb
71
+ - test/pgp_writer_test.rb
79
72
  - test/test_helper.rb
80
73
  - test/xlsx_reader_test.rb
81
74
  - test/zip_reader_test.rb
82
75
  - test/zip_writer_test.rb
83
- homepage: https://github.com/rocketjob/streams
76
+ homepage: https://github.com/rocketjob/iostreams
84
77
  licenses:
85
78
  - Apache-2.0
86
79
  metadata: {}
@@ -92,7 +85,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
92
85
  requirements:
93
86
  - - ">="
94
87
  - !ruby/object:Gem::Version
95
- version: '0'
88
+ version: '2.1'
96
89
  required_rubygems_version: !ruby/object:Gem::Requirement
97
90
  requirements:
98
91
  - - ">="
@@ -100,10 +93,11 @@ required_rubygems_version: !ruby/object:Gem::Requirement
100
93
  version: '0'
101
94
  requirements: []
102
95
  rubyforge_project:
103
- rubygems_version: 2.4.8
96
+ rubygems_version: 2.5.1
104
97
  signing_key:
105
98
  specification_version: 4
106
- summary: Ruby Input and Output streaming with support for Zip, Gzip, and Encryption.
99
+ summary: Ruby file streaming. Supports Text, Zip, Gzip, Xlsx, csv, PGP / GPG and Symmetric
100
+ Encryption.
107
101
  test_files:
108
102
  - test/csv_reader_test.rb
109
103
  - test/csv_writer_test.rb
@@ -119,6 +113,8 @@ test_files:
119
113
  - test/files/text.zip
120
114
  - test/gzip_reader_test.rb
121
115
  - test/gzip_writer_test.rb
116
+ - test/pgp_reader_test.rb
117
+ - test/pgp_writer_test.rb
122
118
  - test/test_helper.rb
123
119
  - test/xlsx_reader_test.rb
124
120
  - test/zip_reader_test.rb