zip_kit 6.2.0 → 6.2.2
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +13 -1
- data/CONTRIBUTING.md +2 -2
- data/README.md +50 -31
- data/examples/sinatra_application.rb +16 -0
- data/lib/zip_kit/block_deflate.rb +0 -2
- data/lib/zip_kit/block_write.rb +4 -1
- data/lib/zip_kit/output_enumerator.rb +25 -5
- data/lib/zip_kit/rails_streaming.rb +56 -15
- data/lib/zip_kit/streamer.rb +11 -11
- data/lib/zip_kit/version.rb +1 -1
- data/lib/zip_kit/write_shovel.rb +2 -2
- data/lib/zip_kit.rb +1 -0
- data/rbi/zip_kit.rbi +71 -47
- data/zip_kit.gemspec +7 -3
- metadata +20 -4
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: e1136ebba851638486c9e47150a8d706c49a2bbc0074f457c794582d8ce19089
|
4
|
+
data.tar.gz: 80de3edcb5bc748aaf855a7bf0b1f19439522c8efa4b97754f813fe9413bac2c
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 20c5922a4178f2068a4f06388b201bd263f01c387d308c2c6297feba1c05385d601072cae0451d59a4a0b4e1ba1e354a6fa7f622ff1b58daf70947e6991b1e82
|
7
|
+
data.tar.gz: c373972ec6980000b40d1808247759b44f317a7aa3795b406e02005412cf0687e0f2311e5809f011eb1fbc19e6b2b7eb2a6a8f036cafe27a2645f6476cf0c441
|
data/CHANGELOG.md
CHANGED
@@ -1,3 +1,15 @@
|
|
1
|
+
## 6.2.2
|
2
|
+
|
3
|
+
* Make sure "zlib" gets required at the top, as it is used everywhere
|
4
|
+
* Improve documentation
|
5
|
+
* Make sure `zip_kit_stream` honors the custom `Content-Type` parameter
|
6
|
+
* Add a streaming example with Sinatra (and add a Sinatra app to the test harness)
|
7
|
+
|
8
|
+
## 6.2.1
|
9
|
+
|
10
|
+
* Make `RailsStreaming` compatible with `ActionController::Live` (previously the response would hang)
|
11
|
+
* Make `BlockWrite` respond to `write` in addition to `<<`
|
12
|
+
|
1
13
|
## 6.2.0
|
2
14
|
|
3
15
|
* Remove forced `Transfer-Encoding: chunked` and the chunking body wrapper. It is actually a good idea to trust the app webserver to apply the transfer encoding as is appropriate. For the case when "you really have to", add a bypass in `RailsStreaming#zip_kit_stream` for forcing the chunking manually.
|
@@ -149,7 +161,7 @@
|
|
149
161
|
## 4.4.2
|
150
162
|
|
151
163
|
* Add 2.4 to Travis rubies
|
152
|
-
* Fix a severe performance degradation in Streamer with large file counts (https://github.com/WeTransfer/
|
164
|
+
* Fix a severe performance degradation in Streamer with large file counts (https://github.com/WeTransfer/zip_tricks/pull/14)
|
153
165
|
|
154
166
|
## 4.4.1
|
155
167
|
|
data/CONTRIBUTING.md
CHANGED
@@ -106,11 +106,11 @@ project:
|
|
106
106
|
|
107
107
|
```bash
|
108
108
|
# Clone your fork of the repo into the current directory
|
109
|
-
git clone git@github.com:
|
109
|
+
git clone git@github.com:julik/zip_kit.git
|
110
110
|
# Navigate to the newly cloned directory
|
111
111
|
cd zip_kit
|
112
112
|
# Assign the original repo to a remote called "upstream"
|
113
|
-
git remote add upstream git@github.com:
|
113
|
+
git remote add upstream git@github.com:julik/zip_kit.git
|
114
114
|
```
|
115
115
|
|
116
116
|
2. If you cloned a while ago, get the latest changes from upstream:
|
data/README.md
CHANGED
@@ -55,11 +55,11 @@ If you want some more conveniences you can also use [zipline](https://github.com
|
|
55
55
|
will automatically process and stream attachments (Carrierwave, Shrine, ActiveStorage) and remote objects
|
56
56
|
via HTTP.
|
57
57
|
|
58
|
-
`RailsStreaming`
|
59
|
-
and
|
60
|
-
|
58
|
+
`RailsStreaming` does *not* require [ActionController::Live](https://api.rubyonrails.org/classes/ActionController/Live.html)
|
59
|
+
and will stream without it. See {ZipKit::RailsStreaming#zip_kit_stream} for more details on this. You can use it
|
60
|
+
together with `Live` just fine if you need to.
|
61
61
|
|
62
|
-
## Writing into
|
62
|
+
## Writing into streaming destinations
|
63
63
|
|
64
64
|
Any object that accepts bytes via either `<<` or `write` methods can be a write destination. For example, here
|
65
65
|
is how to upload a sizeable ZIP to S3 - the SDK will happily chop your upload into multipart upload parts:
|
@@ -69,23 +69,23 @@ bucket = Aws::S3::Bucket.new("mybucket")
|
|
69
69
|
obj = bucket.object("big.zip")
|
70
70
|
obj.upload_stream do |write_stream|
|
71
71
|
ZipKit::Streamer.open(write_stream) do |zip|
|
72
|
-
zip.write_file("
|
73
|
-
|
74
|
-
|
75
|
-
20_000.times do |n|
|
76
|
-
csv << [n, "Item number #{n}"]
|
77
|
-
end
|
72
|
+
zip.write_file("file.csv") do |sink|
|
73
|
+
File.open("large.csv", "rb") do |file_input|
|
74
|
+
IO.copy_stream(file_input, sink)
|
78
75
|
end
|
79
76
|
end
|
80
77
|
end
|
81
78
|
end
|
82
79
|
```
|
83
80
|
|
81
|
+
## Writing through streaming wrappers
|
82
|
+
|
84
83
|
Any object that writes using either `<<` or `write` can write into a `sink`. For example, you can do streaming
|
85
|
-
output with [builder](https://github.com/jimweirich/builder#project-builder)
|
84
|
+
output with [builder](https://github.com/jimweirich/builder#project-builder) which calls `<<` on its `target`
|
85
|
+
every time a complete write call is done:
|
86
86
|
|
87
87
|
```ruby
|
88
|
-
zip.write_file('
|
88
|
+
zip.write_file('employees.xml') do |sink|
|
89
89
|
builder = Builder::XmlMarkup.new(target: sink, indent: 2)
|
90
90
|
builder.people do
|
91
91
|
Person.all.find_each do |person|
|
@@ -95,14 +95,30 @@ zip.write_file('report1.csv') do |sink|
|
|
95
95
|
end
|
96
96
|
```
|
97
97
|
|
98
|
-
|
99
|
-
Ruby code that streams its output into a destination.
|
98
|
+
The output will be compressed and output into the ZIP file on the fly. Same for CSV:
|
100
99
|
|
101
|
-
|
100
|
+
```ruby
|
101
|
+
zip.write_file('line_items.csv') do |sink|
|
102
|
+
CSV(sink) do |csv|
|
103
|
+
csv << ["Line", "Item"]
|
104
|
+
20_000.times do |n|
|
105
|
+
csv << [n, "Item number #{n}"]
|
106
|
+
end
|
107
|
+
end
|
108
|
+
end
|
109
|
+
```
|
110
|
+
|
111
|
+
## Automatic storage mode (stored vs. deflated)
|
112
|
+
|
113
|
+
The ZIP file format allows storage in both compressed and raw storage modes. The raw ("stored")
|
114
|
+
mode does not require decompression and unarchives faster.
|
102
115
|
|
103
|
-
|
104
|
-
|
105
|
-
|
116
|
+
ZipKit will buffer a small amount of output and attempt to compress it using deflate compression.
|
117
|
+
If this turns out to be significantly smaller than raw data, it is then going to proceed with
|
118
|
+
all further output using deflate compression. Memory use is going to be very modest, but it allows
|
119
|
+
you to not have to think about the appropriate storage mode.
|
120
|
+
|
121
|
+
Deflate compression will work great for JSONs, CSVs and other text- or text-like formats. For example, here is how to
|
106
122
|
output direct to STDOUT (so that you can run `$ ruby archive.rb > file.zip` in your terminal):
|
107
123
|
|
108
124
|
```ruby
|
@@ -116,18 +132,16 @@ ZipKit::Streamer.open($stdout) do |zip|
|
|
116
132
|
end
|
117
133
|
```
|
118
134
|
|
119
|
-
|
120
|
-
|
135
|
+
If you want to use specific storage modes, use `write_deflated_file` and `write_stored_file` instead of
|
136
|
+
`write_file`.
|
121
137
|
|
122
138
|
## Send a ZIP from a Rack response
|
123
139
|
|
124
140
|
zip_kit provides an `OutputEnumerator` object which will yield the binary chunks piece
|
125
|
-
by piece, and apply some amount of buffering as well.
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
start executing once your response body starts getting iterated over - when actually sending
|
130
|
-
the response to the client (unless you are using a buffering Rack webserver, such as Webrick).
|
141
|
+
by piece, and apply some amount of buffering as well. Return the headers and the body to your webserver
|
142
|
+
and you will have your ZIP streamed! The block that you give to the `OutputEnumerator` will receive
|
143
|
+
the {ZipKit::Streamer} object and will only start executing once your response body starts getting iterated
|
144
|
+
over - when actually sending the response to the client (unless you are using a buffering Rack webserver, such as Webrick).
|
131
145
|
|
132
146
|
```ruby
|
133
147
|
body = ZipKit::OutputEnumerator.new do | zip |
|
@@ -139,13 +153,16 @@ body = ZipKit::OutputEnumerator.new do | zip |
|
|
139
153
|
end
|
140
154
|
end
|
141
155
|
|
142
|
-
|
143
|
-
[200, headers, streaming_body]
|
156
|
+
[200, body.streaming_http_headers, body]
|
144
157
|
```
|
145
158
|
|
146
159
|
## Send a ZIP file of known size, with correct headers
|
147
160
|
|
148
|
-
|
161
|
+
Sending a file with data descriptors is not always desirable - you don't really know how large your ZIP is going to be.
|
162
|
+
If you want to present your users with proper download progress, you would need to set a `Content-Length` header - and
|
163
|
+
know ahead of time how large your download is going to be. This can be done with ZipKit, provided you know how large
|
164
|
+
the compressed versions of your file are going to be. Use the {ZipKit::SizeEstimator} to do the pre-calculation - it
|
165
|
+
is not going to produce any large amounts of output, and will give you a to-the-byte value for your future archive:
|
149
166
|
|
150
167
|
```ruby
|
151
168
|
bytesize = ZipKit::SizeEstimator.estimate do |z|
|
@@ -160,8 +177,10 @@ zip_body = ZipKit::OutputEnumerator.new do | zip |
|
|
160
177
|
zip << read_file('myfile2.bin')
|
161
178
|
end
|
162
179
|
|
163
|
-
|
164
|
-
[
|
180
|
+
hh = zip_body.streaming_http_headers
|
181
|
+
hh["Content-Length"] = bytesize.to_s
|
182
|
+
|
183
|
+
[200, hh, zip_body]
|
165
184
|
```
|
166
185
|
|
167
186
|
## Writing ZIP files using the Streamer bypass
|
@@ -0,0 +1,16 @@
|
|
1
|
+
require "sinatra/base"
|
2
|
+
|
3
|
+
class SinatraApp < Sinatra::Base
|
4
|
+
get "/" do
|
5
|
+
content_type :zip
|
6
|
+
stream do |out|
|
7
|
+
ZipKit::Streamer.open(out) do |z|
|
8
|
+
z.write_file(File.basename(__FILE__)) do |io|
|
9
|
+
File.open(__FILE__, "r") do |f|
|
10
|
+
IO.copy_stream(f, io)
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
data/lib/zip_kit/block_write.rb
CHANGED
@@ -17,9 +17,12 @@
|
|
17
17
|
# end
|
18
18
|
# [200, {}, MyRackResponse.new]
|
19
19
|
class ZipKit::BlockWrite
|
20
|
+
include ZipKit::WriteShovel
|
21
|
+
|
20
22
|
# Creates a new BlockWrite.
|
21
23
|
#
|
22
24
|
# @param block The block that will be called when this object receives the `<<` message
|
25
|
+
# @yieldparam bytes[String] A string in binary encoding which has just been written into the object
|
23
26
|
def initialize(&block)
|
24
27
|
@block = block
|
25
28
|
end
|
@@ -36,7 +39,7 @@ class ZipKit::BlockWrite
|
|
36
39
|
# @param buf[String] the string to write. Note that a zero-length String
|
37
40
|
# will not be forwarded to the block, as it has special meaning when used
|
38
41
|
# with chunked encoding (it indicates the end of the stream).
|
39
|
-
# @return
|
42
|
+
# @return [ZipKit::BlockWrite]
|
40
43
|
def <<(buf)
|
41
44
|
# Zero-size output has a special meaning when using chunked encoding
|
42
45
|
return if buf.nil? || buf.bytesize.zero?
|
@@ -34,6 +34,15 @@ require "time" # for .httpdate
|
|
34
34
|
#
|
35
35
|
# to bypass things like `Rack::ETag` and the nginx buffering.
|
36
36
|
class ZipKit::OutputEnumerator
|
37
|
+
# With HTTP output it is better to apply a small amount of buffering. While Streamer
|
38
|
+
# output does not buffer at all, the `OutputEnumerator` does as it is going to
|
39
|
+
# be used as a Rack response body. Applying some buffering helps reduce the number
|
40
|
+
# of syscalls for otherwise tiny writes, which relieves the app webserver from
|
41
|
+
# doing too much work managing those writes. While we recommend buffering, the
|
42
|
+
# buffer size is configurable via the constructor - so you can disable buffering
|
43
|
+
# if you really need to. While ZipKit ams not to buffer, in this instance this
|
44
|
+
# buffering is justified. See https://github.com/WeTransfer/zip_tricks/issues/78
|
45
|
+
# for the background on buffering.
|
37
46
|
DEFAULT_WRITE_BUFFER_SIZE = 64 * 1024
|
38
47
|
|
39
48
|
# Creates a new OutputEnumerator enumerator. The enumerator can be read from using `each`,
|
@@ -60,14 +69,11 @@ class ZipKit::OutputEnumerator
|
|
60
69
|
# ...
|
61
70
|
# end
|
62
71
|
#
|
63
|
-
# @param kwargs_for_new [Hash] keyword arguments for {Streamer.new}
|
64
|
-
# @return [ZipKit::OutputEnumerator] the enumerator you can read bytestrings of the ZIP from by calling `each`
|
65
|
-
#
|
66
72
|
# @param streamer_options[Hash] options for Streamer, see {ZipKit::Streamer.new}
|
67
73
|
# @param write_buffer_size[Integer] By default all ZipKit writes are unbuffered. For output to sockets
|
68
74
|
# it is beneficial to bulkify those writes so that they are roughly sized to a socket buffer chunk. This
|
69
75
|
# object will bulkify writes for you in this way (so `each` will yield not on every call to `<<` from the Streamer
|
70
|
-
# but at block size boundaries or greater). Set
|
76
|
+
# but at block size boundaries or greater). Set the parameter to 0 for unbuffered writes.
|
71
77
|
# @param blk a block that will receive the Streamer object when executing. The block will not be executed
|
72
78
|
# immediately but only once `each` is called on the OutputEnumerator
|
73
79
|
def initialize(write_buffer_size: DEFAULT_WRITE_BUFFER_SIZE, **streamer_options, &blk)
|
@@ -100,9 +106,14 @@ class ZipKit::OutputEnumerator
|
|
100
106
|
end
|
101
107
|
|
102
108
|
# Returns a Hash of HTTP response headers you are likely to need to have your response stream correctly.
|
109
|
+
# This is on the {ZipKit::OutputEnumerator} class since those headers are common, independent of the
|
110
|
+
# particular response body getting served. You might want to override the headers with your particular
|
111
|
+
# ones - for example, specific content types are needed for files which are, technically, ZIP files
|
112
|
+
# but are of a file format built "on top" of ZIPs - such as ODTs, [pkpass files](https://developer.apple.com/documentation/walletpasses/building_a_pass)
|
113
|
+
# and ePubs.
|
103
114
|
#
|
104
115
|
# @return [Hash]
|
105
|
-
def streaming_http_headers
|
116
|
+
def self.streaming_http_headers
|
106
117
|
_headers = {
|
107
118
|
# We need to ensure Rack::ETag does not suddenly start buffering us, see
|
108
119
|
# https://github.com/rack/rack/issues/1619#issuecomment-606315714
|
@@ -121,6 +132,15 @@ class ZipKit::OutputEnumerator
|
|
121
132
|
}
|
122
133
|
end
|
123
134
|
|
135
|
+
# Returns a Hash of HTTP response headers for this particular response. This used to contain "Content-Length" for
|
136
|
+
# presized responses, but is now effectively a no-op.
|
137
|
+
#
|
138
|
+
# @see [ZipKit::OutputEnumerator.streaming_http_headers]
|
139
|
+
# @return [Hash]
|
140
|
+
def streaming_http_headers
|
141
|
+
self.class.streaming_http_headers
|
142
|
+
end
|
143
|
+
|
124
144
|
# Returns a tuple of `headers, body` - headers are a `Hash` and the body is
|
125
145
|
# an object that can be used as a Rack response body. This method used to accept arguments
|
126
146
|
# but will now just ignore them.
|
@@ -5,18 +5,29 @@ module ZipKit::RailsStreaming
|
|
5
5
|
# Opens a {ZipKit::Streamer} and yields it to the caller. The output of the streamer
|
6
6
|
# gets automatically forwarded to the Rails response stream. When the output completes,
|
7
7
|
# the Rails response stream is going to be closed automatically.
|
8
|
+
#
|
9
|
+
# Note that there is an important difference in how this method works, depending whether
|
10
|
+
# you use it in a controller which includes `ActionController::Live` vs. one that does not.
|
11
|
+
# With a standard `ActionController` this method will assign a response body, but streaming
|
12
|
+
# will begin when your action method returns. With `ActionController::Live` the streaming
|
13
|
+
# will begin immediately, before the method returns. In all other aspects the method should
|
14
|
+
# stream correctly in both types of controllers.
|
15
|
+
#
|
16
|
+
# If you encounter buffering (streaming does not start for a very long time) you probably
|
17
|
+
# have a piece of Rack middleware in your stack which buffers. Known offenders are `Rack::ContentLength`,
|
18
|
+
# `Rack::MiniProfiler` and `Rack::ETag`. ZipKit will try to work around these but it is not
|
19
|
+
# always possible. If you encounter buffering, examine your middleware stack and try to suss
|
20
|
+
# out whether any middleware might be buffering. You can also try setting `use_chunked_transfer_encoding`
|
21
|
+
# to `true` - this is not recommended but sometimes necessary, for example to bypass `Rack::ContentLength`.
|
22
|
+
#
|
8
23
|
# @param filename[String] name of the file for the Content-Disposition header
|
9
24
|
# @param type[String] the content type (MIME type) of the archive being output
|
10
25
|
# @param use_chunked_transfer_encoding[Boolean] whether to forcibly encode output as chunked. Normally you should not need this.
|
11
|
-
# @param
|
12
|
-
# See {ZipKit::
|
26
|
+
# @param output_enumerator_options[Hash] options that will be passed to the OutputEnumerator - these include
|
27
|
+
# options for the Streamer. See {ZipKit::OutputEnumerator#initialize} for the full list of options.
|
13
28
|
# @yieldparam [ZipKit::Streamer] the streamer that can be written to
|
14
|
-
# @return [
|
15
|
-
def zip_kit_stream(filename: "download.zip", type: "application/zip", use_chunked_transfer_encoding: false, **
|
16
|
-
# The output enumerator yields chunks of bytes generated from ZipKit. Instantiating it
|
17
|
-
# first will also validate the Streamer options.
|
18
|
-
output_enum = ZipKit::OutputEnumerator.new(**zip_streamer_options, &zip_streaming_blk)
|
19
|
-
|
29
|
+
# @return [Boolean] always returns true
|
30
|
+
def zip_kit_stream(filename: "download.zip", type: "application/zip", use_chunked_transfer_encoding: false, **output_enumerator_options, &zip_streaming_blk)
|
20
31
|
# We want some common headers for file sending. Rails will also set
|
21
32
|
# self.sending_file = true for us when we call send_file_headers!
|
22
33
|
send_file_headers!(type: type, filename: filename)
|
@@ -29,16 +40,46 @@ module ZipKit::RailsStreaming
|
|
29
40
|
logger&.warn { "The downstream HTTP proxy/LB insists on HTTP/1.0 protocol, ZIP response will be buffered." }
|
30
41
|
end
|
31
42
|
|
32
|
-
headers =
|
43
|
+
headers = ZipKit::OutputEnumerator.streaming_http_headers
|
44
|
+
|
45
|
+
# Allow Rails headers to override ours. This is important if, for example, a content type gets
|
46
|
+
# set to something else than "application/zip"
|
47
|
+
response.headers.reverse_merge!(headers)
|
48
|
+
|
49
|
+
# The output enumerator yields chunks of bytes generated from the Streamer,
|
50
|
+
# with some buffering. See OutputEnumerator docs for more.
|
51
|
+
rack_zip_body = ZipKit::OutputEnumerator.new(**output_enumerator_options, &zip_streaming_blk)
|
33
52
|
|
34
|
-
#
|
35
|
-
#
|
53
|
+
# Chunked encoding may be forced if, for example, you _need_ to bypass Rack::ContentLength.
|
54
|
+
# Rack::ContentLength is normally not in a Rails middleware stack, but it might get
|
55
|
+
# introduced unintentionally - for example, "rackup" adds the ContentLength middleware for you.
|
56
|
+
# There is a recommendation to leave the chunked encoding to the app server, so that servers
|
57
|
+
# that support HTTP/2 can use native framing and not have to deal with the chunked encoding,
|
58
|
+
# see https://github.com/julik/zip_kit/issues/7
|
59
|
+
# But it is not to be excluded that a user may need to force the chunked encoding to bypass
|
60
|
+
# some especially pesky Rack middleware that just would not cooperate. Those include
|
61
|
+
# Rack::MiniProfiler and the above-mentioned Rack::ContentLength.
|
36
62
|
if use_chunked_transfer_encoding
|
37
|
-
|
38
|
-
|
63
|
+
response.headers["Transfer-Encoding"] = "chunked"
|
64
|
+
rack_zip_body = ZipKit::RackChunkedBody.new(rack_zip_body)
|
65
|
+
end
|
66
|
+
|
67
|
+
# Time for some branching, which mostly has to do with the 999 flavours of
|
68
|
+
# "how to make both Rails and Rack stream"
|
69
|
+
if self.class.ancestors.include?(ActionController::Live)
|
70
|
+
# If this controller includes Live it will not work correctly with a Rack
|
71
|
+
# response body assignment - the action will just hang. We need to read out the response
|
72
|
+
# body ourselves and write it into the Rails stream.
|
73
|
+
begin
|
74
|
+
rack_zip_body.each { |bytes| response.stream.write(bytes) }
|
75
|
+
ensure
|
76
|
+
response.stream.close
|
77
|
+
end
|
78
|
+
else
|
79
|
+
# Stream using a Rack body assigned to the ActionController response body
|
80
|
+
self.response_body = rack_zip_body
|
39
81
|
end
|
40
82
|
|
41
|
-
|
42
|
-
self.response_body = output_enum
|
83
|
+
true
|
43
84
|
end
|
44
85
|
end
|
data/lib/zip_kit/streamer.rb
CHANGED
@@ -2,19 +2,19 @@
|
|
2
2
|
|
3
3
|
require "set"
|
4
4
|
|
5
|
-
# Is used to write
|
6
|
-
#
|
7
|
-
# of this object can be coupled directly to, say, a Rack output. The
|
8
|
-
# output can also be a String, Array or anything that responds to `<<`.
|
5
|
+
# Is used to write ZIP archives without having to read them back or to overwrite
|
6
|
+
# data. It outputs into any object that supports `<<` or `write`, namely:
|
9
7
|
#
|
10
|
-
#
|
11
|
-
#
|
8
|
+
# An `Array`, `File`, `IO`, `Socket` and even `String` all can be output destinations
|
9
|
+
# for the `Streamer`.
|
12
10
|
#
|
13
|
-
#
|
14
|
-
#
|
11
|
+
# You can also combine output through the `Streamer` with direct output to the destination,
|
12
|
+
# all while preserving the correct offsets in the ZIP file structures. This allows usage
|
13
|
+
# of `sendfile()` or socket `splice()` calls for "through" proxying.
|
15
14
|
#
|
16
|
-
#
|
17
|
-
#
|
15
|
+
# If you want to avoid data descriptors - or write data bypassing the Streamer -
|
16
|
+
# you need to know the CRC32 (as a uint) and the filesize upfront,
|
17
|
+
# before the writing of the entry body starts.
|
18
18
|
#
|
19
19
|
# ## Using the Streamer with runtime compression
|
20
20
|
#
|
@@ -34,7 +34,7 @@ require "set"
|
|
34
34
|
# end
|
35
35
|
# end
|
36
36
|
#
|
37
|
-
# The central directory will be written automatically at the end of the block.
|
37
|
+
# The central directory will be written automatically at the end of the `open` block.
|
38
38
|
#
|
39
39
|
# ## Using the Streamer with entries of known size and having a known CRC32 checksum
|
40
40
|
#
|
data/lib/zip_kit/version.rb
CHANGED
data/lib/zip_kit/write_shovel.rb
CHANGED
@@ -13,8 +13,8 @@ module ZipKit::WriteShovel
|
|
13
13
|
# Writes the given data to the output stream. Allows the object to be used as
|
14
14
|
# a target for `IO.copy_stream(from, to)`
|
15
15
|
#
|
16
|
-
# @param
|
17
|
-
# @return [Fixnum] the number of bytes written
|
16
|
+
# @param bytes[String] the binary string to write (part of the uncompressed file)
|
17
|
+
# @return [Fixnum] the number of bytes written (will always be the bytesize of `bytes`)
|
18
18
|
def write(bytes)
|
19
19
|
self << bytes
|
20
20
|
bytes.bytesize
|
data/lib/zip_kit.rb
CHANGED
data/rbi/zip_kit.rbi
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
# typed: strong
|
2
2
|
module ZipKit
|
3
|
-
VERSION = T.let("6.2.
|
3
|
+
VERSION = T.let("6.2.2", T.untyped)
|
4
4
|
|
5
5
|
# A ZIP archive contains a flat list of entries. These entries can implicitly
|
6
6
|
# create directories when the archive is expanded. For example, an entry with
|
@@ -100,19 +100,19 @@ module ZipKit
|
|
100
100
|
end
|
101
101
|
end
|
102
102
|
|
103
|
-
# Is used to write
|
104
|
-
#
|
105
|
-
# of this object can be coupled directly to, say, a Rack output. The
|
106
|
-
# output can also be a String, Array or anything that responds to `<<`.
|
103
|
+
# Is used to write ZIP archives without having to read them back or to overwrite
|
104
|
+
# data. It outputs into any object that supports `<<` or `write`, namely:
|
107
105
|
#
|
108
|
-
#
|
109
|
-
#
|
106
|
+
# An `Array`, `File`, `IO`, `Socket` and even `String` all can be output destinations
|
107
|
+
# for the `Streamer`.
|
110
108
|
#
|
111
|
-
#
|
112
|
-
#
|
109
|
+
# You can also combine output through the `Streamer` with direct output to the destination,
|
110
|
+
# all while preserving the correct offsets in the ZIP file structures. This allows usage
|
111
|
+
# of `sendfile()` or socket `splice()` calls for "through" proxying.
|
113
112
|
#
|
114
|
-
#
|
115
|
-
#
|
113
|
+
# If you want to avoid data descriptors - or write data bypassing the Streamer -
|
114
|
+
# you need to know the CRC32 (as a uint) and the filesize upfront,
|
115
|
+
# before the writing of the entry body starts.
|
116
116
|
#
|
117
117
|
# ## Using the Streamer with runtime compression
|
118
118
|
#
|
@@ -132,7 +132,7 @@ module ZipKit
|
|
132
132
|
# end
|
133
133
|
# end
|
134
134
|
#
|
135
|
-
# The central directory will be written automatically at the end of the block.
|
135
|
+
# The central directory will be written automatically at the end of the `open` block.
|
136
136
|
#
|
137
137
|
# ## Using the Streamer with entries of known size and having a known CRC32 checksum
|
138
138
|
#
|
@@ -563,13 +563,12 @@ module ZipKit
|
|
563
563
|
sig { params(filename: T.untyped).returns(T.untyped) }
|
564
564
|
def remove_backslash(filename); end
|
565
565
|
|
566
|
-
# sord infer - argument name in single @param inferred as "bytes"
|
567
566
|
# Writes the given data to the output stream. Allows the object to be used as
|
568
567
|
# a target for `IO.copy_stream(from, to)`
|
569
568
|
#
|
570
|
-
# _@param_ `
|
569
|
+
# _@param_ `bytes` — the binary string to write (part of the uncompressed file)
|
571
570
|
#
|
572
|
-
# _@return_ — the number of bytes written
|
571
|
+
# _@return_ — the number of bytes written (will always be the bytesize of `bytes`)
|
573
572
|
sig { params(bytes: String).returns(Fixnum) }
|
574
573
|
def write(bytes); end
|
575
574
|
|
@@ -678,13 +677,12 @@ module ZipKit
|
|
678
677
|
sig { returns(T.untyped) }
|
679
678
|
def close; end
|
680
679
|
|
681
|
-
# sord infer - argument name in single @param inferred as "bytes"
|
682
680
|
# Writes the given data to the output stream. Allows the object to be used as
|
683
681
|
# a target for `IO.copy_stream(from, to)`
|
684
682
|
#
|
685
|
-
# _@param_ `
|
683
|
+
# _@param_ `bytes` — the binary string to write (part of the uncompressed file)
|
686
684
|
#
|
687
|
-
# _@return_ — the number of bytes written
|
685
|
+
# _@return_ — the number of bytes written (will always be the bytesize of `bytes`)
|
688
686
|
sig { params(bytes: String).returns(Fixnum) }
|
689
687
|
def write(bytes); end
|
690
688
|
end
|
@@ -748,13 +746,12 @@ module ZipKit
|
|
748
746
|
sig { returns(T::Hash[T.untyped, T.untyped]) }
|
749
747
|
def finish; end
|
750
748
|
|
751
|
-
# sord infer - argument name in single @param inferred as "bytes"
|
752
749
|
# Writes the given data to the output stream. Allows the object to be used as
|
753
750
|
# a target for `IO.copy_stream(from, to)`
|
754
751
|
#
|
755
|
-
# _@param_ `
|
752
|
+
# _@param_ `bytes` — the binary string to write (part of the uncompressed file)
|
756
753
|
#
|
757
|
-
# _@return_ — the number of bytes written
|
754
|
+
# _@return_ — the number of bytes written (will always be the bytesize of `bytes`)
|
758
755
|
sig { params(bytes: String).returns(Fixnum) }
|
759
756
|
def write(bytes); end
|
760
757
|
end
|
@@ -787,13 +784,12 @@ module ZipKit
|
|
787
784
|
sig { returns(T::Hash[T.untyped, T.untyped]) }
|
788
785
|
def finish; end
|
789
786
|
|
790
|
-
# sord infer - argument name in single @param inferred as "bytes"
|
791
787
|
# Writes the given data to the output stream. Allows the object to be used as
|
792
788
|
# a target for `IO.copy_stream(from, to)`
|
793
789
|
#
|
794
|
-
# _@param_ `
|
790
|
+
# _@param_ `bytes` — the binary string to write (part of the uncompressed file)
|
795
791
|
#
|
796
|
-
# _@return_ — the number of bytes written
|
792
|
+
# _@return_ — the number of bytes written (will always be the bytesize of `bytes`)
|
797
793
|
sig { params(bytes: String).returns(Fixnum) }
|
798
794
|
def write(bytes); end
|
799
795
|
end
|
@@ -1107,19 +1103,28 @@ end, T.untyped)
|
|
1107
1103
|
# end
|
1108
1104
|
# [200, {}, MyRackResponse.new]
|
1109
1105
|
class BlockWrite
|
1106
|
+
include ZipKit::WriteShovel
|
1107
|
+
|
1110
1108
|
# Creates a new BlockWrite.
|
1111
1109
|
#
|
1112
1110
|
# _@param_ `block` — The block that will be called when this object receives the `<<` message
|
1113
|
-
sig { params(block: T.
|
1111
|
+
sig { params(block: T.proc.params(bytes: String).void).void }
|
1114
1112
|
def initialize(&block); end
|
1115
1113
|
|
1116
1114
|
# Sends a string through to the block stored in the BlockWrite.
|
1117
1115
|
#
|
1118
1116
|
# _@param_ `buf` — the string to write. Note that a zero-length String will not be forwarded to the block, as it has special meaning when used with chunked encoding (it indicates the end of the stream).
|
1119
|
-
|
1120
|
-
# _@return_ — self
|
1121
|
-
sig { params(buf: String).returns(T.untyped) }
|
1117
|
+
sig { params(buf: String).returns(ZipKit::BlockWrite) }
|
1122
1118
|
def <<(buf); end
|
1119
|
+
|
1120
|
+
# Writes the given data to the output stream. Allows the object to be used as
|
1121
|
+
# a target for `IO.copy_stream(from, to)`
|
1122
|
+
#
|
1123
|
+
# _@param_ `bytes` — the binary string to write (part of the uncompressed file)
|
1124
|
+
#
|
1125
|
+
# _@return_ — the number of bytes written (will always be the bytesize of `bytes`)
|
1126
|
+
sig { params(bytes: String).returns(Fixnum) }
|
1127
|
+
def write(bytes); end
|
1123
1128
|
end
|
1124
1129
|
|
1125
1130
|
# A very barebones ZIP file reader. Is made for maximum interoperability, but at the same
|
@@ -1657,13 +1662,12 @@ end, T.untyped)
|
|
1657
1662
|
sig { params(crc32: Fixnum, blob_size: Fixnum).returns(Fixnum) }
|
1658
1663
|
def append(crc32, blob_size); end
|
1659
1664
|
|
1660
|
-
# sord infer - argument name in single @param inferred as "bytes"
|
1661
1665
|
# Writes the given data to the output stream. Allows the object to be used as
|
1662
1666
|
# a target for `IO.copy_stream(from, to)`
|
1663
1667
|
#
|
1664
|
-
# _@param_ `
|
1668
|
+
# _@param_ `bytes` — the binary string to write (part of the uncompressed file)
|
1665
1669
|
#
|
1666
|
-
# _@return_ — the number of bytes written
|
1670
|
+
# _@return_ — the number of bytes written (will always be the bytesize of `bytes`)
|
1667
1671
|
sig { params(bytes: String).returns(Fixnum) }
|
1668
1672
|
def write(bytes); end
|
1669
1673
|
end
|
@@ -1728,13 +1732,12 @@ end, T.untyped)
|
|
1728
1732
|
# "IO-ish" things to also respond to `write`? This is what this module does.
|
1729
1733
|
# Jim would be proud. We miss you, Jim.
|
1730
1734
|
module WriteShovel
|
1731
|
-
# sord infer - argument name in single @param inferred as "bytes"
|
1732
1735
|
# Writes the given data to the output stream. Allows the object to be used as
|
1733
1736
|
# a target for `IO.copy_stream(from, to)`
|
1734
1737
|
#
|
1735
|
-
# _@param_ `
|
1738
|
+
# _@param_ `bytes` — the binary string to write (part of the uncompressed file)
|
1736
1739
|
#
|
1737
|
-
# _@return_ — the number of bytes written
|
1740
|
+
# _@return_ — the number of bytes written (will always be the bytesize of `bytes`)
|
1738
1741
|
sig { params(bytes: String).returns(Fixnum) }
|
1739
1742
|
def write(bytes); end
|
1740
1743
|
end
|
@@ -1960,13 +1963,12 @@ end, T.untyped)
|
|
1960
1963
|
sig { returns(T.untyped) }
|
1961
1964
|
def tell; end
|
1962
1965
|
|
1963
|
-
# sord infer - argument name in single @param inferred as "bytes"
|
1964
1966
|
# Writes the given data to the output stream. Allows the object to be used as
|
1965
1967
|
# a target for `IO.copy_stream(from, to)`
|
1966
1968
|
#
|
1967
|
-
# _@param_ `
|
1969
|
+
# _@param_ `bytes` — the binary string to write (part of the uncompressed file)
|
1968
1970
|
#
|
1969
|
-
# _@return_ — the number of bytes written
|
1971
|
+
# _@return_ — the number of bytes written (will always be the bytesize of `bytes`)
|
1970
1972
|
sig { params(bytes: String).returns(Fixnum) }
|
1971
1973
|
def write(bytes); end
|
1972
1974
|
end
|
@@ -1977,25 +1979,39 @@ end, T.untyped)
|
|
1977
1979
|
# gets automatically forwarded to the Rails response stream. When the output completes,
|
1978
1980
|
# the Rails response stream is going to be closed automatically.
|
1979
1981
|
#
|
1982
|
+
# Note that there is an important difference in how this method works, depending whether
|
1983
|
+
# you use it in a controller which includes `ActionController::Live` vs. one that does not.
|
1984
|
+
# With a standard `ActionController` this method will assign a response body, but streaming
|
1985
|
+
# will begin when your action method returns. With `ActionController::Live` the streaming
|
1986
|
+
# will begin immediately, before the method returns. In all other aspects the method should
|
1987
|
+
# stream correctly in both types of controllers.
|
1988
|
+
#
|
1989
|
+
# If you encounter buffering (streaming does not start for a very long time) you probably
|
1990
|
+
# have a piece of Rack middleware in your stack which buffers. Known offenders are `Rack::ContentLength`,
|
1991
|
+
# `Rack::MiniProfiler` and `Rack::ETag`. ZipKit will try to work around these but it is not
|
1992
|
+
# always possible. If you encounter buffering, examine your middleware stack and try to suss
|
1993
|
+
# out whether any middleware might be buffering. You can also try setting `use_chunked_transfer_encoding`
|
1994
|
+
# to `true` - this is not recommended but sometimes necessary, for example to bypass `Rack::ContentLength`.
|
1995
|
+
#
|
1980
1996
|
# _@param_ `filename` — name of the file for the Content-Disposition header
|
1981
1997
|
#
|
1982
1998
|
# _@param_ `type` — the content type (MIME type) of the archive being output
|
1983
1999
|
#
|
1984
2000
|
# _@param_ `use_chunked_transfer_encoding` — whether to forcibly encode output as chunked. Normally you should not need this.
|
1985
2001
|
#
|
1986
|
-
# _@param_ `
|
2002
|
+
# _@param_ `output_enumerator_options` — options that will be passed to the OutputEnumerator - these include options for the Streamer. See {ZipKit::OutputEnumerator#initialize} for the full list of options.
|
1987
2003
|
#
|
1988
|
-
# _@return_ —
|
2004
|
+
# _@return_ — always returns true
|
1989
2005
|
sig do
|
1990
2006
|
params(
|
1991
2007
|
filename: String,
|
1992
2008
|
type: String,
|
1993
2009
|
use_chunked_transfer_encoding: T::Boolean,
|
1994
|
-
|
2010
|
+
output_enumerator_options: T::Hash[T.untyped, T.untyped],
|
1995
2011
|
zip_streaming_blk: T.proc.params(the: ZipKit::Streamer).void
|
1996
|
-
).returns(
|
2012
|
+
).returns(T::Boolean)
|
1997
2013
|
end
|
1998
|
-
def zip_kit_stream(filename: "download.zip", type: "application/zip", use_chunked_transfer_encoding: false, **
|
2014
|
+
def zip_kit_stream(filename: "download.zip", type: "application/zip", use_chunked_transfer_encoding: false, **output_enumerator_options, &zip_streaming_blk); end
|
1999
2015
|
end
|
2000
2016
|
|
2001
2017
|
# The output enumerator makes it possible to "pull" from a ZipKit streamer
|
@@ -2056,15 +2072,11 @@ end, T.untyped)
|
|
2056
2072
|
# ...
|
2057
2073
|
# end
|
2058
2074
|
#
|
2059
|
-
# _@param_ `kwargs_for_new` — keyword arguments for {Streamer.new}
|
2060
|
-
#
|
2061
2075
|
# _@param_ `streamer_options` — options for Streamer, see {ZipKit::Streamer.new}
|
2062
2076
|
#
|
2063
|
-
# _@param_ `write_buffer_size` — By default all ZipKit writes are unbuffered. For output to sockets it is beneficial to bulkify those writes so that they are roughly sized to a socket buffer chunk. This object will bulkify writes for you in this way (so `each` will yield not on every call to `<<` from the Streamer but at block size boundaries or greater). Set
|
2077
|
+
# _@param_ `write_buffer_size` — By default all ZipKit writes are unbuffered. For output to sockets it is beneficial to bulkify those writes so that they are roughly sized to a socket buffer chunk. This object will bulkify writes for you in this way (so `each` will yield not on every call to `<<` from the Streamer but at block size boundaries or greater). Set the parameter to 0 for unbuffered writes.
|
2064
2078
|
#
|
2065
2079
|
# _@param_ `blk` — a block that will receive the Streamer object when executing. The block will not be executed immediately but only once `each` is called on the OutputEnumerator
|
2066
|
-
#
|
2067
|
-
# _@return_ — the enumerator you can read bytestrings of the ZIP from by calling `each`
|
2068
2080
|
sig { params(write_buffer_size: Integer, streamer_options: T::Hash[T.untyped, T.untyped], blk: T.untyped).void }
|
2069
2081
|
def initialize(write_buffer_size: DEFAULT_WRITE_BUFFER_SIZE, **streamer_options, &blk); end
|
2070
2082
|
|
@@ -2083,6 +2095,18 @@ end, T.untyped)
|
|
2083
2095
|
def each; end
|
2084
2096
|
|
2085
2097
|
# Returns a Hash of HTTP response headers you are likely to need to have your response stream correctly.
|
2098
|
+
# This is on the {ZipKit::OutputEnumerator} class since those headers are common, independent of the
|
2099
|
+
# particular response body getting served. You might want to override the headers with your particular
|
2100
|
+
# ones - for example, specific content types are needed for files which are, technically, ZIP files
|
2101
|
+
# but are of a file format built "on top" of ZIPs - such as ODTs, [pkpass files](https://developer.apple.com/documentation/walletpasses/building_a_pass)
|
2102
|
+
# and ePubs.
|
2103
|
+
sig { returns(T::Hash[T.untyped, T.untyped]) }
|
2104
|
+
def self.streaming_http_headers; end
|
2105
|
+
|
2106
|
+
# Returns a Hash of HTTP response headers for this particular response. This used to contain "Content-Length" for
|
2107
|
+
# presized responses, but is now effectively a no-op.
|
2108
|
+
#
|
2109
|
+
# _@see_ `[ZipKit::OutputEnumerator.streaming_http_headers]`
|
2086
2110
|
sig { returns(T::Hash[T.untyped, T.untyped]) }
|
2087
2111
|
def streaming_http_headers; end
|
2088
2112
|
|
data/zip_kit.gemspec
CHANGED
@@ -7,7 +7,7 @@ Gem::Specification.new do |spec|
|
|
7
7
|
spec.version = ZipKit::VERSION
|
8
8
|
spec.authors = ["Julik Tarkhanov", "Noah Berman", "Dmitry Tymchuk", "David Bosveld", "Felix Bünemann"]
|
9
9
|
spec.email = ["me@julik.nl"]
|
10
|
-
|
10
|
+
spec.license = "MIT"
|
11
11
|
spec.summary = "Stream out ZIP files from Ruby. Successor to zip_tricks."
|
12
12
|
spec.description = "Stream out ZIP files from Ruby. Successor to zip_tricks."
|
13
13
|
spec.homepage = "https://github.com/julik/zip_kit"
|
@@ -23,9 +23,12 @@ Gem::Specification.new do |spec|
|
|
23
23
|
spec.require_paths = ["lib"]
|
24
24
|
|
25
25
|
spec.add_development_dependency "bundler"
|
26
|
-
spec.add_development_dependency "rubyzip", "~> 1"
|
27
26
|
|
28
|
-
|
27
|
+
# zip_kit does not use any runtime dependencies (besides zlib). However, for testing
|
28
|
+
# things quite a few things are used - and for a good reason.
|
29
|
+
|
30
|
+
spec.add_development_dependency "rubyzip", "~> 1" # We test our output with _another_ ZIP library, which is the way to go here
|
31
|
+
spec.add_development_dependency "rack" # For tests where we spin up a server. Both for streaming out and for testing reads over HTTP
|
29
32
|
spec.add_development_dependency "rake", "~> 12.2"
|
30
33
|
spec.add_development_dependency "rspec", "~> 3"
|
31
34
|
spec.add_development_dependency "rspec-mocks", "~> 3.10", ">= 3.10.2" # ruby 3 compatibility
|
@@ -39,5 +42,6 @@ Gem::Specification.new do |spec|
|
|
39
42
|
spec.add_development_dependency "puma"
|
40
43
|
spec.add_development_dependency "actionpack", "~> 5" # For testing RailsStreaming against an actual Rails controller
|
41
44
|
spec.add_development_dependency "nokogiri", "~> 1", ">= 1.13" # Rails 5 does by mistake use an older Nokogiri otherwise
|
45
|
+
spec.add_development_dependency "sinatra"
|
42
46
|
spec.add_development_dependency "sord"
|
43
47
|
end
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: zip_kit
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 6.2.
|
4
|
+
version: 6.2.2
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Julik Tarkhanov
|
@@ -12,7 +12,7 @@ authors:
|
|
12
12
|
autorequire:
|
13
13
|
bindir: exe
|
14
14
|
cert_chain: []
|
15
|
-
date: 2024-03-
|
15
|
+
date: 2024-03-27 00:00:00.000000000 Z
|
16
16
|
dependencies:
|
17
17
|
- !ruby/object:Gem::Dependency
|
18
18
|
name: bundler
|
@@ -250,6 +250,20 @@ dependencies:
|
|
250
250
|
- - ">="
|
251
251
|
- !ruby/object:Gem::Version
|
252
252
|
version: '1.13'
|
253
|
+
- !ruby/object:Gem::Dependency
|
254
|
+
name: sinatra
|
255
|
+
requirement: !ruby/object:Gem::Requirement
|
256
|
+
requirements:
|
257
|
+
- - ">="
|
258
|
+
- !ruby/object:Gem::Version
|
259
|
+
version: '0'
|
260
|
+
type: :development
|
261
|
+
prerelease: false
|
262
|
+
version_requirements: !ruby/object:Gem::Requirement
|
263
|
+
requirements:
|
264
|
+
- - ">="
|
265
|
+
- !ruby/object:Gem::Version
|
266
|
+
version: '0'
|
253
267
|
- !ruby/object:Gem::Dependency
|
254
268
|
name: sord
|
255
269
|
requirement: !ruby/object:Gem::Requirement
|
@@ -292,6 +306,7 @@ files:
|
|
292
306
|
- examples/parallel_compression_with_block_deflate.rb
|
293
307
|
- examples/rack_application.rb
|
294
308
|
- examples/s3_upload.rb
|
309
|
+
- examples/sinatra_application.rb
|
295
310
|
- lib/zip_kit.rb
|
296
311
|
- lib/zip_kit/block_deflate.rb
|
297
312
|
- lib/zip_kit/block_write.rb
|
@@ -324,7 +339,8 @@ files:
|
|
324
339
|
- rbi/zip_kit.rbi
|
325
340
|
- zip_kit.gemspec
|
326
341
|
homepage: https://github.com/julik/zip_kit
|
327
|
-
licenses:
|
342
|
+
licenses:
|
343
|
+
- MIT
|
328
344
|
metadata:
|
329
345
|
allowed_push_host: https://rubygems.org
|
330
346
|
post_install_message:
|
@@ -342,7 +358,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
342
358
|
- !ruby/object:Gem::Version
|
343
359
|
version: '0'
|
344
360
|
requirements: []
|
345
|
-
rubygems_version: 3.
|
361
|
+
rubygems_version: 3.1.6
|
346
362
|
signing_key:
|
347
363
|
specification_version: 4
|
348
364
|
summary: Stream out ZIP files from Ruby. Successor to zip_tricks.
|