zip_tricks 4.6.0 → 4.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.travis.yml +1 -2
- data/CHANGELOG.md +6 -0
- data/README.md +1 -0
- data/Rakefile +3 -2
- data/examples/deferred_write.rb +58 -0
- data/examples/rack_application.rb +1 -1
- data/lib/zip_tricks/block_write.rb +2 -10
- data/lib/zip_tricks/output_enumerator.rb +45 -0
- data/lib/zip_tricks/rack_body.rb +3 -41
- data/lib/zip_tricks/rails_streaming.rb +3 -0
- data/lib/zip_tricks/streamer.rb +29 -0
- data/lib/zip_tricks/version.rb +1 -1
- data/zip_tricks.gemspec +1 -1
- metadata +6 -4
    
        checksums.yaml
    CHANGED
    
    | @@ -1,7 +1,7 @@ | |
| 1 1 | 
             
            ---
         | 
| 2 2 | 
             
            SHA1:
         | 
| 3 | 
            -
              metadata.gz:  | 
| 4 | 
            -
              data.tar.gz:  | 
| 3 | 
            +
              metadata.gz: ec7428d7634361abe1384db4d39a8c7751dd3cdc
         | 
| 4 | 
            +
              data.tar.gz: 3a4b3327e9edc0cc4b84e8ea8d064682104603d1
         | 
| 5 5 | 
             
            SHA512:
         | 
| 6 | 
            -
              metadata.gz:  | 
| 7 | 
            -
              data.tar.gz:  | 
| 6 | 
            +
              metadata.gz: a9740445e2b7b646a5e7c5670edf61450543fd33f6e687d2d6adfc07a2a8097a244b9ecc9a8122f80eade94e534193ccdda34ad20ea97289b73f741f7e464a20
         | 
| 7 | 
            +
              data.tar.gz: 649ffbc1803e3022092bf7da01245e13a838b0e27bf85f2be40e0cc1792249c4207936f1aa83b33eba74ec1f646962c6eb1cf11ca7677f48978c8721fcdb2c87
         | 
    
        data/.travis.yml
    CHANGED
    
    
    
        data/CHANGELOG.md
    CHANGED
    
    | @@ -1,3 +1,9 @@ | |
| 1 | 
            +
            ## 4.7.0
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            * Replace `RackBody` with `OutputEnumerator` since we want to provide a generic way of deferring ZIP output, also when using enumerators.
         | 
| 4 | 
            +
            * Remove `RackBody#close` since we got nothing to close 🤷♂️
         | 
| 5 | 
            +
            * Hint nginx that response buffering should be disabled when using Rails zip streaming
         | 
| 6 | 
            +
             | 
| 1 7 | 
             
            ## 4.6.0
         | 
| 2 8 |  | 
| 3 9 | 
             
            * Add `mtime:` option to all Streamer methods for adding files and directories, to permit setting modification time per-entry
         | 
    
        data/README.md
    CHANGED
    
    | @@ -1,6 +1,7 @@ | |
| 1 1 | 
             
            # zip_tricks
         | 
