iostreams 0.7.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 +7 -0
- data/README.md +56 -0
- data/Rakefile +28 -0
- data/lib/io_streams/file/reader.rb +15 -0
- data/lib/io_streams/file/writer.rb +15 -0
- data/lib/io_streams/gzip/reader.rb +20 -0
- data/lib/io_streams/gzip/writer.rb +20 -0
- data/lib/io_streams/io_streams.rb +243 -0
- data/lib/io_streams/version.rb +3 -0
- data/lib/io_streams/zip/reader.rb +82 -0
- data/lib/io_streams/zip/writer.rb +86 -0
- data/lib/iostreams.rb +16 -0
- data/test/file_reader_test.rb +31 -0
- data/test/file_writer_test.rb +33 -0
- data/test/files/text.txt +3 -0
- data/test/files/text.txt.gz +0 -0
- data/test/files/text.txt.gz.zip +0 -0
- data/test/files/text.zip +0 -0
- data/test/gzip_reader_test.rb +32 -0
- data/test/gzip_writer_test.rb +38 -0
- data/test/test_helper.rb +20 -0
- data/test/zip_reader_test.rb +33 -0
- data/test/zip_writer_test.rb +47 -0
- metadata +105 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: dbaf3d597b1fc3ad00a9c371f932236332d3a1b4
|
4
|
+
data.tar.gz: ee333efa4fe7737d77dfdaeff353e4811fa4f298
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 801f73a031c8fbadd38bf7939fa90dd6fca5360fef7c8f77b08c65ed72bdb142deaecccf8c9a3dd2c7f47af4725397635959cdb14a87e4af603967ced96709a2
|
7
|
+
data.tar.gz: 7abef1b3814194b1fa69d05e5c007aaca2b386329fb2d4af3a228dcedccd465501c5a2036f510b7e3602c96ad54b030fe7385dbb096a566e3a26ead686d3f167
|
data/README.md
ADDED
@@ -0,0 +1,56 @@
|
|
1
|
+
# iostreams
|
2
|
+
|
3
|
+
Ruby Input and Output streaming with support for Zip, Gzip, and Encryption.
|
4
|
+
|
5
|
+
## Status
|
6
|
+
|
7
|
+
Alpha - Feedback on the API is welcome. API will change.
|
8
|
+
|
9
|
+
## Introduction
|
10
|
+
|
11
|
+
`iostreams` allows files to be read and written in a streaming fashion to reduce
|
12
|
+
memory overhead. It supports reading and writing of Zip, GZip and encrypted files.
|
13
|
+
|
14
|
+
These streams can be chained together just like piped programs in linux.
|
15
|
+
This allows one stream to read the file, another stream to decrypt the file and
|
16
|
+
then a third stream to decompress the result.
|
17
|
+
|
18
|
+
The objective is that all of these streaming processes are performed used streaming
|
19
|
+
so that only portions of the file are loaded into memory at a time.
|
20
|
+
Where possible each stream never goes to disk, which for example could expose
|
21
|
+
un-encrypted data.
|
22
|
+
|
23
|
+
## Notes
|
24
|
+
|
25
|
+
* Due to the nature of Zip, both its Reader and Writer methods will create
|
26
|
+
a temp file when reading from or writing to a stream.
|
27
|
+
Recommended to use Gzip over Zip since it can be streamed.
|
28
|
+
|
29
|
+
## Meta
|
30
|
+
|
31
|
+
* Code: `git clone git://github.com/rocketjob/iostreams.git`
|
32
|
+
* Home: <https://github.com/rocketjob/iostreams>
|
33
|
+
* Issues: <http://github.com/rocketjob/iostreams/issues>
|
34
|
+
* Gems: <http://rubygems.org/gems/iostreams>
|
35
|
+
|
36
|
+
This project uses [Semantic Versioning](http://semver.org/).
|
37
|
+
|
38
|
+
## Author
|
39
|
+
|
40
|
+
[Reid Morrison](https://github.com/reidmorrison)
|
41
|
+
|
42
|
+
## License
|
43
|
+
|
44
|
+
Copyright 2015 Reid Morrison
|
45
|
+
|
46
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
47
|
+
you may not use this file except in compliance with the License.
|
48
|
+
You may obtain a copy of the License at
|
49
|
+
|
50
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
51
|
+
|
52
|
+
Unless required by applicable law or agreed to in writing, software
|
53
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
54
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
55
|
+
See the License for the specific language governing permissions and
|
56
|
+
limitations under the License.
|
data/Rakefile
ADDED
@@ -0,0 +1,28 @@
|
|
1
|
+
require 'rake/clean'
|
2
|
+
require 'rake/testtask'
|
3
|
+
|
4
|
+
$LOAD_PATH.unshift File.expand_path("../lib", __FILE__)
|
5
|
+
require 'io_streams/version'
|
6
|
+
|
7
|
+
task :gem do
|
8
|
+
system "gem build iostreams.gemspec"
|
9
|
+
end
|
10
|
+
|
11
|
+
task :publish => :gem do
|
12
|
+
system "git tag -a v#{IOStreams::VERSION} -m 'Tagging #{IOStreams::VERSION}'"
|
13
|
+
system "git push --tags"
|
14
|
+
system "gem push iostreams-#{IOStreams::VERSION}.gem"
|
15
|
+
system "rm iostreams-#{IOStreams::VERSION}.gem"
|
16
|
+
end
|
17
|
+
|
18
|
+
desc "Run Test Suite"
|
19
|
+
task :test do
|
20
|
+
Rake::TestTask.new(:functional) do |t|
|
21
|
+
t.test_files = FileList['test/**/*_test.rb']
|
22
|
+
t.verbose = true
|
23
|
+
end
|
24
|
+
|
25
|
+
Rake::Task['functional'].invoke
|
26
|
+
end
|
27
|
+
|
28
|
+
task :default => :test
|
@@ -0,0 +1,15 @@
|
|
1
|
+
module IOStreams
|
2
|
+
module File
|
3
|
+
class Reader
|
4
|
+
# Read from a file or stream
|
5
|
+
def self.open(file_name_or_io, _=nil, &block)
|
6
|
+
unless file_name_or_io.respond_to?(:read)
|
7
|
+
::File.open(file_name_or_io, 'rb', &block)
|
8
|
+
else
|
9
|
+
block.call(file_name_or_io)
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
module IOStreams
|
2
|
+
module File
|
3
|
+
class Writer
|
4
|
+
# Write to a file or stream
|
5
|
+
def self.open(file_name_or_io, _=nil, &block)
|
6
|
+
unless file_name_or_io.respond_to?(:write)
|
7
|
+
::File.open(file_name_or_io, 'wb', &block)
|
8
|
+
else
|
9
|
+
block.call(file_name_or_io)
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
module IOStreams
|
2
|
+
module Gzip
|
3
|
+
class Reader
|
4
|
+
# Read from a gzip file or stream, decompressing the contents as it is read
|
5
|
+
def self.open(file_name_or_io, _=nil, &block)
|
6
|
+
unless file_name_or_io.respond_to?(:read)
|
7
|
+
::Zlib::GzipReader.open(file_name_or_io, &block)
|
8
|
+
else
|
9
|
+
begin
|
10
|
+
io = ::Zlib::GzipReader.new(file_name_or_io)
|
11
|
+
block.call(io)
|
12
|
+
ensure
|
13
|
+
io.close if io && (io.respond_to?(:closed?) && !io.closed?)
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
module IOStreams
|
2
|
+
module Gzip
|
3
|
+
class Writer
|
4
|
+
# Write to a file / stream, compressing with GZip
|
5
|
+
def self.open(file_name_or_io, _=nil, &block)
|
6
|
+
unless file_name_or_io.respond_to?(:write)
|
7
|
+
Zlib::GzipWriter.open(file_name_or_io, &block)
|
8
|
+
else
|
9
|
+
begin
|
10
|
+
io = Zlib::GzipWriter.new(file_name_or_io)
|
11
|
+
block.call(io)
|
12
|
+
ensure
|
13
|
+
io.close if io && (io.respond_to?(:closed?) && !io.closed?)
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,243 @@
|
|
1
|
+
require 'thread_safe'
|
2
|
+
module IOStreams
|
3
|
+
# A registry to hold formats for processing files during upload or download
|
4
|
+
@@extensions = ThreadSafe::Hash.new
|
5
|
+
|
6
|
+
# Returns [Array] the formats required to process the file by looking at
|
7
|
+
# its extension(s)
|
8
|
+
#
|
9
|
+
# Extensions supported:
|
10
|
+
# .zip Zip File [ :zip ]
|
11
|
+
# .gz, .gzip GZip File [ :gzip ]
|
12
|
+
# .enc File Encrypted using symmetric encryption [ :enc ]
|
13
|
+
# other All other extensions will be returned as: [ :file ]
|
14
|
+
#
|
15
|
+
# When a file is encrypted, it may also be compressed:
|
16
|
+
# .zip.enc [ :zip, :enc ]
|
17
|
+
# .gz.enc [ :gz, :enc ]
|
18
|
+
#
|
19
|
+
# Example Zip file:
|
20
|
+
# RocketJob::Formatter::Formats.streams_for_file_name('myfile.zip')
|
21
|
+
# => [ :zip ]
|
22
|
+
#
|
23
|
+
# Example Encrypted Gzip file:
|
24
|
+
# RocketJob::Formatter::Formats.streams_for_file_name('myfile.csv.gz.enc')
|
25
|
+
# => [ :gz, :enc ]
|
26
|
+
#
|
27
|
+
# Example plain text / binary file:
|
28
|
+
# RocketJob::Formatter::Formats.streams_for_file_name('myfile.csv')
|
29
|
+
# => [ :file ]
|
30
|
+
def self.streams_for_file_name(file_name)
|
31
|
+
raise ArgumentError.new("File name cannot be nil") if file_name.nil?
|
32
|
+
raise ArgumentError.new("RocketJob Cannot detect file format when uploading to stream: #{file_name.inspect}") if file_name.respond_to?(:read)
|
33
|
+
parts = file_name.split('.')
|
34
|
+
extensions = []
|
35
|
+
while extension = parts.pop
|
36
|
+
break unless @@extensions[extension.to_sym]
|
37
|
+
extensions.unshift(extension.to_sym)
|
38
|
+
end
|
39
|
+
extensions << :file if extensions.size == 0
|
40
|
+
extensions
|
41
|
+
end
|
42
|
+
|
43
|
+
Extension = Struct.new(:reader_class, :writer_class)
|
44
|
+
|
45
|
+
# Register a file extension and the reader and writer classes to use to format it
|
46
|
+
#
|
47
|
+
# Example:
|
48
|
+
# # MyXls::Reader and MyXls::Writer must implement .open
|
49
|
+
# register_extension(:xls, MyXls::Reader, MyXls::Writer)
|
50
|
+
def self.register_extension(extension, reader_class, writer_class)
|
51
|
+
raise "Invalid extension #{extension.inspect}" unless extension.to_s =~ /\A\w+\Z/
|
52
|
+
@@extensions[extension.to_sym] = Extension.new(reader_class, writer_class)
|
53
|
+
end
|
54
|
+
|
55
|
+
# De-Register a file extension
|
56
|
+
#
|
57
|
+
# Returns [Symbol] the extension removed, or nil if the extension was not registered
|
58
|
+
#
|
59
|
+
# Example:
|
60
|
+
# register_extension(:xls)
|
61
|
+
def self.deregister_extension(extension)
|
62
|
+
raise "Invalid extension #{extension.inspect}" unless extension.to_s =~ /\A\w+\Z/
|
63
|
+
@@extensions.delete(extension.to_sym)
|
64
|
+
end
|
65
|
+
|
66
|
+
# Returns a Reader for reading a file / stream
|
67
|
+
#
|
68
|
+
# Parameters
|
69
|
+
# file_name_or_io [String|IO]
|
70
|
+
# The file_name of the file to write to, or an IO Stream that implements
|
71
|
+
# #read.
|
72
|
+
#
|
73
|
+
# streams [Symbol|Array]
|
74
|
+
# The formats/streams that be used to convert the data whilst it is
|
75
|
+
# being read.
|
76
|
+
# When nil, the file_name will be inspected to try and determine what
|
77
|
+
# streams should be applied.
|
78
|
+
# Default: nil
|
79
|
+
#
|
80
|
+
# Stream types / extensions supported:
|
81
|
+
# .zip Zip File [ :zip ]
|
82
|
+
# .gz, .gzip GZip File [ :gzip ]
|
83
|
+
# .enc File Encrypted using symmetric encryption [ :enc ]
|
84
|
+
# other All other extensions will be returned as: [ :file ]
|
85
|
+
#
|
86
|
+
# When a file is encrypted, it may also be compressed:
|
87
|
+
# .zip.enc [ :zip, :enc ]
|
88
|
+
# .gz.enc [ :gz, :enc ]
|
89
|
+
#
|
90
|
+
# Example: Zip
|
91
|
+
# IOStreams.reader('myfile.zip') do |stream|
|
92
|
+
# puts stream.read
|
93
|
+
# end
|
94
|
+
#
|
95
|
+
# Example: Encrypted Zip
|
96
|
+
# IOStreams.reader('myfile.zip.enc') do |stream|
|
97
|
+
# puts stream.read
|
98
|
+
# end
|
99
|
+
#
|
100
|
+
# Example: Explicitly set the streams
|
101
|
+
# IOStreams.reader('myfile.zip.enc', [:zip, :enc]) do |stream|
|
102
|
+
# puts stream.read
|
103
|
+
# end
|
104
|
+
#
|
105
|
+
# Example: Supply custom options
|
106
|
+
# # Encrypt the file and get Symmetric Encryption to also compress it
|
107
|
+
# IOStreams.reader('myfile.csv.enc', [:enc]) do |stream|
|
108
|
+
# puts stream.read
|
109
|
+
# end
|
110
|
+
def self.reader(file_name_or_io, streams=nil, &block)
|
111
|
+
stream(:reader, file_name_or_io, streams, &block)
|
112
|
+
end
|
113
|
+
|
114
|
+
# Returns a Writer for writing to a file / stream
|
115
|
+
#
|
116
|
+
# Parameters
|
117
|
+
# file_name_or_io [String|IO]
|
118
|
+
# The file_name of the file to write to, or an IO Stream that implements
|
119
|
+
# #write.
|
120
|
+
#
|
121
|
+
# streams [Symbol|Array]
|
122
|
+
# The formats/streams that be used to convert the data whilst it is
|
123
|
+
# being written.
|
124
|
+
# When nil, the file_name will be inspected to try and determine what
|
125
|
+
# streams should be applied.
|
126
|
+
# Default: nil
|
127
|
+
#
|
128
|
+
# Stream types / extensions supported:
|
129
|
+
# .zip Zip File [ :zip ]
|
130
|
+
# .gz, .gzip GZip File [ :gzip ]
|
131
|
+
# .enc File Encrypted using symmetric encryption [ :enc ]
|
132
|
+
# other All other extensions will be returned as: [ :file ]
|
133
|
+
#
|
134
|
+
# When a file is encrypted, it may also be compressed:
|
135
|
+
# .zip.enc [ :zip, :enc ]
|
136
|
+
# .gz.enc [ :gz, :enc ]
|
137
|
+
#
|
138
|
+
# Example: Zip
|
139
|
+
# IOStreams.writer('myfile.zip') do |stream|
|
140
|
+
# stream.write(data)
|
141
|
+
# end
|
142
|
+
#
|
143
|
+
# Example: Encrypted Zip
|
144
|
+
# IOStreams.writer('myfile.zip.enc') do |stream|
|
145
|
+
# stream.write(data)
|
146
|
+
# end
|
147
|
+
#
|
148
|
+
# Example: Explicitly set the streams
|
149
|
+
# IOStreams.writer('myfile.zip.enc', [:zip, :enc]) do |stream|
|
150
|
+
# stream.write(data)
|
151
|
+
# end
|
152
|
+
#
|
153
|
+
# Example: Supply custom options
|
154
|
+
# IOStreams.writer('myfile.csv.enc', [enc: { compress: true }]) do |stream|
|
155
|
+
# stream.write(data)
|
156
|
+
# end
|
157
|
+
#
|
158
|
+
# Example: Set internal filename when creating a zip file
|
159
|
+
# IOStreams.writer('myfile.csv.zip', zip: { zip_file_name: 'myfile.csv' }) do |stream|
|
160
|
+
# stream.write(data)
|
161
|
+
# end
|
162
|
+
def self.writer(file_name_or_io, streams=nil, &block)
|
163
|
+
stream(:writer, file_name_or_io, streams, &block)
|
164
|
+
end
|
165
|
+
|
166
|
+
# Copies the source stream to the target stream
|
167
|
+
# Returns [Integer] the number of bytes copied
|
168
|
+
#
|
169
|
+
# Example:
|
170
|
+
# IOStreams.reader('a.csv') do |source_stream|
|
171
|
+
# IOStreams.writer('b.csv.enc') do |target_stream|
|
172
|
+
# IOStreams.copy(source_stream, target_stream)
|
173
|
+
# end
|
174
|
+
# end
|
175
|
+
def self.copy(source_stream, target_stream, buffer_size=65536)
|
176
|
+
bytes = 0
|
177
|
+
while data = source_stream.read(buffer_size)
|
178
|
+
break if data.size == 0
|
179
|
+
bytes += data.size
|
180
|
+
target_stream.write(data)
|
181
|
+
end
|
182
|
+
bytes
|
183
|
+
end
|
184
|
+
|
185
|
+
##########################################################################
|
186
|
+
private
|
187
|
+
|
188
|
+
# Struct to hold the Stream and options if any
|
189
|
+
StreamStruct = Struct.new(:klass, :options)
|
190
|
+
|
191
|
+
# Returns a reader or writer stream
|
192
|
+
def self.stream(type, file_name_or_io, streams=nil, &block)
|
193
|
+
unless streams
|
194
|
+
respond_to = type == :reader ? :read : :write
|
195
|
+
streams = file_name_or_io.respond_to?(respond_to) ? [ :file ] : streams_for_file_name(file_name_or_io)
|
196
|
+
end
|
197
|
+
stream_structs = streams_for(type, streams)
|
198
|
+
if stream_structs.size == 1
|
199
|
+
stream_struct = stream_structs.first
|
200
|
+
stream_struct.klass.open(file_name_or_io, stream_struct.options, &block)
|
201
|
+
else
|
202
|
+
# Daisy chain multiple streams together
|
203
|
+
last = stream_structs.inject(block){ |inner, stream_struct| -> io { stream_struct.klass.open(io, stream_struct.options, &inner) } }
|
204
|
+
last.call(file_name_or_io)
|
205
|
+
end
|
206
|
+
end
|
207
|
+
|
208
|
+
# type: :reader or :writer
|
209
|
+
def self.streams_for(type, params)
|
210
|
+
if params.is_a?(Symbol)
|
211
|
+
[ stream_struct_for_stream(type, params) ]
|
212
|
+
elsif params.is_a?(Array)
|
213
|
+
a = []
|
214
|
+
params.each do |stream|
|
215
|
+
if stream.is_a?(Hash)
|
216
|
+
stream.each_pair { |stream_sym, options| a << stream_struct_for_stream(type, stream_sym, options) }
|
217
|
+
else
|
218
|
+
a << stream_struct_for_stream(type, stream)
|
219
|
+
end
|
220
|
+
end
|
221
|
+
a
|
222
|
+
elsif params.is_a?(Hash)
|
223
|
+
a = []
|
224
|
+
params.each_pair { |stream, options| a << stream_struct_for_stream(type, stream, options) }
|
225
|
+
a
|
226
|
+
else
|
227
|
+
raise ArgumentError, "Invalid params supplied: #{params.inspect}"
|
228
|
+
end
|
229
|
+
end
|
230
|
+
|
231
|
+
def self.stream_struct_for_stream(type, stream, options={})
|
232
|
+
ext = @@extensions[stream.to_sym] || raise(ArgumentError, "Unknown Stream type: #{stream.inspect}")
|
233
|
+
klass = ext.send("#{type}_class")
|
234
|
+
StreamStruct.new(klass, options)
|
235
|
+
end
|
236
|
+
|
237
|
+
# Register File extensions
|
238
|
+
register_extension(:enc, SymmetricEncryption::Reader, SymmetricEncryption::Writer) if defined?(SymmetricEncryption)
|
239
|
+
register_extension(:file, IOStreams::File::Reader, IOStreams::File::Writer)
|
240
|
+
register_extension(:gz, IOStreams::Gzip::Reader, IOStreams::Gzip::Writer)
|
241
|
+
register_extension(:gzip, IOStreams::Gzip::Reader, IOStreams::Gzip::Writer)
|
242
|
+
register_extension(:zip, IOStreams::Zip::Reader, IOStreams::Zip::Writer)
|
243
|
+
end
|
@@ -0,0 +1,82 @@
|
|
1
|
+
module IOStreams
|
2
|
+
module Zip
|
3
|
+
class Reader
|
4
|
+
# Read from a zip file or stream, decompressing the contents as it is read
|
5
|
+
# The input stream from the first file found in the zip file is passed
|
6
|
+
# to the supplied block
|
7
|
+
#
|
8
|
+
# Example:
|
9
|
+
# IOStreams::ZipReader.open('abc.zip') do |io_stream|
|
10
|
+
# # Read 256 bytes at a time
|
11
|
+
# while data = io_stream.read(256)
|
12
|
+
# puts data
|
13
|
+
# end
|
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
|
+
|
20
|
+
# File name supplied
|
21
|
+
return read_file(file_name_or_io, &block) unless file_name_or_io.respond_to?(:read)
|
22
|
+
|
23
|
+
# Stream supplied
|
24
|
+
begin
|
25
|
+
# Since ZIP cannot be streamed, download un-zipped data to a local file before streaming
|
26
|
+
temp_file = Tempfile.new('rocket_job')
|
27
|
+
file_name = temp_file.to_path
|
28
|
+
|
29
|
+
# Stream zip stream into temp file
|
30
|
+
::File.open(file_name, 'wb') do |file|
|
31
|
+
IOStreams.copy(file_name_or_io, file, buffer_size)
|
32
|
+
end
|
33
|
+
|
34
|
+
read_file(file_name, &block)
|
35
|
+
ensure
|
36
|
+
temp_file.delete if temp_file
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
if defined?(JRuby)
|
41
|
+
# Java has built-in support for Zip files
|
42
|
+
def self.read_file(file_name, &block)
|
43
|
+
fin = Java::JavaIo::FileInputStream.new(file_name)
|
44
|
+
zin = Java::JavaUtilZip::ZipInputStream.new(fin)
|
45
|
+
zin.get_next_entry
|
46
|
+
block.call(zin.to_io)
|
47
|
+
ensure
|
48
|
+
zin.close if zin
|
49
|
+
fin.close if fin
|
50
|
+
end
|
51
|
+
|
52
|
+
else
|
53
|
+
# MRI needs Ruby Zip, since it only has native support for GZip
|
54
|
+
begin
|
55
|
+
require 'zip'
|
56
|
+
rescue LoadError => exc
|
57
|
+
puts "Please install gem rubyzip so that RocketJob can read Zip files in Ruby MRI"
|
58
|
+
raise(exc)
|
59
|
+
end
|
60
|
+
|
61
|
+
# Read from a zip file or stream, decompressing the contents as it is read
|
62
|
+
# The input stream from the first file found in the zip file is passed
|
63
|
+
# to the supplied block
|
64
|
+
def self.read_file(file_name, &block)
|
65
|
+
begin
|
66
|
+
zin = ::Zip::InputStream.new(file_name)
|
67
|
+
zin.get_next_entry
|
68
|
+
block.call(zin)
|
69
|
+
ensure
|
70
|
+
begin
|
71
|
+
zin.close if zin
|
72
|
+
rescue IOError
|
73
|
+
# Ignore file already closed errors since Zip::InputStream
|
74
|
+
# does not have a #closed? method
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
@@ -0,0 +1,86 @@
|
|
1
|
+
module IOStreams
|
2
|
+
module Zip
|
3
|
+
class Writer
|
4
|
+
# Write a single file in Zip format to the supplied output file name
|
5
|
+
#
|
6
|
+
# Parameters
|
7
|
+
# zip_file_name [String]
|
8
|
+
# Full path and filename for the output zip file
|
9
|
+
#
|
10
|
+
# file_name [String]
|
11
|
+
# Name of the file within the Zip Stream
|
12
|
+
#
|
13
|
+
# The stream supplied to the block only responds to #write
|
14
|
+
#
|
15
|
+
# Example:
|
16
|
+
# IOStreams::ZipWriter.open('myfile.zip', zip_file_name: 'myfile.txt') do |io_stream|
|
17
|
+
# io_stream.write("hello world\n")
|
18
|
+
# io_stream.write("and more\n")
|
19
|
+
# end
|
20
|
+
#
|
21
|
+
# Notes:
|
22
|
+
# - Since Zip cannot write to streams, if a stream is supplied, a temp file
|
23
|
+
# is automatically created under the covers
|
24
|
+
def self.open(file_name_or_io, options={}, &block)
|
25
|
+
options = options.dup
|
26
|
+
zip_file_name = options.delete(:zip_file_name)
|
27
|
+
buffer_size = options.delete(:buffer_size) || 65536
|
28
|
+
raise(ArgumentError, "Unknown IOStreams::Zip::Writer option: #{options.inspect}") if options.size > 0
|
29
|
+
|
30
|
+
# Default the name of the file within the zip to the supplied file_name without the zip extension
|
31
|
+
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/)
|
32
|
+
zip_file_name ||= 'file'
|
33
|
+
|
34
|
+
# File name supplied
|
35
|
+
return write_file(file_name_or_io, zip_file_name, &block) unless file_name_or_io.respond_to?(:write)
|
36
|
+
|
37
|
+
# Stream supplied
|
38
|
+
begin
|
39
|
+
# Since ZIP cannot be streamed, download to a local file before streaming
|
40
|
+
temp_file = Tempfile.new('rocket_job')
|
41
|
+
write_file(temp_file.to_path, zip_file_name, &block)
|
42
|
+
|
43
|
+
# Stream temp file into output stream
|
44
|
+
IOStreams.copy(temp_file, file_name_or_io, buffer_size)
|
45
|
+
ensure
|
46
|
+
temp_file.delete if temp_file
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
private
|
51
|
+
|
52
|
+
if defined?(JRuby)
|
53
|
+
|
54
|
+
def self.write_file(file_name, zip_file_name, &block)
|
55
|
+
out = Java::JavaIo::FileOutputStream.new(file_name)
|
56
|
+
zout = Java::JavaUtilZip::ZipOutputStream.new(out)
|
57
|
+
zout.put_next_entry(Java::JavaUtilZip::ZipEntry.new(zip_file_name))
|
58
|
+
io = zout.to_io
|
59
|
+
block.call(io)
|
60
|
+
ensure
|
61
|
+
io.close if io && !io.closed?
|
62
|
+
out.close if out
|
63
|
+
end
|
64
|
+
|
65
|
+
else
|
66
|
+
# MRI needs Ruby Zip, since it only has native support for GZip
|
67
|
+
begin
|
68
|
+
require 'zip'
|
69
|
+
rescue LoadError => exc
|
70
|
+
puts "Please install gem rubyzip so that RocketJob can read Zip files in Ruby MRI"
|
71
|
+
raise(exc)
|
72
|
+
end
|
73
|
+
|
74
|
+
def self.write_file(file_name, zip_file_name, &block)
|
75
|
+
zos = ::Zip::OutputStream.new(file_name)
|
76
|
+
zos.put_next_entry(zip_file_name)
|
77
|
+
block.call(zos)
|
78
|
+
ensure
|
79
|
+
zos.close if zos
|
80
|
+
end
|
81
|
+
|
82
|
+
end
|
83
|
+
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
data/lib/iostreams.rb
ADDED
@@ -0,0 +1,16 @@
|
|
1
|
+
require 'io_streams/version'
|
2
|
+
module IOStreams
|
3
|
+
module File
|
4
|
+
autoload :Reader, 'io_streams/file/reader'
|
5
|
+
autoload :Writer, 'io_streams/file/writer'
|
6
|
+
end
|
7
|
+
module Gzip
|
8
|
+
autoload :Reader, 'io_streams/gzip/reader'
|
9
|
+
autoload :Writer, 'io_streams/gzip/writer'
|
10
|
+
end
|
11
|
+
module Zip
|
12
|
+
autoload :Reader, 'io_streams/zip/reader'
|
13
|
+
autoload :Writer, 'io_streams/zip/writer'
|
14
|
+
end
|
15
|
+
end
|
16
|
+
require 'io_streams/io_streams'
|
@@ -0,0 +1,31 @@
|
|
1
|
+
require_relative 'test_helper'
|
2
|
+
|
3
|
+
# Unit Test for IOStreams::File
|
4
|
+
module Streams
|
5
|
+
class FileReaderTest < Minitest::Test
|
6
|
+
context IOStreams::File::Reader do
|
7
|
+
setup do
|
8
|
+
@file_name = File.join(File.dirname(__FILE__), 'files', 'text.txt')
|
9
|
+
@data = File.read(@file_name)
|
10
|
+
end
|
11
|
+
|
12
|
+
context '.open' do
|
13
|
+
should 'file' do
|
14
|
+
result = IOStreams::File::Reader.open(@file_name) do |io|
|
15
|
+
io.read
|
16
|
+
end
|
17
|
+
assert_equal @data, result
|
18
|
+
end
|
19
|
+
should 'stream' do
|
20
|
+
result = File.open(@file_name) do |file|
|
21
|
+
IOStreams::File::Reader.open(file) do |io|
|
22
|
+
io.read
|
23
|
+
end
|
24
|
+
end
|
25
|
+
assert_equal @data, result
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
require_relative 'test_helper'
|
2
|
+
|
3
|
+
# Unit Test for IOStreams::File
|
4
|
+
module Streams
|
5
|
+
class FileWriterTest < Minitest::Test
|
6
|
+
context IOStreams::File::Writer do
|
7
|
+
setup do
|
8
|
+
@file_name = File.join(File.dirname(__FILE__), 'files', 'text.txt')
|
9
|
+
@data = File.read(@file_name)
|
10
|
+
end
|
11
|
+
|
12
|
+
context '.open' do
|
13
|
+
should 'file' do
|
14
|
+
temp_file = Tempfile.new('rocket_job')
|
15
|
+
file_name = temp_file.to_path
|
16
|
+
IOStreams::File::Writer.open(file_name) do |io|
|
17
|
+
io.write(@data)
|
18
|
+
end
|
19
|
+
result = File.read(file_name)
|
20
|
+
assert_equal @data, result
|
21
|
+
end
|
22
|
+
should 'stream' do
|
23
|
+
io_string = StringIO.new
|
24
|
+
IOStreams::File::Writer.open(io_string) do |io|
|
25
|
+
io.write(@data)
|
26
|
+
end
|
27
|
+
assert_equal @data, io_string.string
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
data/test/files/text.txt
ADDED
Binary file
|
Binary file
|
data/test/files/text.zip
ADDED
Binary file
|
@@ -0,0 +1,32 @@
|
|
1
|
+
require_relative 'test_helper'
|
2
|
+
|
3
|
+
# Unit Test for IOStreams::Gzip
|
4
|
+
module Streams
|
5
|
+
class GzipReaderTest < Minitest::Test
|
6
|
+
context IOStreams::Gzip::Reader do
|
7
|
+
setup do
|
8
|
+
@file_name = File.join(File.dirname(__FILE__), 'files', 'text.txt.gz')
|
9
|
+
@gzip_data = File.open(@file_name, 'rb') { |f| f.read }
|
10
|
+
@data = Zlib::GzipReader.open(@file_name) {|gz| gz.read }
|
11
|
+
end
|
12
|
+
|
13
|
+
context '.open' do
|
14
|
+
should 'file' do
|
15
|
+
result = IOStreams::Gzip::Reader.open(@file_name) do |io|
|
16
|
+
io.read
|
17
|
+
end
|
18
|
+
assert_equal @data, result
|
19
|
+
end
|
20
|
+
should 'stream' do
|
21
|
+
result = File.open(@file_name) do |file|
|
22
|
+
IOStreams::Gzip::Reader.open(file) do |io|
|
23
|
+
io.read
|
24
|
+
end
|
25
|
+
end
|
26
|
+
assert_equal @data, result
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
require_relative 'test_helper'
|
2
|
+
|
3
|
+
# Unit Test for IOStreams::Gzip
|
4
|
+
module Streams
|
5
|
+
class GzipWriterTest < Minitest::Test
|
6
|
+
context IOStreams::Gzip::Writer do
|
7
|
+
setup do
|
8
|
+
@file_name = File.join(File.dirname(__FILE__), 'files', 'text.txt.gz')
|
9
|
+
@data = Zlib::GzipReader.open(@file_name) {|gz| gz.read }
|
10
|
+
end
|
11
|
+
|
12
|
+
context '.open' do
|
13
|
+
should 'file' do
|
14
|
+
temp_file = Tempfile.new('rocket_job')
|
15
|
+
file_name = temp_file.to_path
|
16
|
+
IOStreams::Gzip::Writer.open(file_name) do |io|
|
17
|
+
io.write(@data)
|
18
|
+
end
|
19
|
+
result = Zlib::GzipReader.open(file_name) {|gz| gz.read }
|
20
|
+
temp_file.delete
|
21
|
+
assert_equal @data, result
|
22
|
+
end
|
23
|
+
should 'stream' do
|
24
|
+
io_string = StringIO.new(''.force_encoding('ASCII-8BIT'))
|
25
|
+
IOStreams::Gzip::Writer.open(io_string) do |io|
|
26
|
+
io.write(@data)
|
27
|
+
end
|
28
|
+
io = StringIO.new(io_string.string)
|
29
|
+
gz = Zlib::GzipReader.new(io)
|
30
|
+
data = gz.read
|
31
|
+
gz.close
|
32
|
+
assert_equal @data, data
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
data/test/test_helper.rb
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
$LOAD_PATH.unshift File.dirname(__FILE__) + '/../lib'
|
2
|
+
|
3
|
+
require 'yaml'
|
4
|
+
require 'minitest/autorun'
|
5
|
+
require 'minitest/reporters'
|
6
|
+
require 'shoulda/context'
|
7
|
+
require 'iostreams'
|
8
|
+
require 'awesome_print'
|
9
|
+
require 'symmetric-encryption'
|
10
|
+
|
11
|
+
Minitest::Reporters.use! Minitest::Reporters::SpecReporter.new
|
12
|
+
|
13
|
+
# Test cipher
|
14
|
+
SymmetricEncryption.cipher = SymmetricEncryption::Cipher.new(
|
15
|
+
cipher_name: 'aes-128-cbc',
|
16
|
+
key: '1234567890ABCDEF1234567890ABCDEF',
|
17
|
+
iv: '1234567890ABCDEF',
|
18
|
+
encoding: :base64strict
|
19
|
+
)
|
20
|
+
|
@@ -0,0 +1,33 @@
|
|
1
|
+
require_relative 'test_helper'
|
2
|
+
require 'zip'
|
3
|
+
|
4
|
+
# Unit Test for IOStreams::Zip
|
5
|
+
module Streams
|
6
|
+
class ZipReaderTest < Minitest::Test
|
7
|
+
context IOStreams::Zip::Reader do
|
8
|
+
setup do
|
9
|
+
@file_name = File.join(File.dirname(__FILE__), 'files', 'text.zip')
|
10
|
+
@zip_data = File.open(@file_name, 'rb') { |f| f.read }
|
11
|
+
@data = Zip::File.open(@file_name) { |zip_file| zip_file.first.get_input_stream.read }
|
12
|
+
end
|
13
|
+
|
14
|
+
context '.open' do
|
15
|
+
should 'file' do
|
16
|
+
result = IOStreams::Zip::Reader.open(@file_name) do |io|
|
17
|
+
io.read
|
18
|
+
end
|
19
|
+
assert_equal @data, result
|
20
|
+
end
|
21
|
+
should 'stream' do
|
22
|
+
result = File.open(@file_name) do |file|
|
23
|
+
IOStreams::Zip::Reader.open(file) do |io|
|
24
|
+
io.read
|
25
|
+
end
|
26
|
+
end
|
27
|
+
assert_equal @data, result
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,47 @@
|
|
1
|
+
require_relative 'test_helper'
|
2
|
+
require 'zip'
|
3
|
+
|
4
|
+
# Unit Test for IOStreams::Zip
|
5
|
+
module Streams
|
6
|
+
class ZipWriterTest < Minitest::Test
|
7
|
+
context IOStreams::Zip::Writer do
|
8
|
+
setup do
|
9
|
+
file_name = File.join(File.dirname(__FILE__), 'files', 'text.txt')
|
10
|
+
@data = File.read(file_name)
|
11
|
+
end
|
12
|
+
|
13
|
+
context '.open' do
|
14
|
+
should 'file' do
|
15
|
+
temp_file = Tempfile.new('rocket_job')
|
16
|
+
file_name = temp_file.to_path
|
17
|
+
IOStreams::Zip::Writer.open(file_name, zip_file_name: 'text.txt') do |io|
|
18
|
+
io.write(@data)
|
19
|
+
end
|
20
|
+
result = Zip::File.open(file_name) do |zip_file|
|
21
|
+
zip_file.first.get_input_stream.read
|
22
|
+
end
|
23
|
+
temp_file.delete
|
24
|
+
assert_equal @data, result
|
25
|
+
end
|
26
|
+
|
27
|
+
should 'stream' do
|
28
|
+
io_string = StringIO.new(''.force_encoding('ASCII-8BIT'))
|
29
|
+
IOStreams::Zip::Writer.open(io_string) do |io|
|
30
|
+
io.write(@data)
|
31
|
+
end
|
32
|
+
io = StringIO.new(io_string.string)
|
33
|
+
result = nil
|
34
|
+
begin
|
35
|
+
zin = ::Zip::InputStream.new(io)
|
36
|
+
entry = zin.get_next_entry
|
37
|
+
result = zin.read
|
38
|
+
ensure
|
39
|
+
zin.close if zin
|
40
|
+
end
|
41
|
+
assert_equal @data, result
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
metadata
ADDED
@@ -0,0 +1,105 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: iostreams
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.7.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Reid Morrison
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2015-07-14 00:00:00.000000000 Z
|
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
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: thread_safe
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - ">="
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '0'
|
34
|
+
type: :runtime
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - ">="
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '0'
|
41
|
+
description:
|
42
|
+
email:
|
43
|
+
- reidmo@gmail.com
|
44
|
+
executables: []
|
45
|
+
extensions: []
|
46
|
+
extra_rdoc_files: []
|
47
|
+
files:
|
48
|
+
- README.md
|
49
|
+
- Rakefile
|
50
|
+
- lib/io_streams/file/reader.rb
|
51
|
+
- lib/io_streams/file/writer.rb
|
52
|
+
- lib/io_streams/gzip/reader.rb
|
53
|
+
- lib/io_streams/gzip/writer.rb
|
54
|
+
- lib/io_streams/io_streams.rb
|
55
|
+
- lib/io_streams/version.rb
|
56
|
+
- lib/io_streams/zip/reader.rb
|
57
|
+
- lib/io_streams/zip/writer.rb
|
58
|
+
- lib/iostreams.rb
|
59
|
+
- test/file_reader_test.rb
|
60
|
+
- test/file_writer_test.rb
|
61
|
+
- test/files/text.txt
|
62
|
+
- test/files/text.txt.gz
|
63
|
+
- test/files/text.txt.gz.zip
|
64
|
+
- test/files/text.zip
|
65
|
+
- test/gzip_reader_test.rb
|
66
|
+
- test/gzip_writer_test.rb
|
67
|
+
- test/test_helper.rb
|
68
|
+
- test/zip_reader_test.rb
|
69
|
+
- test/zip_writer_test.rb
|
70
|
+
homepage: https://github.com/rocketjob/streams
|
71
|
+
licenses:
|
72
|
+
- Apache License V2.0
|
73
|
+
metadata: {}
|
74
|
+
post_install_message:
|
75
|
+
rdoc_options: []
|
76
|
+
require_paths:
|
77
|
+
- lib
|
78
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - ">="
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: '0'
|
83
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
84
|
+
requirements:
|
85
|
+
- - ">="
|
86
|
+
- !ruby/object:Gem::Version
|
87
|
+
version: '0'
|
88
|
+
requirements: []
|
89
|
+
rubyforge_project:
|
90
|
+
rubygems_version: 2.4.8
|
91
|
+
signing_key:
|
92
|
+
specification_version: 4
|
93
|
+
summary: Ruby Input and Output streaming with support for Zip, Gzip, and Encryption.
|
94
|
+
test_files:
|
95
|
+
- test/file_reader_test.rb
|
96
|
+
- test/file_writer_test.rb
|
97
|
+
- test/files/text.txt
|
98
|
+
- test/files/text.txt.gz
|
99
|
+
- test/files/text.txt.gz.zip
|
100
|
+
- test/files/text.zip
|
101
|
+
- test/gzip_reader_test.rb
|
102
|
+
- test/gzip_writer_test.rb
|
103
|
+
- test/test_helper.rb
|
104
|
+
- test/zip_reader_test.rb
|
105
|
+
- test/zip_writer_test.rb
|