zip_tricks 4.2.1 → 4.2.2
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/Gemfile +1 -1
- data/README.md +70 -57
- data/Rakefile +0 -2
- data/lib/zip_tricks.rb +1 -1
- data/lib/zip_tricks/file_reader/inflating_reader.rb +3 -0
- data/lib/zip_tricks/file_reader/stored_reader.rb +3 -0
- data/lib/zip_tricks/zip_writer.rb +51 -42
- data/zip_tricks.gemspec +6 -6
- metadata +10 -4
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 62ea03396a8a75e72000952d6c5ff69b9d8f0a80
|
4
|
+
data.tar.gz: 6cf0bca2293065a1b5430e73d26a9a0117064adb
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: c5ca0c25882b1770b663b199bc6f00b4763691f51f9bc34cd9594442be37e942738c1fbbcf6eff0d8aac56e99a81310f4e691a32aed3a84647e0a8e3d48aa9f2
|
7
|
+
data.tar.gz: 4d53363180343a0017e4054c2d55b40da0c74213a75a3aaf1e339715a6a3ad5ae9b7150abb79685f9cc9d19822d84e9122a12399a75f5488fccad18e35b31357
|
data/Gemfile
CHANGED
data/README.md
CHANGED
@@ -4,23 +4,28 @@
|
|
4
4
|
|
5
5
|
Allows streaming, non-rewinding ZIP file output from Ruby.
|
6
6
|
Spiritual successor to [zipline](https://github.com/fringd/zipline)
|
7
|
-
|
8
7
|
Requires Ruby 2.1+ syntax support and a working zlib (all available to jRuby as well).
|
9
8
|
|
9
|
+
Allows you to write a ZIP archive out to a File, Socket, String or Array without having to rewind it at any
|
10
|
+
point. Usable for creating very large ZIP archives for immediate sending out to clients, or for writing
|
11
|
+
large ZIP archives without memory inflation.
|
12
|
+
|
10
13
|
## Create a ZIP file without size estimation, compress on-the-fly)
|
11
14
|
|
12
15
|
When you compress on the fly and use data descriptors it is not really possible to compute the file size upfront.
|
13
16
|
But it is very likely to yield good compression - especially if you send things like CSV files.
|
14
17
|
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
18
|
+
```ruby
|
19
|
+
out = my_tempfile # can also be a socket
|
20
|
+
ZipTricks::Streamer.open(out) do |zip|
|
21
|
+
zip.write_stored_file('mov.mp4.txt') do |sink|
|
22
|
+
File.open('mov.mp4', 'rb'){|source| IO.copy_stream(source, sink) }
|
23
|
+
end
|
24
|
+
zip.write_deflated_file('long-novel.txt') do |sink|
|
25
|
+
File.open('novel.txt', 'rb'){|source| IO.copy_stream(source, sink) }
|
26
|
+
end
|
27
|
+
end
|
28
|
+
```
|
24
29
|
|
25
30
|
## Send the same ZIP file from a Rack response
|
26
31
|
|
@@ -28,35 +33,39 @@ Create a `RackBody` object and give it's constructor a block that adds files.
|
|
28
33
|
The block will only be called when actually sending the response to the client
|
29
34
|
(unless you are using a buffering Rack webserver, such as Webrick).
|
30
35
|
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
36
|
+
```ruby
|
37
|
+
body = ZipTricks::RackBody.new do | zip |
|
38
|
+
zip.write_stored_file('mov.mp4') do |sink| # Those MPEG4 files do not compress that well
|
39
|
+
File.open('mov.mp4', 'rb'){|source| IO.copy_stream(source, sink) }
|
40
|
+
end
|
41
|
+
zip.write_deflated_file('long-novel.txt') do |sink|
|
42
|
+
File.open('novel.txt', 'rb'){|source| IO.copy_stream(source, sink) }
|
43
|
+
end
|
44
|
+
end
|
45
|
+
[200, {'Transfer-Encoding' => 'chunked'}, body]
|
46
|
+
```
|
40
47
|
|
41
48
|
## Send a ZIP file of known size, with correct headers
|
42
49
|
|
43
50
|
Use the `SizeEstimator` to compute the correct size of the resulting archive.
|
44
51
|
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
52
|
+
```ruby
|
53
|
+
# Prepare the response body. The block will only be called when the response starts to be written.
|
54
|
+
zip_body = ZipTricks::RackBody.new do | zip |
|
55
|
+
zip.add_stored_entry(filename: "myfile1.bin", size: 9090821, crc32: 12485)
|
56
|
+
zip << read_file('myfile1.bin')
|
57
|
+
zip.add_stored_entry(filename: "myfile2.bin", size: 458678, crc32: 89568)
|
58
|
+
zip << read_file('myfile2.bin')
|
59
|
+
end
|
60
|
+
|
61
|
+
# Precompute the Content-Length ahead of time
|
62
|
+
bytesize = ZipTricks::SizeEstimator.estimate do |z|
|
63
|
+
z.add_stored_entry(filename: 'myfile1.bin', size: 9090821)
|
64
|
+
z.add_stored_entry(filename: 'myfile2.bin', size: 458678)
|
65
|
+
end
|
66
|
+
|
67
|
+
[200, {'Content-Length' => bytesize.to_s}, zip_body]
|
68
|
+
```
|
60
69
|
|
61
70
|
## Other usage examples
|
62
71
|
|
@@ -70,32 +79,36 @@ If the write destination for your use case is a `Socket` (say, you are writing u
|
|
70
79
|
the metadata of the file upfront (the CRC32 of the uncompressed file and the sizes), you can write directly
|
71
80
|
to that socket using some accelerated writing technique, and only use the Streamer to write out the ZIP metadata.
|
72
81
|
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
82
|
+
```ruby
|
83
|
+
# io has to be an object that supports #<<
|
84
|
+
ZipTricks::Streamer.open(io) do | zip |
|
85
|
+
# raw_file is written "as is" (STORED mode).
|
86
|
+
# Write the local file header first..
|
87
|
+
zip.add_stored_entry(filename: "first-file.bin", size: raw_file.size, crc32: raw_file_crc32)
|
88
|
+
|
89
|
+
# then send the actual file contents bypassing the Streamer interface
|
90
|
+
io.sendfile(my_temp_file)
|
91
|
+
|
92
|
+
# ...and then adjust the ZIP offsets within the Streamer
|
93
|
+
zip.simulate_write(my_temp_file.size)
|
94
|
+
end
|
95
|
+
```
|
85
96
|
|
86
97
|
## Computing the CRC32 value of a large file
|
87
98
|
|
88
|
-
`BlockCRC32` computes the CRC32 checksum of an IO in a streaming fashion.
|
89
|
-
than using the raw Zlib library functions.
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
+
`BlockCRC32` computes the CRC32 checksum of an IO in a streaming fashion.
|
100
|
+
It is slightly more convenient for the purpose than using the raw Zlib library functions.
|
101
|
+
|
102
|
+
```ruby
|
103
|
+
crc = ZipTricks::StreamCRC32.new
|
104
|
+
crc << large_file.read(1024 * 12) until large_file.eof?
|
105
|
+
...
|
106
|
+
|
107
|
+
crc.to_i # Returns the actual CRC32 value computed so far
|
108
|
+
...
|
109
|
+
# Append a known CRC32 value that has been computed previosuly
|
110
|
+
crc.append(precomputed_crc32, size_of_the_blob_computed_from)
|
111
|
+
```
|
99
112
|
|
100
113
|
## Contributing to zip_tricks
|
101
114
|
|
data/Rakefile
CHANGED
data/lib/zip_tricks.rb
CHANGED
@@ -48,11 +48,12 @@ class ZipTricks::ZipWriter
|
|
48
48
|
C_V = 'V'.freeze # Encode a 4-byte little-endian uint
|
49
49
|
C_v = 'v'.freeze # Encode a 2-byte little-endian uint
|
50
50
|
C_Qe = 'Q<'.freeze # Encode an 8-byte little-endian uint
|
51
|
-
|
51
|
+
C_es = ''.freeze
|
52
|
+
|
52
53
|
private_constant :FOUR_BYTE_MAX_UINT, :TWO_BYTE_MAX_UINT,
|
53
54
|
:VERSION_MADE_BY, :VERSION_NEEDED_TO_EXTRACT, :VERSION_NEEDED_TO_EXTRACT_ZIP64,
|
54
55
|
:DEFAULT_EXTERNAL_ATTRS, :MADE_BY_SIGNATURE,
|
55
|
-
:C_V, :C_v, :C_Qe, :ZIP_TRICKS_COMMENT
|
56
|
+
:C_V, :C_v, :C_Qe, :C_es, :ZIP_TRICKS_COMMENT
|
56
57
|
|
57
58
|
# Writes the local file header, that precedes the actual file _data_.
|
58
59
|
#
|
@@ -92,21 +93,21 @@ class ZipTricks::ZipWriter
|
|
92
93
|
# Filename should not be longer than 0xFFFF otherwise this wont fit here
|
93
94
|
io << [filename.bytesize].pack(C_v) # file name length 2 bytes
|
94
95
|
|
95
|
-
|
96
|
-
|
97
|
-
|
96
|
+
extra_fields = if requires_zip64
|
97
|
+
zip_64_extra_for_local_file_header(compressed_size: compressed_size, uncompressed_size: uncompressed_size)
|
98
|
+
else
|
99
|
+
C_es
|
98
100
|
end
|
99
|
-
io << [extra_size].pack(C_v) # extra field length 2 bytes
|
100
101
|
|
101
|
-
io <<
|
102
|
+
io << [extra_fields.bytesize].pack(C_v) # extra field length 2 bytes
|
103
|
+
|
104
|
+
io << filename # file name (variable size)
|
102
105
|
|
103
106
|
# Interesting tidbit:
|
104
107
|
# https://social.technet.microsoft.com/Forums/windows/en-US/6a60399f-2879-4859-b7ab-6ddd08a70948
|
105
108
|
# TL;DR of it is: Windows 7 Explorer _will_ open Zip64 entries. However, it desires to have the
|
106
109
|
# Zip64 extra field as _the first_ extra field. If we decide to add the Info-ZIP UTF-8 field...
|
107
|
-
if requires_zip64
|
108
|
-
write_zip_64_extra_for_local_file_header(io: io, compressed_size: compressed_size, uncompressed_size: uncompressed_size)
|
109
|
-
end
|
110
|
+
io << extra_fields if requires_zip64
|
110
111
|
end
|
111
112
|
|
112
113
|
# Writes the file header for the central directory, for a particular file in the archive. When writing out this data,
|
@@ -153,14 +154,13 @@ class ZipTricks::ZipWriter
|
|
153
154
|
# Filename should not be longer than 0xFFFF otherwise this wont fit here
|
154
155
|
io << [filename.bytesize].pack(C_v) # file name length 2 bytes
|
155
156
|
|
156
|
-
|
157
|
-
|
158
|
-
|
159
|
-
|
160
|
-
|
161
|
-
}
|
157
|
+
extra_fields = if add_zip64
|
158
|
+
zip_64_extra_for_central_directory_file_header(local_file_header_location: local_file_header_location,
|
159
|
+
compressed_size: compressed_size, uncompressed_size: uncompressed_size)
|
160
|
+
else
|
161
|
+
C_es
|
162
162
|
end
|
163
|
-
io << [
|
163
|
+
io << [extra_fields.bytesize].pack(C_v) # extra field length 2 bytes
|
164
164
|
|
165
165
|
io << [0].pack(C_v) # file comment length 2 bytes
|
166
166
|
|
@@ -181,11 +181,7 @@ class ZipTricks::ZipWriter
|
|
181
181
|
io << [local_file_header_location].pack(C_V)
|
182
182
|
end
|
183
183
|
io << filename # file name (variable size)
|
184
|
-
|
185
|
-
if add_zip64 # extra field (variable size)
|
186
|
-
write_zip_64_extra_for_central_directory_file_header(io: io, local_file_header_location: local_file_header_location,
|
187
|
-
compressed_size: compressed_size, uncompressed_size: uncompressed_size)
|
188
|
-
end
|
184
|
+
io << extra_fields # extra field (variable size)
|
189
185
|
#(empty) # file comment (variable size)
|
190
186
|
end
|
191
187
|
|
@@ -305,38 +301,38 @@ class ZipTricks::ZipWriter
|
|
305
301
|
|
306
302
|
# Writes the Zip64 extra field for the local file header. Will be used by `write_local_file_header` when any sizes given to it warrant that.
|
307
303
|
#
|
308
|
-
# @param io[#<<] the buffer to write the local file header to
|
309
304
|
# @param compressed_size[Fixnum] The size of the compressed (or stored) data - how much space it uses in the ZIP
|
310
305
|
# @param uncompressed_size[Fixnum] The size of the file once extracted
|
311
|
-
# @return [
|
312
|
-
def
|
313
|
-
|
314
|
-
|
315
|
-
|
316
|
-
|
306
|
+
# @return [String]
|
307
|
+
def zip_64_extra_for_local_file_header(compressed_size:, uncompressed_size:)
|
308
|
+
data_and_packspecs = [
|
309
|
+
0x0001, C_v, # 2 bytes Tag for this "extra" block type
|
310
|
+
16, C_v, # 2 bytes Size of this "extra" block. For us it will always be 16 (2x8)
|
311
|
+
uncompressed_size, C_Qe, # 8 bytes Original uncompressed file size
|
312
|
+
compressed_size, C_Qe, # 8 bytes Size of compressed data
|
313
|
+
]
|
314
|
+
pack_array(data_and_packspecs)
|
317
315
|
end
|
318
316
|
|
319
317
|
# Writes the Zip64 extra field for the central directory header.It differs from the extra used in the local file header because it
|
320
318
|
# also contains the location of the local file header in the ZIP as an 8-byte int.
|
321
319
|
#
|
322
|
-
# @param io[#<<] the buffer to write the local file header to
|
323
320
|
# @param compressed_size[Fixnum] The size of the compressed (or stored) data - how much space it uses in the ZIP
|
324
321
|
# @param uncompressed_size[Fixnum] The size of the file once extracted
|
325
322
|
# @param local_file_header_location[Fixnum] Byte offset of the start of the local file header from the beginning of the ZIP archive
|
326
|
-
# @return [
|
327
|
-
def
|
328
|
-
|
329
|
-
|
330
|
-
|
331
|
-
|
332
|
-
|
333
|
-
|
323
|
+
# @return [String]
|
324
|
+
def zip_64_extra_for_central_directory_file_header(compressed_size:, uncompressed_size:, local_file_header_location:)
|
325
|
+
data_and_packspecs = [
|
326
|
+
0x0001, C_v, # 2 bytes Tag for this "extra" block type
|
327
|
+
28, C_v, # 2 bytes Size of this "extra" block. For us it will always be 28
|
328
|
+
uncompressed_size, C_Qe, # 8 bytes Original uncompressed file size
|
329
|
+
compressed_size, C_Qe, # 8 bytes Size of compressed data
|
330
|
+
local_file_header_location, C_Qe, # 8 bytes Offset of local header record
|
331
|
+
0, C_V, # 4 bytes Number of the disk on which this file starts
|
332
|
+
]
|
333
|
+
pack_array(data_and_packspecs)
|
334
334
|
end
|
335
335
|
|
336
|
-
def bytesize_of
|
337
|
-
''.force_encoding(Encoding::BINARY).tap {|b| yield(b) }.bytesize
|
338
|
-
end
|
339
|
-
|
340
336
|
def to_binary_dos_time(t)
|
341
337
|
(t.sec/2) + (t.min << 5) + (t.hour << 11)
|
342
338
|
end
|
@@ -344,4 +340,17 @@ class ZipTricks::ZipWriter
|
|
344
340
|
def to_binary_dos_date(t)
|
345
341
|
(t.day) + (t.month << 5) + ((t.year - 1980) << 9)
|
346
342
|
end
|
343
|
+
|
344
|
+
# Unzips a given array of tuples of "numeric value, pack specifier" and then packs all the odd
|
345
|
+
# values using specifiers from all the even values. It is harder to explain than to show:
|
346
|
+
#
|
347
|
+
# pack_array([1, 'V', 2, 'v', 148, 'v]) #=> "\x01\x00\x00\x00\x02\x00\x94\x00"
|
348
|
+
#
|
349
|
+
# will do the following two transforms:
|
350
|
+
#
|
351
|
+
# [1, 'V', 2, 'v', 148, 'v] -> [1,2,148], ['V','v','v'] -> [1,2,148].pack('Vvv') -> "\x01\x00\x00\x00\x02\x00\x94\x00"
|
352
|
+
def pack_array(values_to_packspecs)
|
353
|
+
values, packspecs = values_to_packspecs.partition.each_with_index { |_, i| i.even? }
|
354
|
+
values.pack(packspecs.join)
|
355
|
+
end
|
347
356
|
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 4.2.
|
5
|
+
# stub: zip_tricks 4.2.2 ruby lib
|
6
6
|
|
7
7
|
Gem::Specification.new do |s|
|
8
8
|
s.name = "zip_tricks"
|
9
|
-
s.version = "4.2.
|
9
|
+
s.version = "4.2.2"
|
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-11-13"
|
15
15
|
s.description = "Stream out ZIP files from Ruby"
|
16
16
|
s.email = "me@julik.nl"
|
17
17
|
s.extra_rdoc_files = [
|
@@ -86,7 +86,7 @@ Gem::Specification.new do |s|
|
|
86
86
|
s.add_development_dependency(%q<coderay>, [">= 0"])
|
87
87
|
s.add_development_dependency(%q<yard>, ["~> 0.8"])
|
88
88
|
s.add_development_dependency(%q<bundler>, ["~> 1.0"])
|
89
|
-
s.add_development_dependency(%q<jeweler>, ["
|
89
|
+
s.add_development_dependency(%q<jeweler>, [">= 2.1.2", "~> 2"])
|
90
90
|
else
|
91
91
|
s.add_dependency(%q<rubyzip>, ["~> 1.1"])
|
92
92
|
s.add_dependency(%q<terminal-table>, [">= 0"])
|
@@ -97,7 +97,7 @@ Gem::Specification.new do |s|
|
|
97
97
|
s.add_dependency(%q<coderay>, [">= 0"])
|
98
98
|
s.add_dependency(%q<yard>, ["~> 0.8"])
|
99
99
|
s.add_dependency(%q<bundler>, ["~> 1.0"])
|
100
|
-
s.add_dependency(%q<jeweler>, ["
|
100
|
+
s.add_dependency(%q<jeweler>, [">= 2.1.2", "~> 2"])
|
101
101
|
end
|
102
102
|
else
|
103
103
|
s.add_dependency(%q<rubyzip>, ["~> 1.1"])
|
@@ -109,7 +109,7 @@ Gem::Specification.new do |s|
|
|
109
109
|
s.add_dependency(%q<coderay>, [">= 0"])
|
110
110
|
s.add_dependency(%q<yard>, ["~> 0.8"])
|
111
111
|
s.add_dependency(%q<bundler>, ["~> 1.0"])
|
112
|
-
s.add_dependency(%q<jeweler>, ["
|
112
|
+
s.add_dependency(%q<jeweler>, [">= 2.1.2", "~> 2"])
|
113
113
|
end
|
114
114
|
end
|
115
115
|
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: zip_tricks
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 4.2.
|
4
|
+
version: 4.2.2
|
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-11-13 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: rubyzip
|
@@ -146,16 +146,22 @@ dependencies:
|
|
146
146
|
name: jeweler
|
147
147
|
requirement: !ruby/object:Gem::Requirement
|
148
148
|
requirements:
|
149
|
+
- - ">="
|
150
|
+
- !ruby/object:Gem::Version
|
151
|
+
version: 2.1.2
|
149
152
|
- - "~>"
|
150
153
|
- !ruby/object:Gem::Version
|
151
|
-
version: 2
|
154
|
+
version: '2'
|
152
155
|
type: :development
|
153
156
|
prerelease: false
|
154
157
|
version_requirements: !ruby/object:Gem::Requirement
|
155
158
|
requirements:
|
159
|
+
- - ">="
|
160
|
+
- !ruby/object:Gem::Version
|
161
|
+
version: 2.1.2
|
156
162
|
- - "~>"
|
157
163
|
- !ruby/object:Gem::Version
|
158
|
-
version: 2
|
164
|
+
version: '2'
|
159
165
|
description: Stream out ZIP files from Ruby
|
160
166
|
email: me@julik.nl
|
161
167
|
executables: []
|