zip_tricks 4.2.1 → 4.2.2
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/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: []
|