zip_tricks 2.5.0 → 2.6.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.travis.yml +4 -0
- data/Gemfile +4 -3
- data/README.md +6 -1
- data/Rakefile +6 -8
- data/examples/archive_size_estimate.rb +13 -0
- data/examples/config.ru +5 -0
- data/examples/parallel_compression_with_block_deflate.rb +75 -0
- data/examples/rack_application.rb +59 -0
- data/lib/zip_tricks/manifest.rb +1 -1
- data/lib/zip_tricks/remote_io.rb +85 -0
- data/lib/zip_tricks/remote_uncap.rb +73 -0
- data/lib/zip_tricks.rb +1 -1
- data/spec/zip_tricks/remote_io_spec.rb +127 -0
- data/spec/zip_tricks/remote_uncap_spec.rb +128 -0
- data/zip_tricks.gemspec +23 -12
- metadata +49 -21
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 33d767b5968d6743916c79ae85953e802a3f3e4a
|
4
|
+
data.tar.gz: 30bcfd521a5db58cfea176d6b36c82c09609c485
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 41b5f45f0ae560ca28386e939da7cbb4648fe34fe0c84acbff1a761786b797d1a17a30a33cf72201596783a0dfa307d8d7910089872d76aecaf8b10986a9ccd9
|
7
|
+
data.tar.gz: 0539bde9484ba1cb6bf580cfbc252271a1498bdbca9703701d22333a67f6d495360558590e6eaf2e945742a46e63bbe0d519f32664bdc329908a1b80f6ad8b76
|
data/.travis.yml
CHANGED
data/Gemfile
CHANGED
@@ -1,13 +1,14 @@
|
|
1
1
|
source "http://rubygems.org"
|
2
2
|
|
3
|
-
gem 'rubyzip', '~> 1.1.7'
|
3
|
+
gem 'rubyzip', '~> 1.1', '>= 1.1.7'
|
4
4
|
gem 'very_tiny_state_machine', '~> 2'
|
5
5
|
|
6
6
|
group :development do
|
7
|
+
gem 'range_utils'
|
8
|
+
gem 'rack', '~> 1.6' # For Jeweler
|
7
9
|
gem 'rake', '~> 10.4'
|
8
10
|
gem "rspec", "~> 3.2.0", '< 3.3'
|
9
|
-
gem "
|
11
|
+
gem "yard", "~> 0.8"
|
10
12
|
gem "bundler", "~> 1.0"
|
11
13
|
gem "jeweler", "~> 2.0.1"
|
12
|
-
gem 'range_utils'
|
13
14
|
end
|
data/README.md
CHANGED
@@ -6,7 +6,12 @@ Makes Rubyzip sing, dance and play saxophone for streaming applications.
|
|
6
6
|
Spiritual successor to [zipline](https://github.com/fringd/zipline)
|
7
7
|
|
8
8
|
Requires Ruby 2.1+, rubyzip and a couple of other gems (all available to jRuby as well).
|
9
|
-
The library is composed of a loose set of modules
|
9
|
+
The library is composed of a loose set of modules.
|
10
|
+
|
11
|
+
## Usage by example
|
12
|
+
|
13
|
+
Check out the `examples/` directory at the root of the project. This will give you a good idea
|
14
|
+
of various use cases the library supports.
|
10
15
|
|
11
16
|
## BlockDeflate
|
12
17
|
|
data/Rakefile
CHANGED
@@ -40,12 +40,10 @@ end
|
|
40
40
|
|
41
41
|
task :default => :spec
|
42
42
|
|
43
|
-
require '
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
rdoc.rdoc_files.include('README*')
|
50
|
-
rdoc.rdoc_files.include('lib/**/*.rb')
|
43
|
+
require 'yard'
|
44
|
+
desc "Generate YARD documentation"
|
45
|
+
YARD::Rake::YardocTask.new do |t|
|
46
|
+
t.files = ['lib/**/*.rb', 'ext/**/*.c' ]
|
47
|
+
t.options = ['--markup markdown']
|
48
|
+
t.stats_options = ['--list-undoc']
|
51
49
|
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
require_relative '../lib/zip_tricks'
|
2
|
+
|
3
|
+
# Predict how large a ZIP file is going to be without having access to the actual
|
4
|
+
# file contents, but using just the filenames (influences the file size) and the size
|
5
|
+
# of the files
|
6
|
+
zip_archive_size_in_bytes = ZipTricks::StoredSizeEstimator.perform_fake_archiving do |zip|
|
7
|
+
# Pretend we are going to make a ZIP file which contains a few
|
8
|
+
# MP4 files (those do not compress all too well)
|
9
|
+
zip.add_stored_entry("MOV_1234.MP4", 898090)
|
10
|
+
zip.add_stored_entry("MOV_1235.MP4", 7855126)
|
11
|
+
end
|
12
|
+
|
13
|
+
zip_archive_size_in_bytes #=> 8753438
|
data/examples/config.ru
ADDED
@@ -0,0 +1,75 @@
|
|
1
|
+
require_relative '../lib/zip_tricks'
|
2
|
+
require 'tempfile'
|
3
|
+
|
4
|
+
# This shows how to perform compression in parallel (a-la pigz, but in a less
|
5
|
+
# advanced fashion since the compression tables are not shared - to minimize shared state).
|
6
|
+
#
|
7
|
+
# When using this approach, compressing a large file can be performed as a map-reduce operation.
|
8
|
+
# First you prepare all the data per part of your (potentially very large) file, and then you use
|
9
|
+
# the reduce task to combine that data into one linear zip. In this example we will generate threads
|
10
|
+
# and collect their return values in the order the threads were launched, which guarantees a consistent
|
11
|
+
# reduce.
|
12
|
+
#
|
13
|
+
# So, let each thread generate a part of the file, and also
|
14
|
+
# compute the CRC32 of it. The thread will compress it's own part
|
15
|
+
# as well, in an independent deflate segment - the threads do not share anything. You could also
|
16
|
+
# multiplex this over multiple processes or even machines.
|
17
|
+
threads = (0..12).map do
|
18
|
+
Thread.new do
|
19
|
+
source_tempfile = Tempfile.new 't'
|
20
|
+
source_tempfile.binmode
|
21
|
+
|
22
|
+
# Fill the part with random content
|
23
|
+
12.times { source_tempfile << Random.new.bytes(1 * 1024 * 1024) }
|
24
|
+
source_tempfile.rewind
|
25
|
+
|
26
|
+
# Compute the CRC32 of the source file
|
27
|
+
part_crc = ZipTricks::StreamCRC32.from_io(source_tempfile)
|
28
|
+
source_tempfile.rewind
|
29
|
+
|
30
|
+
# Create a compressed part
|
31
|
+
compressed_tempfile = Tempfile.new('tc')
|
32
|
+
compressed_tempfile.binmode
|
33
|
+
ZipTricks::BlockDeflate.deflate_in_blocks(source_tempfile, compressed_tempfile)
|
34
|
+
|
35
|
+
source_tempfile.close!
|
36
|
+
# The data that the splicing process needs.
|
37
|
+
[compressed_tempfile, part_crc, source_tempfile.size]
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
# Threads return us a tuple with [compressed_tempfile, source_part_size, source_part_crc]
|
42
|
+
compressed_tempfiles_and_crc_of_parts = threads.map(&:join).map(&:value)
|
43
|
+
|
44
|
+
# Now we need to compute the CRC32 of the _entire_ file, and it has to be the CRC32
|
45
|
+
# of the _source_ file (uncompressed), not of the compressed variant. Handily we know
|
46
|
+
entire_file_crc = ZipTricks::StreamCRC32.new
|
47
|
+
compressed_tempfiles_and_crc_of_parts.each do | _, source_part_crc, source_part_size|
|
48
|
+
entire_file_crc.append(source_part_crc, source_part_size)
|
49
|
+
end
|
50
|
+
|
51
|
+
# We need to append the the terminator bytes to the end of the last part.
|
52
|
+
last_compressed_part = compressed_tempfiles_and_crc_of_parts[-1][0]
|
53
|
+
ZipTricks::BlockDeflate.write_terminator(last_compressed_part)
|
54
|
+
|
55
|
+
# and we need to know how big the deflated segment of the ZIP is going to be, in total.
|
56
|
+
# To figure that out we just sum the sizes of the files
|
57
|
+
compressed_part_files = compressed_tempfiles_and_crc_of_parts.map(&:first)
|
58
|
+
size_of_deflated_segment = compressed_part_files.map(&:size).inject(&:+)
|
59
|
+
size_of_uncompressed_file = compressed_tempfiles_and_crc_of_parts.map{|e| e[2]}.inject(&:+)
|
60
|
+
|
61
|
+
# And now we can create a ZIP with our compressed file in it's entirety.
|
62
|
+
# We use a File as a destination here, but you can also use a socket or a
|
63
|
+
# non-rewindable IO. ZipTricks never needs to rewind your output, since it is
|
64
|
+
# made for streaming.
|
65
|
+
output = File.open('zip_created_in_parallel.zip', 'wb')
|
66
|
+
|
67
|
+
ZipTricks::Streamer.open(output) do | zip |
|
68
|
+
zip.add_compressed_entry("parallel.bin", size_of_uncompressed_file, entire_file_crc.to_i, size_of_deflated_segment)
|
69
|
+
compressed_part_files.each do |part_file|
|
70
|
+
part_file.rewind
|
71
|
+
while blob = part_file.read(2048)
|
72
|
+
zip << blob
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
@@ -0,0 +1,59 @@
|
|
1
|
+
require_relative '../lib/zip_tricks'
|
2
|
+
|
3
|
+
# An example of how you can create a Rack endpoint for your ZIP downloads.
|
4
|
+
# NEVER run this in production - it is a huge security risk.
|
5
|
+
# What this app will do is pick PATH_INFO (your request URL path)
|
6
|
+
# and grab a file located at this path on your filesystem. The file will then
|
7
|
+
# be added to a ZIP archive created completely programmatically. No data will be cached
|
8
|
+
# on disk and the contents of the ZIP file will _not_ be buffered in it's entirety
|
9
|
+
# before sending. Unless you use a buffering Rack server of course (WEBrick or Thin).
|
10
|
+
class ZipDownload
|
11
|
+
def call(env)
|
12
|
+
file_path = env['PATH_INFO'] # Should be the absolute path on the filesystem
|
13
|
+
|
14
|
+
# Open the file for binary reading
|
15
|
+
f = File.open(file_path, 'rb')
|
16
|
+
filename = File.basename(file_path)
|
17
|
+
|
18
|
+
# Compute the CRC32 upfront. We do not use local footers for post-computing the CRC32,
|
19
|
+
# so you _do_ have to precompute it beforehand. Ideally, you would do that before
|
20
|
+
# storing the files you will be sending out later on.
|
21
|
+
crc32 = ZipTricks::StreamCRC32.from_io(f)
|
22
|
+
f.rewind
|
23
|
+
|
24
|
+
# Compute the size of the download, so that a
|
25
|
+
# real Content-Length header can be sent. Also, if your download
|
26
|
+
# stops at some point, the downloading browser will be able to tell
|
27
|
+
# the user that the download stalled or was aborted in-flight.
|
28
|
+
# Note that using the size estimator here does _not_ read or compress
|
29
|
+
# your original file, so it is very fast.
|
30
|
+
size = ZipTricks::StoredSizeEstimator.perform_fake_archiving do |ar|
|
31
|
+
ar.add_stored_entry(filename, f.size)
|
32
|
+
end
|
33
|
+
|
34
|
+
# Create a suitable Rack response body, that will support each(),
|
35
|
+
# close() and all the other methods. We can then return it up the stack.
|
36
|
+
zip_response_body = ZipTricks::RackBody.new do |zip|
|
37
|
+
begin
|
38
|
+
# We are adding only one file to the ZIP here, but you could do that
|
39
|
+
# with an arbitrary number of files of course.
|
40
|
+
zip.add_stored_entry(filename, f.size, crc32)
|
41
|
+
# Write the contents of the file. It is stored, so the writes go directly
|
42
|
+
# to the Rack output, bypassing any RubyZip deflaters/compressors. In fact you
|
43
|
+
# are yielding the "blob" string here directly to the Rack server handler.
|
44
|
+
while blob = f.read(1024 * 128)
|
45
|
+
zip << blob
|
46
|
+
end
|
47
|
+
ensure
|
48
|
+
f.close # Make sure the opened file we read from gets closed
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
# Add a Content-Disposition so that the download has a .zip extension
|
53
|
+
# (this will not work well with UTF-8 filenames on Windows, but hey!)
|
54
|
+
content_disposition = 'attachment; filename=%s.zip' % filename
|
55
|
+
|
56
|
+
# and return the response, adding the Content-Length we have computed earlier
|
57
|
+
[200, {'Content-Length' => size.to_s, 'Content-Disposition' => content_disposition}, zip_response_body]
|
58
|
+
end
|
59
|
+
end
|
data/lib/zip_tricks/manifest.rb
CHANGED
@@ -15,7 +15,7 @@ class ZipTricks::Manifest < Struct.new(:zip_streamer, :io, :part_list)
|
|
15
15
|
# zip_spans[0] #=> Manifest::ZipSpan(part_type: :entry_header, byte_range_in_zip: 0..44, ...)
|
16
16
|
# zip_spans[-1] #=> Manifest::ZipSpan(part_type: :central_directory, byte_range_in_zip: 776721..898921, ...)
|
17
17
|
#
|
18
|
-
# @return [Array<
|
18
|
+
# @return [Array<ZipSpan>, Fixnum] an array of byte spans within the final ZIP, and the total size of the archive
|
19
19
|
# @yield [Manifest] the manifest object you can add entries to
|
20
20
|
def self.build
|
21
21
|
output_io = ZipTricks::WriteAndTell.new(ZipTricks::NullWriter)
|
@@ -0,0 +1,85 @@
|
|
1
|
+
# An object that fakes just-enough of an IO to be dangerous
|
2
|
+
# - or, more precisely, to be useful as a source for the RubyZip
|
3
|
+
# central directory parser
|
4
|
+
class ZipTricks::RemoteIO
|
5
|
+
|
6
|
+
# @param fetcher[#request_object_size, #request_range] an object that can fetch
|
7
|
+
def initialize(fetcher = :NOT_SET)
|
8
|
+
@pos = 0
|
9
|
+
@fetcher = fetcher
|
10
|
+
@remote_size = false
|
11
|
+
end
|
12
|
+
|
13
|
+
# Emulates IO#seek
|
14
|
+
def seek(offset, mode = IO::SEEK_SET)
|
15
|
+
case mode
|
16
|
+
when IO::SEEK_SET
|
17
|
+
@remote_size ||= request_object_size
|
18
|
+
@pos = clamp(0, offset, @remote_size)
|
19
|
+
when IO::SEEK_END
|
20
|
+
@remote_size ||= request_object_size
|
21
|
+
@pos = clamp(0, @remote_size + offset, @remote_size)
|
22
|
+
else
|
23
|
+
raise Errno::ENOTSUP, "Seek mode #{mode.inspect} not supported"
|
24
|
+
end
|
25
|
+
0 # always return 0!
|
26
|
+
end
|
27
|
+
|
28
|
+
# Emulates IO#read
|
29
|
+
def read(n_bytes = nil)
|
30
|
+
@remote_size ||= request_object_size
|
31
|
+
|
32
|
+
# If the resource is empty there is nothing to read
|
33
|
+
return nil if @remote_size.zero?
|
34
|
+
|
35
|
+
maximum_avaialable = @remote_size - @pos
|
36
|
+
n_bytes ||= maximum_avaialable # nil == read to the end of file
|
37
|
+
raise ArgumentError, "No negative reads(#{n_bytes})" if n_bytes < 0
|
38
|
+
|
39
|
+
n_bytes = clamp(0, n_bytes, maximum_avaialable)
|
40
|
+
|
41
|
+
read_n_bytes_from_remote(@pos, n_bytes).tap do |data|
|
42
|
+
if data.bytesize != n_bytes
|
43
|
+
raise "Remote read returned #{data.bytesize} bytes instead of #{n_bytes} as requested"
|
44
|
+
end
|
45
|
+
@pos = clamp(0, @pos + data.bytesize, @remote_size)
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
# Returns the current pointer position within the IO.
|
50
|
+
# Not used by RubyZip but used in tests of our own
|
51
|
+
#
|
52
|
+
# @return [Fixnum]
|
53
|
+
def pos
|
54
|
+
@pos
|
55
|
+
end
|
56
|
+
|
57
|
+
protected
|
58
|
+
|
59
|
+
def request_range(range)
|
60
|
+
@fetcher.request_range(range)
|
61
|
+
end
|
62
|
+
|
63
|
+
def request_object_size
|
64
|
+
@fetcher.request_object_size
|
65
|
+
end
|
66
|
+
|
67
|
+
# Reads N bytes at offset from remote
|
68
|
+
def read_n_bytes_from_remote(start_at, n_bytes)
|
69
|
+
range = (start_at..(start_at + n_bytes - 1))
|
70
|
+
request_range(range)
|
71
|
+
end
|
72
|
+
|
73
|
+
# Reads the Content-Length and caches it
|
74
|
+
def remote_size
|
75
|
+
@remote_size ||= request_object_size
|
76
|
+
end
|
77
|
+
|
78
|
+
private
|
79
|
+
|
80
|
+
def clamp(a,b,c)
|
81
|
+
return a if b < a
|
82
|
+
return c if b > c
|
83
|
+
b
|
84
|
+
end
|
85
|
+
end
|
@@ -0,0 +1,73 @@
|
|
1
|
+
# Alows reading the central directory of a remote ZIP file without
|
2
|
+
# downloading the entire file. The central directory provides the
|
3
|
+
# offsets at which the actual file contents is located. You can then
|
4
|
+
# use the `Range:` HTTP headers to download those entries separately.
|
5
|
+
class ZipTricks::RemoteUncap
|
6
|
+
|
7
|
+
# Represents a file embedded within a remote ZIP archive
|
8
|
+
class RemoteZipEntry
|
9
|
+
|
10
|
+
# @return [String] filename of the file in the remote ZIP
|
11
|
+
attr_accessor :name
|
12
|
+
|
13
|
+
# @return [Fixnum] size in bytes of the file when uncompressed
|
14
|
+
attr_accessor :size_uncompressed
|
15
|
+
|
16
|
+
# @return [Fixnum] size in bytes of the file when compressed (the segment in the ZIP)
|
17
|
+
attr_accessor :size_compressed
|
18
|
+
|
19
|
+
# @return [Fixnum] compression method (0 for stored, 8 for deflate)
|
20
|
+
attr_accessor :compression_method
|
21
|
+
|
22
|
+
# @return [Fixnum] where the file data starts within the ZIP
|
23
|
+
attr_accessor :starts_at_offset
|
24
|
+
|
25
|
+
# @return [Fixnum] where the file data ends within the zip.
|
26
|
+
# Will be equal to starts_at_offset if the file is empty
|
27
|
+
attr_accessor :ends_at_offset
|
28
|
+
|
29
|
+
# Yields the object during initialization
|
30
|
+
def initialize
|
31
|
+
yield self
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
# @param uri[String] the HTTP(S) URL to read the ZIP footer from
|
36
|
+
# @return [Array<RemoteZipEntry>] metadata about the files within the remote archive
|
37
|
+
def self.files_within_zip_at(uri)
|
38
|
+
fetcher = new(uri)
|
39
|
+
fake_io = ZipTricks::RemoteIO.new(fetcher)
|
40
|
+
dir = Zip::CentralDirectory.read_from_stream(fake_io)
|
41
|
+
|
42
|
+
dir.entries.map do | rubyzip_entry |
|
43
|
+
RemoteZipEntry.new do | entry |
|
44
|
+
entry.name = rubyzip_entry.name
|
45
|
+
entry.size_uncompressed = rubyzip_entry.size
|
46
|
+
entry.size_compressed = rubyzip_entry.compressed_size
|
47
|
+
entry.compression_method = rubyzip_entry.compression_method
|
48
|
+
|
49
|
+
entry.starts_at_offset = rubyzip_entry.local_header_offset + rubyzip_entry.calculate_local_header_size
|
50
|
+
entry.ends_at_offset = entry.starts_at_offset + rubyzip_entry.compressed_size
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
def initialize(uri)
|
56
|
+
@uri = URI(uri)
|
57
|
+
end
|
58
|
+
|
59
|
+
# @param range[Range] the HTTP range of data to fetch from remote
|
60
|
+
# @return [String] the response body of the ranged request
|
61
|
+
def request_range(range)
|
62
|
+
request = Net::HTTP::Get.new(@uri)
|
63
|
+
request.range = range
|
64
|
+
http = Net::HTTP.start(@uri.hostname, @uri.port)
|
65
|
+
http.request(request).body
|
66
|
+
end
|
67
|
+
|
68
|
+
# @return [Fixnum] the byte size of the ranged request
|
69
|
+
def request_object_size
|
70
|
+
http = Net::HTTP.start(@uri.hostname, @uri.port)
|
71
|
+
http.request_head(uri)['Content-Length'].to_i
|
72
|
+
end
|
73
|
+
end
|
data/lib/zip_tricks.rb
CHANGED
@@ -0,0 +1,127 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe ZipTricks::RemoteIO do
|
4
|
+
|
5
|
+
context 'working with the fetcher object' do
|
6
|
+
it 'asks the fetcher object to obtain the object size and the actual data when reading' do
|
7
|
+
mock_fetcher = double(request_object_size: 120, request_range: 'abc')
|
8
|
+
subject = described_class.new(mock_fetcher)
|
9
|
+
expect(subject.read(3)).to eq('abc')
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
context 'when it internally addresses a remote resource' do
|
14
|
+
it 'requests the size of the resource once via #request_object_size and does neet to read if resource is empty' do
|
15
|
+
subject = described_class.new
|
16
|
+
expect(subject).to receive(:request_object_size).and_return(0)
|
17
|
+
expect(subject.read).to be_nil
|
18
|
+
end
|
19
|
+
|
20
|
+
it 'performs remote reads when repeatedly requesting the same chunk, via #request_range' do
|
21
|
+
subject = described_class.new
|
22
|
+
|
23
|
+
expect(subject).to receive(:request_object_size).and_return(120)
|
24
|
+
allow(subject).to receive(:request_range) {|range|
|
25
|
+
expect(range).to eq(5..14)
|
26
|
+
Random.new.bytes(10)
|
27
|
+
}
|
28
|
+
20.times do
|
29
|
+
subject.seek(5, IO::SEEK_SET)
|
30
|
+
subject.read(10)
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
describe '#seek' do
|
36
|
+
context 'with an unsupported mode' do
|
37
|
+
it 'raises an error' do
|
38
|
+
uncap = described_class.new
|
39
|
+
expect {
|
40
|
+
uncap.seek(123, :UNSUPPORTED)
|
41
|
+
}.to raise_error(Errno::ENOTSUP)
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
context 'with SEEK_SET mode' do
|
46
|
+
it 'returns the offset of 10 when asked to seek to 10' do
|
47
|
+
uncap = described_class.new
|
48
|
+
expect(uncap).to receive(:request_object_size).and_return(100)
|
49
|
+
mode = IO::SEEK_SET
|
50
|
+
expect(uncap.seek(10, mode)).to eq(0)
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
context 'with SEEK_END mode' do
|
55
|
+
it 'seens to 10 bytes to the end of the IO' do
|
56
|
+
uncap = described_class.new
|
57
|
+
expect(uncap).to receive(:request_object_size).and_return(100)
|
58
|
+
|
59
|
+
mode = IO::SEEK_END
|
60
|
+
offset = -10
|
61
|
+
expect(uncap.seek(-10, IO::SEEK_END)).to eq(0)
|
62
|
+
expect(uncap.pos).to eq(90)
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
describe '#read' do
|
68
|
+
before :each do
|
69
|
+
@buf = Tempfile.new('simulated-http')
|
70
|
+
@buf.binmode
|
71
|
+
5.times { @buf << Random.new.bytes(1024 * 1024 * 3) }
|
72
|
+
@buf.rewind
|
73
|
+
|
74
|
+
@subject = described_class.new
|
75
|
+
|
76
|
+
allow(@subject).to receive(:request_object_size).and_return(@buf.size)
|
77
|
+
allow(@subject).to receive(:request_range) {|range|
|
78
|
+
@buf.read[range].tap { @buf.rewind }
|
79
|
+
}
|
80
|
+
end
|
81
|
+
|
82
|
+
after :each do
|
83
|
+
@buf.close; @buf.unlink
|
84
|
+
end
|
85
|
+
|
86
|
+
context 'without arguments' do
|
87
|
+
it 'reads the entire buffer and alters the position pointer' do
|
88
|
+
expect(@subject.pos).to eq(0)
|
89
|
+
read = @subject.read
|
90
|
+
expect(read.bytesize).to eq(@buf.size)
|
91
|
+
expect(@subject.pos).to eq(@buf.size)
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
context 'with length' do
|
96
|
+
it 'returns exact amount of bytes at the start of the buffer' do
|
97
|
+
bytes_read = @subject.read(10)
|
98
|
+
expect(@subject.pos).to eq(10)
|
99
|
+
@buf.seek(0)
|
100
|
+
expect(bytes_read).to eq(@buf.read(10))
|
101
|
+
end
|
102
|
+
|
103
|
+
it 'returns exact amount of bytes from the middle of the buffer' do
|
104
|
+
@subject.seek(456, IO::SEEK_SET)
|
105
|
+
|
106
|
+
bytes_read = @subject.read(10)
|
107
|
+
expect(@subject.pos).to eq(456+10)
|
108
|
+
|
109
|
+
@buf.seek(456)
|
110
|
+
expect(bytes_read).to eq(@buf.read(10))
|
111
|
+
end
|
112
|
+
|
113
|
+
it 'returns the last N bytes it can read' do
|
114
|
+
at_end = @buf.size - 4
|
115
|
+
@subject.seek(at_end, IO::SEEK_SET)
|
116
|
+
|
117
|
+
expect(@subject.pos).to eq(15728636)
|
118
|
+
bytes_read = @subject.read(10)
|
119
|
+
expect(@subject.pos).to eq(@buf.size) # Should have moved the pos pointer to the end
|
120
|
+
|
121
|
+
expect(bytes_read.bytesize).to eq(4)
|
122
|
+
|
123
|
+
expect(@subject.pos).to eq(@buf.size)
|
124
|
+
end
|
125
|
+
end
|
126
|
+
end
|
127
|
+
end
|
@@ -0,0 +1,128 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe ZipTricks::RemoteUncap, webmock: true do
|
4
|
+
let(:uri) { URI.parse('http://example.com/file.zip') }
|
5
|
+
|
6
|
+
after :each do
|
7
|
+
File.unlink('temp.zip') rescue Errno::ENOENT
|
8
|
+
end
|
9
|
+
|
10
|
+
it 'returns an array of remote entries that can be used to fetch the segments from within the ZIP' do
|
11
|
+
payload1 = Tempfile.new 'payload1'
|
12
|
+
payload1 << Random.new.bytes((1024 * 1024 * 5) + 10)
|
13
|
+
payload1.flush; payload1.rewind;
|
14
|
+
|
15
|
+
payload2 = Tempfile.new 'payload2'
|
16
|
+
payload2 << Random.new.bytes(1024 * 1024 * 3)
|
17
|
+
payload2.flush; payload2.rewind
|
18
|
+
|
19
|
+
payload1_crc = Zlib.crc32(payload1.read).tap { payload1.rewind }
|
20
|
+
payload2_crc = Zlib.crc32(payload2.read).tap { payload2.rewind }
|
21
|
+
|
22
|
+
File.open('temp.zip', 'wb') do |f|
|
23
|
+
ZipTricks::Streamer.open(f) do | zip |
|
24
|
+
zip.add_stored_entry('first-file.bin', payload1.size, payload1_crc)
|
25
|
+
while blob = payload1.read(1024 * 5)
|
26
|
+
zip << blob
|
27
|
+
end
|
28
|
+
zip.add_stored_entry('second-file.bin', payload2.size, payload2_crc)
|
29
|
+
while blob = payload2.read(1024 * 5)
|
30
|
+
zip << blob
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
payload1.rewind; payload2.rewind
|
35
|
+
|
36
|
+
expect(File).to be_exist('temp.zip')
|
37
|
+
|
38
|
+
allow_any_instance_of(described_class).to receive(:request_object_size) {
|
39
|
+
File.size('temp.zip')
|
40
|
+
}
|
41
|
+
allow_any_instance_of(described_class).to receive(:request_range) {|_instance, range|
|
42
|
+
File.open('temp.zip', 'rb') do |f|
|
43
|
+
f.seek(range.begin)
|
44
|
+
f.read(range.end - range.begin + 1)
|
45
|
+
end
|
46
|
+
}
|
47
|
+
|
48
|
+
payload1.rewind; payload2.rewind
|
49
|
+
|
50
|
+
files = described_class.files_within_zip_at('http://fake.example.com')
|
51
|
+
expect(files).to be_kind_of(Array)
|
52
|
+
expect(files.length).to eq(2)
|
53
|
+
|
54
|
+
first, second = *files
|
55
|
+
|
56
|
+
expect(first.name).to eq('first-file.bin')
|
57
|
+
expect(first.size_uncompressed).to eq(payload1.size)
|
58
|
+
File.open('temp.zip', 'rb') do |readback|
|
59
|
+
readback.seek(first.starts_at_offset, IO::SEEK_SET)
|
60
|
+
expect(readback.read(12)).to eq(payload1.read(12))
|
61
|
+
end
|
62
|
+
|
63
|
+
expect(second.name).to eq('second-file.bin')
|
64
|
+
expect(second.size_uncompressed).to eq(payload2.size)
|
65
|
+
File.open('temp.zip', 'rb') do |readback|
|
66
|
+
readback.seek(second.starts_at_offset, IO::SEEK_SET)
|
67
|
+
expect(readback.read(12)).to eq(payload2.read(12))
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
it 'can cope with an empty file within the zip' do
|
72
|
+
payload1 = Tempfile.new 'payload1'
|
73
|
+
payload1.flush; payload1.rewind;
|
74
|
+
|
75
|
+
payload2 = Tempfile.new 'payload2'
|
76
|
+
payload2 << Random.new.bytes(1024)
|
77
|
+
payload2.flush; payload2.rewind
|
78
|
+
|
79
|
+
payload1_crc = Zlib.crc32(payload1.read).tap { payload1.rewind }
|
80
|
+
payload2_crc = Zlib.crc32(payload2.read).tap { payload2.rewind }
|
81
|
+
|
82
|
+
File.open('temp.zip', 'wb') do |f|
|
83
|
+
ZipTricks::Streamer.open(f) do | zip |
|
84
|
+
zip.add_stored_entry('first-file.bin', payload1.size, payload1_crc)
|
85
|
+
zip << '' # It is empty, so a read() would return nil
|
86
|
+
zip.add_stored_entry('second-file.bin', payload2.size, payload2_crc)
|
87
|
+
while blob = payload2.read(1024 * 5)
|
88
|
+
zip << blob
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
92
|
+
payload1.rewind; payload2.rewind
|
93
|
+
|
94
|
+
expect(File).to be_exist('temp.zip')
|
95
|
+
|
96
|
+
allow_any_instance_of(described_class).to receive(:request_object_size) {
|
97
|
+
File.size('temp.zip')
|
98
|
+
}
|
99
|
+
allow_any_instance_of(described_class).to receive(:request_range) {|_instance, range|
|
100
|
+
File.open('temp.zip', 'rb') do |f|
|
101
|
+
f.seek(range.begin)
|
102
|
+
f.read(range.end - range.begin + 1)
|
103
|
+
end
|
104
|
+
}
|
105
|
+
|
106
|
+
payload1.rewind; payload2.rewind
|
107
|
+
|
108
|
+
files = described_class.files_within_zip_at('http://fake.example.com')
|
109
|
+
expect(files).to be_kind_of(Array)
|
110
|
+
expect(files.length).to eq(2)
|
111
|
+
|
112
|
+
first, second = *files
|
113
|
+
|
114
|
+
expect(first.name).to eq('first-file.bin')
|
115
|
+
expect(first.size_uncompressed).to eq(payload1.size)
|
116
|
+
File.open('temp.zip', 'rb') do |readback|
|
117
|
+
readback.seek(first.starts_at_offset, IO::SEEK_SET)
|
118
|
+
expect(readback.read(0)).to eq(payload1.read(0))
|
119
|
+
end
|
120
|
+
|
121
|
+
expect(second.name).to eq('second-file.bin')
|
122
|
+
expect(second.size_uncompressed).to eq(payload2.size)
|
123
|
+
File.open('temp.zip', 'rb') do |readback|
|
124
|
+
readback.seek(second.starts_at_offset, IO::SEEK_SET)
|
125
|
+
expect(readback.read(12)).to eq(payload2.read(12))
|
126
|
+
end
|
127
|
+
end
|
128
|
+
end
|
data/zip_tricks.gemspec
CHANGED
@@ -2,16 +2,16 @@
|
|
2
2
|
# DO NOT EDIT THIS FILE DIRECTLY
|
3
3
|
# Instead, edit Jeweler::Tasks in Rakefile, and run 'rake gemspec'
|
4
4
|
# -*- encoding: utf-8 -*-
|
5
|
-
# stub: zip_tricks 2.
|
5
|
+
# stub: zip_tricks 2.6.0 ruby lib
|
6
6
|
|
7
7
|
Gem::Specification.new do |s|
|
8
8
|
s.name = "zip_tricks"
|
9
|
-
s.version = "2.
|
9
|
+
s.version = "2.6.0"
|
10
10
|
|
11
11
|
s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
|
12
12
|
s.require_paths = ["lib"]
|
13
13
|
s.authors = ["Julik Tarkhanov"]
|
14
|
-
s.date = "2016-
|
14
|
+
s.date = "2016-07-01"
|
15
15
|
s.description = "Makes rubyzip stream, for real"
|
16
16
|
s.email = "me@julik.nl"
|
17
17
|
s.extra_rdoc_files = [
|
@@ -27,12 +27,18 @@ Gem::Specification.new do |s|
|
|
27
27
|
"LICENSE.txt",
|
28
28
|
"README.md",
|
29
29
|
"Rakefile",
|
30
|
+
"examples/archive_size_estimate.rb",
|
31
|
+
"examples/config.ru",
|
32
|
+
"examples/parallel_compression_with_block_deflate.rb",
|
33
|
+
"examples/rack_application.rb",
|
30
34
|
"lib/zip_tricks.rb",
|
31
35
|
"lib/zip_tricks/block_deflate.rb",
|
32
36
|
"lib/zip_tricks/block_write.rb",
|
33
37
|
"lib/zip_tricks/manifest.rb",
|
34
38
|
"lib/zip_tricks/null_writer.rb",
|
35
39
|
"lib/zip_tricks/rack_body.rb",
|
40
|
+
"lib/zip_tricks/remote_io.rb",
|
41
|
+
"lib/zip_tricks/remote_uncap.rb",
|
36
42
|
"lib/zip_tricks/stored_size_estimator.rb",
|
37
43
|
"lib/zip_tricks/stream_crc32.rb",
|
38
44
|
"lib/zip_tricks/streamer.rb",
|
@@ -42,6 +48,8 @@ Gem::Specification.new do |s|
|
|
42
48
|
"spec/zip_tricks/block_write_spec.rb",
|
43
49
|
"spec/zip_tricks/manifest_spec.rb",
|
44
50
|
"spec/zip_tricks/rack_body_spec.rb",
|
51
|
+
"spec/zip_tricks/remote_io_spec.rb",
|
52
|
+
"spec/zip_tricks/remote_uncap_spec.rb",
|
45
53
|
"spec/zip_tricks/stored_size_estimator_spec.rb",
|
46
54
|
"spec/zip_tricks/stream_crc32_spec.rb",
|
47
55
|
"spec/zip_tricks/streamer_spec.rb",
|
@@ -58,33 +66,36 @@ Gem::Specification.new do |s|
|
|
58
66
|
s.specification_version = 4
|
59
67
|
|
60
68
|
if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.2.0') then
|
61
|
-
s.add_runtime_dependency(%q<rubyzip>, ["
|
69
|
+
s.add_runtime_dependency(%q<rubyzip>, [">= 1.1.7", "~> 1.1"])
|
62
70
|
s.add_runtime_dependency(%q<very_tiny_state_machine>, ["~> 2"])
|
71
|
+
s.add_development_dependency(%q<range_utils>, [">= 0"])
|
72
|
+
s.add_development_dependency(%q<rack>, ["~> 1.6"])
|
63
73
|
s.add_development_dependency(%q<rake>, ["~> 10.4"])
|
64
74
|
s.add_development_dependency(%q<rspec>, ["< 3.3", "~> 3.2.0"])
|
65
|
-
s.add_development_dependency(%q<
|
75
|
+
s.add_development_dependency(%q<yard>, ["~> 0.8"])
|
66
76
|
s.add_development_dependency(%q<bundler>, ["~> 1.0"])
|
67
77
|
s.add_development_dependency(%q<jeweler>, ["~> 2.0.1"])
|
68
|
-
s.add_development_dependency(%q<range_utils>, [">= 0"])
|
69
78
|
else
|
70
|
-
s.add_dependency(%q<rubyzip>, ["
|
79
|
+
s.add_dependency(%q<rubyzip>, [">= 1.1.7", "~> 1.1"])
|
71
80
|
s.add_dependency(%q<very_tiny_state_machine>, ["~> 2"])
|
81
|
+
s.add_dependency(%q<range_utils>, [">= 0"])
|
82
|
+
s.add_dependency(%q<rack>, ["~> 1.6"])
|
72
83
|
s.add_dependency(%q<rake>, ["~> 10.4"])
|
73
84
|
s.add_dependency(%q<rspec>, ["< 3.3", "~> 3.2.0"])
|
74
|
-
s.add_dependency(%q<
|
85
|
+
s.add_dependency(%q<yard>, ["~> 0.8"])
|
75
86
|
s.add_dependency(%q<bundler>, ["~> 1.0"])
|
76
87
|
s.add_dependency(%q<jeweler>, ["~> 2.0.1"])
|
77
|
-
s.add_dependency(%q<range_utils>, [">= 0"])
|
78
88
|
end
|
79
89
|
else
|
80
|
-
s.add_dependency(%q<rubyzip>, ["
|
90
|
+
s.add_dependency(%q<rubyzip>, [">= 1.1.7", "~> 1.1"])
|
81
91
|
s.add_dependency(%q<very_tiny_state_machine>, ["~> 2"])
|
92
|
+
s.add_dependency(%q<range_utils>, [">= 0"])
|
93
|
+
s.add_dependency(%q<rack>, ["~> 1.6"])
|
82
94
|
s.add_dependency(%q<rake>, ["~> 10.4"])
|
83
95
|
s.add_dependency(%q<rspec>, ["< 3.3", "~> 3.2.0"])
|
84
|
-
s.add_dependency(%q<
|
96
|
+
s.add_dependency(%q<yard>, ["~> 0.8"])
|
85
97
|
s.add_dependency(%q<bundler>, ["~> 1.0"])
|
86
98
|
s.add_dependency(%q<jeweler>, ["~> 2.0.1"])
|
87
|
-
s.add_dependency(%q<range_utils>, [">= 0"])
|
88
99
|
end
|
89
100
|
end
|
90
101
|
|
metadata
CHANGED
@@ -1,29 +1,35 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: zip_tricks
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 2.
|
4
|
+
version: 2.6.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Julik Tarkhanov
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2016-
|
11
|
+
date: 2016-07-01 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: rubyzip
|
15
15
|
requirement: !ruby/object:Gem::Requirement
|
16
16
|
requirements:
|
17
|
-
- - "
|
17
|
+
- - ">="
|
18
18
|
- !ruby/object:Gem::Version
|
19
19
|
version: 1.1.7
|
20
|
+
- - "~>"
|
21
|
+
- !ruby/object:Gem::Version
|
22
|
+
version: '1.1'
|
20
23
|
type: :runtime
|
21
24
|
prerelease: false
|
22
25
|
version_requirements: !ruby/object:Gem::Requirement
|
23
26
|
requirements:
|
24
|
-
- - "
|
27
|
+
- - ">="
|
25
28
|
- !ruby/object:Gem::Version
|
26
29
|
version: 1.1.7
|
30
|
+
- - "~>"
|
31
|
+
- !ruby/object:Gem::Version
|
32
|
+
version: '1.1'
|
27
33
|
- !ruby/object:Gem::Dependency
|
28
34
|
name: very_tiny_state_machine
|
29
35
|
requirement: !ruby/object:Gem::Requirement
|
@@ -38,6 +44,34 @@ dependencies:
|
|
38
44
|
- - "~>"
|
39
45
|
- !ruby/object:Gem::Version
|
40
46
|
version: '2'
|
47
|
+
- !ruby/object:Gem::Dependency
|
48
|
+
name: range_utils
|
49
|
+
requirement: !ruby/object:Gem::Requirement
|
50
|
+
requirements:
|
51
|
+
- - ">="
|
52
|
+
- !ruby/object:Gem::Version
|
53
|
+
version: '0'
|
54
|
+
type: :development
|
55
|
+
prerelease: false
|
56
|
+
version_requirements: !ruby/object:Gem::Requirement
|
57
|
+
requirements:
|
58
|
+
- - ">="
|
59
|
+
- !ruby/object:Gem::Version
|
60
|
+
version: '0'
|
61
|
+
- !ruby/object:Gem::Dependency
|
62
|
+
name: rack
|
63
|
+
requirement: !ruby/object:Gem::Requirement
|
64
|
+
requirements:
|
65
|
+
- - "~>"
|
66
|
+
- !ruby/object:Gem::Version
|
67
|
+
version: '1.6'
|
68
|
+
type: :development
|
69
|
+
prerelease: false
|
70
|
+
version_requirements: !ruby/object:Gem::Requirement
|
71
|
+
requirements:
|
72
|
+
- - "~>"
|
73
|
+
- !ruby/object:Gem::Version
|
74
|
+
version: '1.6'
|
41
75
|
- !ruby/object:Gem::Dependency
|
42
76
|
name: rake
|
43
77
|
requirement: !ruby/object:Gem::Requirement
|
@@ -73,19 +107,19 @@ dependencies:
|
|
73
107
|
- !ruby/object:Gem::Version
|
74
108
|
version: 3.2.0
|
75
109
|
- !ruby/object:Gem::Dependency
|
76
|
-
name:
|
110
|
+
name: yard
|
77
111
|
requirement: !ruby/object:Gem::Requirement
|
78
112
|
requirements:
|
79
113
|
- - "~>"
|
80
114
|
- !ruby/object:Gem::Version
|
81
|
-
version: '
|
115
|
+
version: '0.8'
|
82
116
|
type: :development
|
83
117
|
prerelease: false
|
84
118
|
version_requirements: !ruby/object:Gem::Requirement
|
85
119
|
requirements:
|
86
120
|
- - "~>"
|
87
121
|
- !ruby/object:Gem::Version
|
88
|
-
version: '
|
122
|
+
version: '0.8'
|
89
123
|
- !ruby/object:Gem::Dependency
|
90
124
|
name: bundler
|
91
125
|
requirement: !ruby/object:Gem::Requirement
|
@@ -114,20 +148,6 @@ dependencies:
|
|
114
148
|
- - "~>"
|
115
149
|
- !ruby/object:Gem::Version
|
116
150
|
version: 2.0.1
|
117
|
-
- !ruby/object:Gem::Dependency
|
118
|
-
name: range_utils
|
119
|
-
requirement: !ruby/object:Gem::Requirement
|
120
|
-
requirements:
|
121
|
-
- - ">="
|
122
|
-
- !ruby/object:Gem::Version
|
123
|
-
version: '0'
|
124
|
-
type: :development
|
125
|
-
prerelease: false
|
126
|
-
version_requirements: !ruby/object:Gem::Requirement
|
127
|
-
requirements:
|
128
|
-
- - ">="
|
129
|
-
- !ruby/object:Gem::Version
|
130
|
-
version: '0'
|
131
151
|
description: Makes rubyzip stream, for real
|
132
152
|
email: me@julik.nl
|
133
153
|
executables: []
|
@@ -144,12 +164,18 @@ files:
|
|
144
164
|
- LICENSE.txt
|
145
165
|
- README.md
|
146
166
|
- Rakefile
|
167
|
+
- examples/archive_size_estimate.rb
|
168
|
+
- examples/config.ru
|
169
|
+
- examples/parallel_compression_with_block_deflate.rb
|
170
|
+
- examples/rack_application.rb
|
147
171
|
- lib/zip_tricks.rb
|
148
172
|
- lib/zip_tricks/block_deflate.rb
|
149
173
|
- lib/zip_tricks/block_write.rb
|
150
174
|
- lib/zip_tricks/manifest.rb
|
151
175
|
- lib/zip_tricks/null_writer.rb
|
152
176
|
- lib/zip_tricks/rack_body.rb
|
177
|
+
- lib/zip_tricks/remote_io.rb
|
178
|
+
- lib/zip_tricks/remote_uncap.rb
|
153
179
|
- lib/zip_tricks/stored_size_estimator.rb
|
154
180
|
- lib/zip_tricks/stream_crc32.rb
|
155
181
|
- lib/zip_tricks/streamer.rb
|
@@ -159,6 +185,8 @@ files:
|
|
159
185
|
- spec/zip_tricks/block_write_spec.rb
|
160
186
|
- spec/zip_tricks/manifest_spec.rb
|
161
187
|
- spec/zip_tricks/rack_body_spec.rb
|
188
|
+
- spec/zip_tricks/remote_io_spec.rb
|
189
|
+
- spec/zip_tricks/remote_uncap_spec.rb
|
162
190
|
- spec/zip_tricks/stored_size_estimator_spec.rb
|
163
191
|
- spec/zip_tricks/stream_crc32_spec.rb
|
164
192
|
- spec/zip_tricks/streamer_spec.rb
|