Skip to content

Commit

Permalink
Add Railtie and include RailsStreaming automatically (#10)
Browse files Browse the repository at this point in the history
since most of our users will be on Rails anyway, it seems logical to not
have them do an extra manual include just to get ZIP streaming to work
  • Loading branch information
julik authored Apr 1, 2024
1 parent 80bb743 commit 26f6f7a
Show file tree
Hide file tree
Showing 9 changed files with 75 additions and 50 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
## 6.3.0

* Include `RailsStreaming` automatically via a Railtie. It is not really necessary to force people to manage it manually.

## 6.2.2

* Make sure "zlib" gets required at the top, as it is used everywhere
Expand Down
8 changes: 4 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,13 +28,11 @@ Ruby 2.6+ syntax support is required, as well as a a working zlib (all available

## Diving in: send some large CSV reports from Rails

The easiest is to include the `ZipKit::RailsStreaming` module into your
controller. You will then have a `zip_kit_stream` method available which accepts a block:
The included `Railtie` will automatically include `ZipKit::RailsStreaming` into the
`ActionController::Base` class. You will then have a `zip_kit_stream` method available which accepts a block:

```ruby
class ZipsController < ActionController::Base
include ZipKit::RailsStreaming

def download
zip_kit_stream do |zip|
zip.write_file('report1.csv') do |sink|
Expand All @@ -53,6 +51,8 @@ class ZipsController < ActionController::Base
end
```

The block receives the `ZipKit::Streamer` object you can write your files through.

The `write_file` method will use some heuristics to determine whether your output file would benefit
from compression, and pick the appropriate storage mode for the file accordingly.

Expand Down
2 changes: 2 additions & 0 deletions lib/zip_kit.rb
Original file line number Diff line number Diff line change
Expand Up @@ -24,4 +24,6 @@ module ZipKit
autoload :WriteShovel, File.dirname(__FILE__) + "/zip_kit/write_shovel.rb"
autoload :RackChunkedBody, File.dirname(__FILE__) + "/zip_kit/rack_chunked_body.rb"
autoload :RackTempfileBody, File.dirname(__FILE__) + "/zip_kit/rack_tempfile_body.rb"

require_relative "zip_kit/railtie" if defined?(::Rails)
end
7 changes: 7 additions & 0 deletions lib/zip_kit/railtie.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# frozen_string_literal: true

class ZipKit::Railtie < ::Rails::Railtie
initializer "zip_kit.install_extensions" do |app|
ActionController::Base.include(ZipKit::RailsStreaming)
end
end
2 changes: 1 addition & 1 deletion lib/zip_kit/version.rb
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# frozen_string_literal: true

module ZipKit
VERSION = "6.2.2"
VERSION = "6.3.0"
end
8 changes: 5 additions & 3 deletions rbi/zip_kit.rbi
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@
module ZipKit
VERSION = T.let("6.2.2", T.untyped)

class Railtie < Rails::Railtie
end

# A ZIP archive contains a flat list of entries. These entries can implicitly
# create directories when the archive is expanded. For example, an entry with
# the filename of "some folder/file.docx" will make the unarchiving application
Expand Down Expand Up @@ -1976,8 +1979,7 @@ end, T.untyped)
# Should be included into a Rails controller for easy ZIP output from any action.
module RailsStreaming
# Opens a {ZipKit::Streamer} and yields it to the caller. The output of the streamer
# gets automatically forwarded to the Rails response stream. When the output completes,
# the Rails response stream is going to be closed automatically.
# will be sent through to the HTTP response body as it gets produced.
#
# Note that there is an important difference in how this method works, depending whether
# you use it in a controller which includes `ActionController::Live` vs. one that does not.
Expand Down Expand Up @@ -2008,7 +2010,7 @@ end, T.untyped)
type: String,
use_chunked_transfer_encoding: T::Boolean,
output_enumerator_options: T::Hash[T.untyped, T.untyped],
zip_streaming_blk: T.proc.params(the: ZipKit::Streamer).void
zip_streaming_blk: T.proc.params(zip: ZipKit::Streamer).void
).returns(T::Boolean)
end
def zip_kit_stream(filename: "download.zip", type: "application/zip", use_chunked_transfer_encoding: false, **output_enumerator_options, &zip_streaming_blk); end
Expand Down
1 change: 1 addition & 0 deletions spec/support/managed_tempfile.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ class ManagedTempfile < Tempfile

def initialize(*)
super
binmode
@@managed_tempfiles << self
end

