iostreams 0.9.1 → 0.10.0

Sign up to get free protection for your applications and to get access to all the features.
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