zip_tricks 2.5.0 → 2.6.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 +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
|