Expand Down
92 changes: 50 additions & 42 deletions spec/zip_kit/rails_streaming_spec.rb
Original file line number Diff line number Diff line change
@@ -1,58 +1,66 @@
require "spec_helper"
require "action_controller"

describe ZipKit::RailsStreaming do
class FakeZipGenerator
def generate_once(streamer)
# Only allow the call to be executed once, to ensure that we run
# our ZIP generation block just once. This is to ensure Rack::ContentLength
# does not run the generation twice
raise "The ZIP has already been generated once" if @did_generate_zip
streamer.write_file("hello.txt") do |f|
f << "ßHello from Rails"
before :all do
# Defer requires of Rails components until the test runs.
# This will make other tests fail if they use Rails-specific things
# and run before this one
require "action_controller"
require "rails"
require "zip_kit/railtie"
ZipKit::Railtie.initializers.each(&:run)

class FakeZipGenerator
def generate_once(streamer)
# Only allow the call to be executed once, to ensure that we run
# our ZIP generation block just once. This is to ensure Rack::ContentLength
# does not run the generation twice
raise "The ZIP has already been generated once" if @did_generate_zip
streamer.write_file("hello.txt") do |f|
f << "ßHello from Rails"
end
@did_generate_zip = true
end
@did_generate_zip = true
end

def self.generate_reference
StringIO.new.binmode.tap do |sio|
ZipKit::Streamer.open(sio) do |streamer|
new.generate_once(streamer)
def self.generate_reference
StringIO.new.binmode.tap do |sio|
ZipKit::Streamer.open(sio) do |streamer|
new.generate_once(streamer)
end
sio.rewind
end
sio.rewind
end
end
end

class FakeController < ActionController::Base
# Make sure both Rack middlewares which are known to cause trouble
# are used in this controller, so that we can ensure they get bypassed.
# Use them in the same order Rails inserts them.
middleware.use Rack::Sendfile
middleware.use Rack::ETag
middleware.use Rack::Deflater
middleware.use Rack::ContentLength # This does not get injected by Rails
middleware.use Rack::TempfileReaper

include ZipKit::RailsStreaming
def stream_zip
generator = FakeZipGenerator.new
zip_kit_stream(auto_rename_duplicate_filenames: true) do |z|
generator.generate_once(z)
class FakeController < ActionController::Base
# Make sure both Rack middlewares which are known to cause trouble
# are used in this controller, so that we can ensure they get bypassed.
# Use them in the same order Rails inserts them.
middleware.use Rack::Sendfile
middleware.use Rack::ETag
middleware.use Rack::Deflater
middleware.use Rack::ContentLength # This does not get injected by Rails
middleware.use Rack::TempfileReaper

def stream_zip
generator = FakeZipGenerator.new
zip_kit_stream(auto_rename_duplicate_filenames: true) do |z|
generator.generate_once(z)
end
end
end

def stream_zip_with_forced_chunking
generator = FakeZipGenerator.new
zip_kit_stream(auto_rename_duplicate_filenames: true, use_chunked_transfer_encoding: true) do |z|
generator.generate_once(z)
def stream_zip_with_forced_chunking
generator = FakeZipGenerator.new
zip_kit_stream(auto_rename_duplicate_filenames: true, use_chunked_transfer_encoding: true) do |z|
generator.generate_once(z)
end
end
end

def stream_zip_with_custom_content_type
generator = FakeZipGenerator.new
zip_kit_stream(type: "application/epub+zip") do |z|
generator.generate_once(z)
def stream_zip_with_custom_content_type
generator = FakeZipGenerator.new
zip_kit_stream(type: "application/epub+zip") do |z|
generator.generate_once(z)
end
end
end
end
Expand Down
1 change: 1 addition & 0 deletions zip_kit.gemspec
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ Gem::Specification.new do |spec|
spec.add_development_dependency "standard", "1.28.5" # Very specific version of standard for 2.6 with _known_ settings
spec.add_development_dependency "magic_frozen_string_literal"
spec.add_development_dependency "puma"
spec.add_development_dependency "rails", "~> 5" # For testing RailsStreaming against an actual Rails controller
spec.add_development_dependency "actionpack", "~> 5" # For testing RailsStreaming against an actual Rails controller
spec.add_development_dependency "nokogiri", "~> 1", ">= 1.13" # Rails 5 does by mistake use an older Nokogiri otherwise
spec.add_development_dependency "sinatra"
Expand Down

0 comments on commit 26f6f7a

Please sign in to comment.