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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: c0cda66fb0e3453ab3c239126c67686d124fcce5
4
- data.tar.gz: 28327929e311c13ad556b85cef2ddf73ee1fc653
3
+ metadata.gz: 33d767b5968d6743916c79ae85953e802a3f3e4a
4
+ data.tar.gz: 30bcfd521a5db58cfea176d6b36c82c09609c485
5
5
  SHA512:
6
- metadata.gz: 5f5e227ce148f6d7468371f8d8653b8fa15331f0495e674f0e8972fa32d532865757acfb1c0051b147fe44fcbc81e0798821bd3c7cf1dd75e4e3e85852715e90
7
- data.tar.gz: 5f455aed3939b423b1459e510a00cfc749701102e1dc714677fc74a2e2de6b9b18f7aebf23a09ffc68b5bd1c51bb627e22c305f1d1ffc0b9c933a4954f111828
6
+ metadata.gz: 41b5f45f0ae560ca28386e939da7cbb4648fe34fe0c84acbff1a761786b797d1a17a30a33cf72201596783a0dfa307d8d7910089872d76aecaf8b10986a9ccd9
7
+ data.tar.gz: 0539bde9484ba1cb6bf580cfbc252271a1498bdbca9703701d22333a67f6d495360558590e6eaf2e945742a46e63bbe0d519f32664bdc329908a1b80f6ad8b76
data/.travis.yml CHANGED
@@ -1,5 +1,9 @@
1
1
  rvm:
2
2
  - 2.1
3
3
  - 2.2
4
+ - jruby-9.0
4
5
  sudo: false
5
6
  cache: bundler
7
+ matrix:
8
+ allow_failures:
9
+ - rvm: jruby-9.0
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 "rdoc", "~> 3.12"
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 which are described below.
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 'rdoc/task'
44
- Rake::RDocTask.new do |rdoc|
45
- version = File.exist?('VERSION') ? File.read('VERSION') : ""
46
-
47
- rdoc.rdoc_dir = 'rdoc'
48
- rdoc.title = "zip_tricks #{version}"
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
@@ -0,0 +1,5 @@
1
+ require File.dirname(__FILE__) + '/rack_application.rb'
2
+
3
+ # Demonstrates a Rack app that can offer a ZIP download composed
4
+ # at runtime (see rack_application.rb)
5
+ run ZipDownload.new
@@ -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
@@ -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<Array<ZipSpan>, Fixnum>] an array of byte spans within the final ZIP, and the total size of the archive
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
@@ -2,7 +2,7 @@ require 'zip'
2
2
  require 'very_tiny_state_machine'
3
3
 
4
4
  module ZipTricks
5
- VERSION = '2.5.0'
5
+ VERSION = '2.6.0'
6
6
 
7
7
  # Require all the sub-components except myself
8
8
  Dir.glob(__dir__ + '/**/*.rb').sort.each {|p| require p unless p == __FILE__ }
@@ -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.0 ruby lib
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.5.0"
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-05-17"
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>, ["~> 1.1.7"])
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<rdoc>, ["~> 3.12"])
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>, ["~> 1.1.7"])
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<rdoc>, ["~> 3.12"])
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>, ["~> 1.1.7"])
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<rdoc>, ["~> 3.12"])
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.5.0
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-05-17 00:00:00.000000000 Z
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: rdoc
110
+ name: yard
77
111
  requirement: !ruby/object:Gem::Requirement
78
112
  requirements:
79
113
  - - "~>"
80
114
  - !ruby/object:Gem::Version
81
- version: '3.12'
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: '3.12'
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