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