From 26f6f7a049b9720567a92dc9eb42ae9be64ac198 Mon Sep 17 00:00:00 2001 From: Julik Tarkhanov Date: Mon, 1 Apr 2024 12:06:13 +0200 Subject: [PATCH] Add Railtie and include RailsStreaming automatically (#10) 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 --- CHANGELOG.md | 4 ++ README.md | 8 +-- lib/zip_kit.rb | 2 + lib/zip_kit/railtie.rb | 7 +++ lib/zip_kit/version.rb | 2 +- rbi/zip_kit.rbi | 8 ++- spec/support/managed_tempfile.rb | 1 + spec/zip_kit/rails_streaming_spec.rb | 92 +++++++++++++++------------- zip_kit.gemspec | 1 + 9 files changed, 75 insertions(+), 50 deletions(-) create mode 100644 lib/zip_kit/railtie.rb diff --git a/CHANGELOG.md b/CHANGELOG.md index 9c91cf6..f52c89c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/README.md b/README.md index c5e13de..ecc6ea3 100644 --- a/README.md +++ b/README.md @@ -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| @@ -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. diff --git a/lib/zip_kit.rb b/lib/zip_kit.rb index d74cb95..9c50b8d 100644 --- a/lib/zip_kit.rb +++ b/lib/zip_kit.rb @@ -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 diff --git a/lib/zip_kit/railtie.rb b/lib/zip_kit/railtie.rb new file mode 100644 index 0000000..bc82a38 --- /dev/null +++ b/lib/zip_kit/railtie.rb @@ -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 diff --git a/lib/zip_kit/version.rb b/lib/zip_kit/version.rb index 00f2784..dfa2df7 100644 --- a/lib/zip_kit/version.rb +++ b/lib/zip_kit/version.rb @@ -1,5 +1,5 @@ # frozen_string_literal: true module ZipKit - VERSION = "6.2.2" + VERSION = "6.3.0" end diff --git a/rbi/zip_kit.rbi b/rbi/zip_kit.rbi index 0f8db32..9144236 100644 --- a/rbi/zip_kit.rbi +++ b/rbi/zip_kit.rbi @@ -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 @@ -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. @@ -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 diff --git a/spec/support/managed_tempfile.rb b/spec/support/managed_tempfile.rb index 8d72bfd..8251926 100644 --- a/spec/support/managed_tempfile.rb +++ b/spec/support/managed_tempfile.rb @@ -3,6 +3,7 @@ class ManagedTempfile < Tempfile def initialize(*) super + binmode @@managed_tempfiles << self end diff --git a/spec/zip_kit/rails_streaming_spec.rb b/spec/zip_kit/rails_streaming_spec.rb index 69a747d..ef0549e 100644 --- a/spec/zip_kit/rails_streaming_spec.rb +++ b/spec/zip_kit/rails_streaming_spec.rb @@ -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 diff --git a/zip_kit.gemspec b/zip_kit.gemspec index 7c3053e..1dc47b0 100644 --- a/zip_kit.gemspec +++ b/zip_kit.gemspec @@ -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"