| 2 2 |  | 
| 3 3 | 
             
            [](https://travis-ci.org/WeTransfer/zip_tricks)
         | 
| 4 | 
            +
            [](https://badge.fury.io/rb/zip_tricks)
         | 
| 4 5 |  | 
| 5 6 | 
             
            Allows streaming, non-rewinding ZIP file output from Ruby.
         | 
| 6 7 |  | 
    
        data/Rakefile
    CHANGED
    
    
| @@ -0,0 +1,58 @@ | |
| 1 | 
            +
            # frozen_string_literal: true
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            require_relative '../lib/zip_tricks'
         | 
| 4 | 
            +
             | 
| 5 | 
            +
            # Using deferred writes (when you want to "pull" from a Streamer)
         | 
| 6 | 
            +
            # is also possible with ZipTricks.
         | 
| 7 | 
            +
            #
         | 
| 8 | 
            +
            # The OutputEnumerator class instead of Streamer is very useful for this
         | 
| 9 | 
            +
            # particular purpose. It does not start the archiving immediately,
         | 
| 10 | 
            +
            # but waits instead until you start pulling data out of it.
         | 
| 11 | 
            +
            #
         | 
| 12 | 
            +
            # Let's make a OutputEnumerator that writes a few files with random content. Note that when you create
         | 
| 13 | 
            +
            # that body it does not immediately write the ZIP:
         | 
| 14 | 
            +
            iterable = ZipTricks::Streamer.output_enum do |zip|
         | 
| 15 | 
            +
              (1..5).each do |i|
         | 
| 16 | 
            +
                zip.write_stored_file('random_%d04d.bin' % i) do |sink|
         | 
| 17 | 
            +
                  warn "Starting on file #{i}...\n"
         | 
| 18 | 
            +
                  sink << Random.new.bytes(1024)
         | 
| 19 | 
            +
                end
         | 
| 20 | 
            +
              end
         | 
| 21 | 
            +
            end
         | 
| 22 | 
            +
             | 
| 23 | 
            +
            warn "\n\nOutput using #each"
         | 
| 24 | 
            +
             | 
| 25 | 
            +
            # Now we can treat the iterable as any Ruby enumerable object, since
         | 
| 26 | 
            +
            # it supports #each yielding every binary string output by the Streamer.
         | 
| 27 | 
            +
            # Only when we start using each() will the ZIP start generating. Just using
         | 
| 28 | 
            +
            # each() like we do here runs the archiving procedure to completion. See how
         | 
| 29 | 
            +
            # the output of the block within OutputEnumerator is interspersed with the stuff
         | 
| 30 | 
            +
            # being yielded to each():
         | 
| 31 | 
            +
            iterable.each do |_binary_string|
         | 
| 32 | 
            +
              $stderr << '.'
         | 
| 33 | 
            +
            end
         | 
| 34 | 
            +
             | 
| 35 | 
            +
            warn "\n\nOutput Enumerator returned from #each"
         | 
| 36 | 
            +
             | 
| 37 | 
            +
            # We now have output the entire archive, so using each() again
         | 
| 38 | 
            +
            # will restart the block we gave it. For example, we can user
         | 
| 39 | 
            +
            # an Enumerator - via enum_for - to "take" chunks of output when
         | 
| 40 | 
            +
            # we find necessary:
         | 
| 41 | 
            +
            enum = iterable.each
         | 
| 42 | 
            +
            15.times do
         | 
| 43 | 
            +
              _bin_str = enum.next  # Obtain the subsequent chunk of the ZIP
         | 
| 44 | 
            +
              $stderr << '*'
         | 
| 45 | 
            +
            end
         | 
| 46 | 
            +
             | 
| 47 | 
            +
            # ... or a Fiber
         | 
| 48 | 
            +
             | 
| 49 | 
            +
            warn "\n\nOutput using a Fiber"
         | 
| 50 | 
            +
            fib = Fiber.new do
         | 
| 51 | 
            +
              iterable.each do |binary_string|
         | 
| 52 | 
            +
                $stderr << '•'
         | 
| 53 | 
            +
                _next_iteration = Fiber.yield(binary_string)
         | 
| 54 | 
            +
              end
         | 
| 55 | 
            +
            end
         | 
| 56 | 
            +
            15.times do
         | 
| 57 | 
            +
              fib.resume # Process the subsequent chunk of the ZIP
         | 
| 58 | 
            +
            end
         | 
| @@ -36,7 +36,7 @@ class ZipDownload | |
| 36 36 |  | 
| 37 37 | 
             
                # Create a suitable Rack response body, that will support each(),
         | 
| 38 38 | 
             
                # close() and all the other methods. We can then return it up the stack.
         | 
| 39 | 
            -
                zip_response_body = ZipTricks:: | 
| 39 | 
            +
                zip_response_body = ZipTricks::Streamer.output_enum do |zip|
         | 
| 40 40 | 
             
                  begin
         | 
| 41 41 | 
             
                    # We are adding only one file to the ZIP here, but you could do that
         | 
| 42 42 | 
             
                    # with an arbitrary number of files of course.
         | 
| @@ -20,7 +20,8 @@ class ZipTricks::BlockWrite | |
| 20 20 | 
             
              # Every time this object gets written to, call the Rack body each() block
         | 
| 21 21 | 
             
              # with the bytes given instead.
         | 
| 22 22 | 
             
              def <<(buf)
         | 
| 23 | 
            -
                 | 
| 23 | 
            +
                # Zero-size output has a special meaning  when using chunked encoding
         | 
| 24 | 
            +
                return if buf.nil? || buf.bytesize.zero?
         | 
| 24 25 |  | 
| 25 26 | 
             
                # Ensure we ALWAYS write in binary encoding.
         | 
| 26 27 | 
             
                encoded =
         | 
| @@ -35,16 +36,7 @@ class ZipTricks::BlockWrite | |
| 35 36 | 
             
                    buf
         | 
| 36 37 | 
             
                  end
         | 
| 37 38 |  | 
| 38 | 
            -
                #  buf.dup.force_encoding(Encoding::BINARY)
         | 
| 39 | 
            -
                # Zero-size output has a special meaning  when using chunked encoding
         | 
| 40 | 
            -
                return if encoded.bytesize.zero?
         | 
| 41 | 
            -
             | 
| 42 39 | 
             
                @block.call(encoded)
         | 
| 43 40 | 
             
                self
         | 
| 44 41 | 
             
              end
         | 
| 45 | 
            -
             | 
| 46 | 
            -
              # Does nothing
         | 
| 47 | 
            -
              def close
         | 
| 48 | 
            -
                nil
         | 
| 49 | 
            -
              end
         | 
| 50 42 | 
             
            end
         | 
| @@ -0,0 +1,45 @@ | |
| 1 | 
            +
            # frozen_string_literal: true
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            # Can be used as a Rack response body directly. Will yield
         | 
| 4 | 
            +
            # a {ZipTricks::Streamer} for adding entries to the archive and writing
         | 
| 5 | 
            +
            # zip entry bodies.
         | 
| 6 | 
            +
            class ZipTricks::OutputEnumerator
         | 
| 7 | 
            +
              # Prepares a new Rack response body with a Zip output stream.
         | 
| 8 | 
            +
              # The block given to the constructor will be called when the response
         | 
| 9 | 
            +
              # body will be read by the webserver, and will receive a {ZipTricks::Streamer}
         | 
| 10 | 
            +
              # as it's block argument. You can then add entries to the Streamer as usual.
         | 
| 11 | 
            +
              # The archive will be automatically closed at the end of the block.
         | 
| 12 | 
            +
              #
         | 
| 13 | 
            +
              #     # Precompute the Content-Length ahead of time
         | 
| 14 | 
            +
              #     content_length = ZipTricks::SizeEstimator.estimate do | estimator |
         | 
| 15 | 
            +
              #       estimator.add_stored_entry(filename: 'large.tif', size: 1289894)
         | 
| 16 | 
            +
              #     end
         | 
| 17 | 
            +
              #
         | 
| 18 | 
            +
              #     # Prepare the response body.
         | 
| 19 | 
            +
              #     # The block will only be called when the
         | 
| 20 | 
            +
              #     # response starts to be written.
         | 
| 21 | 
            +
              #     body = ZipTricks::OutputEnumerator.new do | streamer |
         | 
| 22 | 
            +
              #       streamer.add_stored_entry(filename: 'large.tif', size: 1289894, crc32: 198210)
         | 
| 23 | 
            +
              #       streamer << large_file.read(1024*1024) until large_file.eof?
         | 
| 24 | 
            +
              #       ...
         | 
| 25 | 
            +
              #     end
         | 
| 26 | 
            +
              #
         | 
| 27 | 
            +
              #     return [200, {'Content-Type' => 'binary/octet-stream',
         | 
| 28 | 
            +
              #     'Content-Length' => content_length.to_s}, body]
         | 
| 29 | 
            +
              def initialize(**streamer_options, &blk)
         | 
| 30 | 
            +
                @streamer_options = streamer_options.to_h
         | 
| 31 | 
            +
                @archiving_block = blk
         | 
| 32 | 
            +
              end
         | 
| 33 | 
            +
             | 
| 34 | 
            +
              # Executes the block given to the constructor with a {ZipTricks::Streamer}
         | 
| 35 | 
            +
              # and passes each written chunk to the block given to the method. This allows one
         | 
| 36 | 
            +
              # to "take" output of the ZIP piecewise.
         | 
| 37 | 
            +
              def each
         | 
| 38 | 
            +
                if block_given?
         | 
| 39 | 
            +
                  block_write = ZipTricks::BlockWrite.new { |chunk| yield(chunk) }
         | 
| 40 | 
            +
                  ZipTricks::Streamer.open(block_write, **@streamer_options, &@archiving_block)
         | 
| 41 | 
            +
                else
         | 
| 42 | 
            +
                  enum_for(:each)
         | 
| 43 | 
            +
                end
         | 
| 44 | 
            +
              end
         | 
| 45 | 
            +
            end
         | 
    
        data/lib/zip_tricks/rack_body.rb
    CHANGED
    
    | @@ -1,44 +1,6 @@ | |
| 1 1 | 
             
            # frozen_string_literal: true
         | 
| 2 2 |  | 
| 3 | 
            -
            #  | 
| 4 | 
            -
            #  | 
| 5 | 
            -
             | 
| 6 | 
            -
            class ZipTricks::RackBody
         | 
| 7 | 
            -
              # Prepares a new Rack response body with a Zip output stream.
         | 
| 8 | 
            -
              # The block given to the constructor will be called when the response
         | 
| 9 | 
            -
              # body will be read by the webserver, and will receive a {ZipTricks::Streamer}
         | 
| 10 | 
            -
              # as it's block argument. You can then add entries to the Streamer as usual.
         | 
| 11 | 
            -
              # The archive will be automatically closed at the end of the block.
         | 
| 12 | 
            -
              #
         | 
| 13 | 
            -
              #     # Precompute the Content-Length ahead of time
         | 
| 14 | 
            -
              #     content_length = ZipTricks::SizeEstimator.estimate do | estimator |
         | 
| 15 | 
            -
              #       estimator.add_stored_entry(filename: 'large.tif', size: 1289894)
         | 
| 16 | 
            -
              #     end
         | 
| 17 | 
            -
              #
         | 
| 18 | 
            -
              #     # Prepare the response body. The block will only be called when the
         | 
| 19 | 
            -
              #       response starts to be written.
         | 
| 20 | 
            -
              #     body = ZipTricks::RackBody.new do | streamer |
         | 
| 21 | 
            -
              #       streamer.add_stored_entry(filename: 'large.tif', size: 1289894, crc32: 198210)
         | 
| 22 | 
            -
              #       streamer << large_file.read(1024*1024) until large_file.eof?
         | 
| 23 | 
            -
              #       ...
         | 
| 24 | 
            -
              #     end
         | 
| 25 | 
            -
              #
         | 
| 26 | 
            -
              #     return [200, {'Content-Type' => 'binary/octet-stream',
         | 
| 27 | 
            -
              #     'Content-Length' => content_length.to_s}, body]
         | 
| 28 | 
            -
              def initialize(&blk)
         | 
| 29 | 
            -
                @archiving_block = blk
         | 
| 30 | 
            -
              end
         | 
| 31 | 
            -
             | 
| 32 | 
            -
              # Connects a {ZipTricks::BlockWrite} to the Rack webserver output,
         | 
| 33 | 
            -
              # and calls the proc given to the constructor with a {ZipTricks::Streamer}
         | 
| 34 | 
            -
              # for archive writing.
         | 
| 35 | 
            -
              def each(&body_chunk_block)
         | 
| 36 | 
            -
                fake_io = ZipTricks::BlockWrite.new(&body_chunk_block)
         | 
| 37 | 
            -
                ZipTricks::Streamer.open(fake_io, &@archiving_block)
         | 
| 38 | 
            -
              end
         | 
| 39 | 
            -
             | 
| 40 | 
            -
              # Does nothing because nothing has to be deallocated or canceled
         | 
| 41 | 
            -
              # even if the zip output is incomplete. The archive gets closed
         | 
| 42 | 
            -
              # automatically as part of {ZipTricks::Streamer.open}
         | 
| 43 | 
            -
              def close; end
         | 
| 3 | 
            +
            # RackBody is actually just another use of the OutputEnumerator, since a Rack body
         | 
| 4 | 
            +
            # object must support `#each` yielding successive binary strings.
         | 
| 5 | 
            +
            class ZipTricks::RackBody < ZipTricks::OutputEnumerator
         | 
| 44 6 | 
             
            end
         | 
| @@ -8,7 +8,10 @@ module ZipTricks::RailsStreaming | |
| 8 8 | 
             
              # the Rails response stream is going to be closed automatically.
         | 
| 9 9 | 
             
              # @yield [Streamer] the streamer that can be written to
         | 
| 10 10 | 
             
              def zip_tricks_stream
         | 
| 11 | 
            +
                # Set a reasonable content type
         | 
| 11 12 | 
             
                response.headers['Content-Type'] = 'application/zip'
         | 
| 13 | 
            +
                # Make sure nginx buffering is suppressed - see https://github.com/WeTransfer/zip_tricks/issues/48
         | 
| 14 | 
            +
                response.headers['X-Accel-Buffering'] = 'no'
         | 
| 12 15 | 
             
                # Create a wrapper for the write call that quacks like something you
         | 
| 13 16 | 
             
                # can << to, used by ZipTricks
         | 
| 14 17 | 
             
                w = ZipTricks::BlockWrite.new { |chunk| response.stream.write(chunk) }
         | 
    
        data/lib/zip_tricks/streamer.rb
    CHANGED
    
    | @@ -99,6 +99,7 @@ class ZipTricks::Streamer | |
| 99 99 | 
             
              # directory of the archive to the output.
         | 
| 100 100 | 
             
              #
         | 
| 101 101 | 
             
              # @param stream [IO] the destination IO for the ZIP (should respond to `tell` and `<<`)
         | 
| 102 | 
            +
              # @param kwargs_for_new [Hash] keyword arguments for {Streamer.new}
         | 
| 102 103 | 
             
              # @yield [Streamer] the streamer that can be written to
         | 
| 103 104 | 
             
              def self.open(stream, **kwargs_for_new)
         | 
| 104 105 | 
             
                archive = new(stream, **kwargs_for_new)
         | 
| @@ -106,6 +107,34 @@ class ZipTricks::Streamer | |
| 106 107 | 
             
                archive.close
         | 
| 107 108 | 
             
              end
         | 
| 108 109 |  | 
| 110 | 
            +
              # Creates a new Streamer that writes to a buffer. The buffer can be read from using `each`,
         | 
| 111 | 
            +
              # and the creation of the ZIP is in lockstep with the caller calling `each` on the returned
         | 
| 112 | 
            +
              # output enumerator object. This can be used when the calling program wants to stream the
         | 
| 113 | 
            +
              # output of the ZIP archive and throttle that output, or split it into chunks, or use it
         | 
| 114 | 
            +
              # as a generator.
         | 
| 115 | 
            +
              #
         | 
| 116 | 
            +
              # For example:
         | 
| 117 | 
            +
              #
         | 
| 118 | 
            +
              #     # The block given to {output_enum} won't be executed immediately - rather it
         | 
| 119 | 
            +
              #     # will only start to execute when the caller starts to read from the output
         | 
| 120 | 
            +
              #     # by calling `each`
         | 
| 121 | 
            +
              #     body = ZipTricks::Streamer.output_enum(writer: CustomWriter) do |zip|
         | 
| 122 | 
            +
              #       streamer.add_stored_entry(filename: 'large.tif', size: 1289894, crc32: 198210)
         | 
| 123 | 
            +
              #       streamer << large_file.read(1024*1024) until large_file.eof?
         | 
| 124 | 
            +
              #       ...
         | 
| 125 | 
            +
              #     end
         | 
| 126 | 
            +
              #
         | 
| 127 | 
            +
              #     body.each do |bin_string|
         | 
| 128 | 
            +
              #       # Send the output somewhere, buffer it in a file etc.
         | 
| 129 | 
            +
              #       ...
         | 
| 130 | 
            +
              #     end
         | 
| 131 | 
            +
              #
         | 
| 132 | 
            +
              # @param kwargs_for_new [Hash] keyword arguments for {Streamer.new}
         | 
| 133 | 
            +
              # @return [Enumerator] the enumerator you can read bytestrings of the ZIP from using `each`
         | 
| 134 | 
            +
              def self.output_enum(**kwargs_for_new, &zip_streamer_block)
         | 
| 135 | 
            +
                ZipTricks::OutputEnumerator.new(**kwargs_for_new, &zip_streamer_block)
         | 
| 136 | 
            +
              end
         | 
| 137 | 
            +
             | 
| 109 138 | 
             
              # Creates a new Streamer on top of the given IO-ish object.
         | 
| 110 139 | 
             
              #
         | 
| 111 140 | 
             
              # @param stream[IO] the destination IO for the ZIP. Anything that responds to `<<` can be used.
         | 
    
        data/lib/zip_tricks/version.rb
    CHANGED
    
    
    
        data/zip_tricks.gemspec
    CHANGED
    
    | @@ -41,5 +41,5 @@ Gem::Specification.new do |spec| | |
| 41 41 | 
             
              spec.add_development_dependency 'coderay'
         | 
| 42 42 | 
             
              spec.add_development_dependency 'benchmark-ips'
         | 
| 43 43 | 
             
              spec.add_development_dependency 'yard', '~> 0.9'
         | 
| 44 | 
            -
              spec.add_development_dependency 'wetransfer_style', '0. | 
| 44 | 
            +
              spec.add_development_dependency 'wetransfer_style', '0.6.0'
         | 
| 45 45 | 
             
            end
         | 
    
        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. | 
| 4 | 
            +
              version: 4.7.0
         | 
| 5 5 | 
             
            platform: ruby
         | 
| 6 6 | 
             
            authors:
         | 
| 7 7 | 
             
            - Julik Tarkhanov
         | 
| 8 8 | 
             
            autorequire: 
         | 
| 9 9 | 
             
            bindir: exe
         | 
| 10 10 | 
             
            cert_chain: []
         | 
| 11 | 
            -
            date: 2018- | 
| 11 | 
            +
            date: 2018-08-31 00:00:00.000000000 Z
         | 
| 12 12 | 
             
            dependencies:
         | 
| 13 13 | 
             
            - !ruby/object:Gem::Dependency
         | 
| 14 14 | 
             
              name: bundler
         | 
| @@ -170,14 +170,14 @@ dependencies: | |
| 170 170 | 
             
                requirements:
         | 
| 171 171 | 
             
                - - '='
         | 
| 172 172 | 
             
                  - !ruby/object:Gem::Version
         | 
| 173 | 
            -
                    version: 0. | 
| 173 | 
            +
                    version: 0.6.0
         | 
| 174 174 | 
             
              type: :development
         | 
| 175 175 | 
             
              prerelease: false
         | 
| 176 176 | 
             
              version_requirements: !ruby/object:Gem::Requirement
         | 
| 177 177 | 
             
                requirements:
         | 
| 178 178 | 
             
                - - '='
         | 
| 179 179 | 
             
                  - !ruby/object:Gem::Version
         | 
| 180 | 
            -
                    version: 0. | 
| 180 | 
            +
                    version: 0.6.0
         | 
| 181 181 | 
             
            description: Stream out ZIP files from Ruby
         | 
| 182 182 | 
             
            email:
         | 
| 183 183 | 
             
            - me@julik.nl
         | 
| @@ -203,6 +203,7 @@ files: | |
| 203 203 | 
             
            - bench/buffered_crc32_bench.rb
         | 
| 204 204 | 
             
            - examples/archive_size_estimate.rb
         | 
| 205 205 | 
             
            - examples/config.ru
         | 
| 206 | 
            +
            - examples/deferred_write.rb
         | 
| 206 207 | 
             
            - examples/parallel_compression_with_block_deflate.rb
         | 
| 207 208 | 
             
            - examples/rack_application.rb
         | 
| 208 209 | 
             
            - lib/zip_tricks.rb
         | 
| @@ -212,6 +213,7 @@ files: | |
| 212 213 | 
             
            - lib/zip_tricks/file_reader/inflating_reader.rb
         | 
| 213 214 | 
             
            - lib/zip_tricks/file_reader/stored_reader.rb
         | 
| 214 215 | 
             
            - lib/zip_tricks/null_writer.rb
         | 
| 216 | 
            +
            - lib/zip_tricks/output_enumerator.rb
         | 
| 215 217 | 
             
            - lib/zip_tricks/rack_body.rb
         | 
| 216 218 | 
             
            - lib/zip_tricks/rails_streaming.rb
         | 
| 217 219 | 
             
            - lib/zip_tricks/remote_io.rb
         |