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 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