├── .github └── workflows │ └── ci.yml ├── .gitignore ├── Gemfile ├── LICENSE ├── README.md ├── Rakefile ├── lib ├── zipline.rb └── zipline │ ├── version.rb │ └── zip_handler.rb ├── spec ├── fakefile.txt ├── lib │ └── zipline │ │ ├── zip_handler_spec.rb │ │ └── zipline_spec.rb └── spec_helper.rb └── zipline.gemspec /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: Tests 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | test: 7 | name: Test (${{ matrix.ruby-version }}) 8 | runs-on: ubuntu-latest 9 | 10 | strategy: 11 | fail-fast: false 12 | matrix: 13 | ruby-version: 14 | - '3.1' 15 | - '3.0' 16 | - '2.7' 17 | 18 | steps: 19 | - uses: actions/checkout@v2 20 | - name: Set up Ruby ${{ matrix.ruby-version }} 21 | uses: ruby/setup-ruby@v1 22 | with: 23 | ruby-version: ${{ matrix.ruby-version }} 24 | bundler-cache: true # 'bundle install' and cache 25 | - name: Run tests 26 | run: bundle exec rake 27 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.gem 2 | *.rbc 3 | .bundle 4 | .config 5 | .yardoc 6 | Gemfile.lock 7 | InstalledFiles 8 | _yardoc 9 | coverage 10 | doc/ 11 | lib/bundler/man 12 | pkg 13 | rdoc 14 | spec/reports 15 | test/tmp 16 | test/version_tmp 17 | tmp 18 | .ruby-version 19 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | # Specify your gem's dependencies in zipline.gemspec 4 | gemspec 5 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2012 Ram Dobson 2 | 3 | MIT License 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining 6 | a copy of this software and associated documentation files (the 7 | "Software"), to deal in the Software without restriction, including 8 | without limitation the rights to use, copy, modify, merge, publish, 9 | distribute, sublicense, and/or sell copies of the Software, and to 10 | permit persons to whom the Software is furnished to do so, subject to 11 | the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be 14 | included in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 19 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 20 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 22 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Zipline 2 | [![Tests](https://github.com/fringd/zipline/actions/workflows/ci.yml/badge.svg)](https://github.com/fringd/zipline/actions/workflows/ci.yml) 3 | [![Gem Version](https://badge.fury.io/rb/zipline.svg)](https://badge.fury.io/rb/zipline) 4 | 5 | A gem to stream dynamically generated zip files from a rails application. Unlike other solutions that generate zips for user download, zipline does not wait for the entire zip file to be created (or even for the entire input file in the cloud to be downloaded) before it begins sending the zip file to the user. It does this by never seeking backwards during zip creation, and streaming the zip file over http as it is constructed. The advantages of this are: 6 | 7 | - Removes need for large disk space or memory allocation to generate zips, even huge zips. So it works on Heroku. 8 | - The user begins downloading immediately, which decreaceses latency, download time, and timeouts on Heroku. 9 | 10 | Zipline now depends on [zip_kit](https://github.com/julik/zip_kit), and you might want to just use that directly if you have more advanced use cases. 11 | 12 | ## Installation 13 | 14 | Add this line to your application's Gemfile: 15 | 16 | gem 'zipline' 17 | 18 | And then execute: 19 | 20 | $ bundle 21 | 22 | ## Usage 23 | 24 | Set up some models with [ActiveStorage](http://edgeguides.rubyonrails.org/active_storage_overview.html) 25 | [carrierwave](https://github.com/jnicklas/carrierwave), [paperclip](https://github.com/thoughtbot/paperclip), or 26 | [shrine](https://github.com/janko-m/shrine). Right now only plain file storage and S3 are supported in the case of 27 | [carrierwave](https://github.com/jnicklas/carrierwave) and only plain file storage and S3 are supported in the case of 28 | [paperclip](https://github.com/thoughtbot/paperclip). [Mutiple file storages](http://shrinerb.com/#external) are 29 | supported with [shrine](https://github.com/janko-m/shrine). 30 | 31 | You'll need to be using puma or some other server that supports streaming output. 32 | 33 | ```Ruby 34 | class MyController < ApplicationController 35 | # enable zipline 36 | include Zipline 37 | 38 | def index 39 | users = User.all 40 | # you can replace user.avatar with any stream or any object that 41 | # responds to :url, :path or :file. 42 | # :modification_time is an optional third argument you can use. 43 | files = users.map{ |user| [user.avatar, "#{user.username}.png", modification_time: 1.day.ago] } 44 | 45 | # we can force duplicate file names to be renamed, or raise an error 46 | # we can also pass in our own writer if required to conform with the delegated [ZipKit::Streamer object](https://github.com/julik/zip_kit/blob/main/lib/zip_kit/streamer.rb#L147) object. 47 | zipline(files, 'avatars.zip', auto_rename_duplicate_filenames: true) 48 | end 49 | end 50 | ``` 51 | 52 | ### ActiveStorage 53 | 54 | ```Ruby 55 | users = User.all 56 | files = users.map{ |user| [user.avatar, user.avatar.filename] } 57 | zipline(files, 'avatars.zip') 58 | ``` 59 | 60 | ### Carrierwave 61 | 62 | ```Ruby 63 | users = User.all 64 | files = users.map{ |user| [user.avatar, user.avatar_identifier] } 65 | zipline(files, 'avatars.zip') 66 | ``` 67 | 68 | ### Paperclip ([deprecated](https://thoughtbot.com/blog/closing-the-trombone)) 69 | 70 | ```Ruby 71 | users = User.all 72 | files = users.map{ |user| [user.avatar, user.avatar_file_name] } 73 | zipline(files, 'avatars.zip') 74 | ``` 75 | 76 | ### Url 77 | 78 | If you know the URL of the remote file you want to include, you can just pass in the 79 | URL directly in place of the attachment object. 80 | ```Ruby 81 | avatars = [ 82 | ['http://www.example.com/user1.png', 'user1.png'] 83 | ['http://www.example.com/user2.png', 'user2.png'] 84 | ['http://www.example.com/user3.png', 'user3.png'] 85 | ] 86 | zipline(avatars, 'avatars.zip') 87 | ``` 88 | 89 | ### Directories 90 | 91 | For directories, just give the files names like "directory/file". 92 | 93 | 94 | ```Ruby 95 | avatars = [ 96 | # remote_url zip_path write_file options for Streamer 97 | [ 'http://www.example.com/user1.png', 'avatars/user1.png', modification_time: Time.now.utc ] 98 | [ 'http://www.example.com/user2.png', 'avatars/user2.png', modification_time: 1.day.ago ] 99 | [ 'http://www.example.com/user3.png', 'avatars/user3.png' ] 100 | ] 101 | 102 | zipline(avatars, 'avatars.zip') 103 | ``` 104 | 105 | ## Contributing 106 | 107 | 1. Fork it 108 | 2. Create your feature branch (`git checkout -b my-new-feature`) 109 | 3. Commit your changes (`git commit -am 'Added some feature'`) 110 | 4. Push to the branch (`git push origin my-new-feature`) 111 | 5. Create new Pull Request 112 | 113 | ## TODO (possible contributions?) 114 | 115 | * Add support for your favorite attachment plugin. 116 | * Tests. 117 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env rake 2 | require "bundler/gem_tasks" 3 | 4 | begin 5 | require 'rspec/core/rake_task' 6 | 7 | RSpec::Core::RakeTask.new(:spec) 8 | 9 | task default: :spec 10 | rescue LoadError 11 | # no rspec available 12 | end 13 | -------------------------------------------------------------------------------- /lib/zipline.rb: -------------------------------------------------------------------------------- 1 | require 'content_disposition' 2 | require 'zip_kit' 3 | require 'zipline/version' 4 | require 'zipline/zip_handler' 5 | 6 | # class MyController < ApplicationController 7 | # include Zipline 8 | # def index 9 | # users = User.all 10 | # files = users.map{ |user| [user.avatar, "#{user.username}.png", modification_time: 1.day.ago] } 11 | # zipline(files, 'avatars.zip') 12 | # end 13 | # end 14 | module Zipline 15 | def self.included(into_controller) 16 | into_controller.include(ZipKit::RailsStreaming) 17 | super 18 | end 19 | 20 | def zipline(files, zipname = 'zipline.zip', **kwargs_for_zip_kit_stream) 21 | zip_kit_stream(filename: zipname, **kwargs_for_zip_kit_stream) do |zip_kit_streamer| 22 | handler = Zipline::ZipHandler.new(zip_kit_streamer, logger) 23 | files.each do |file, name, options = {}| 24 | handler.handle_file(file, name.to_s, options) 25 | end 26 | end 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /lib/zipline/version.rb: -------------------------------------------------------------------------------- 1 | module Zipline 2 | VERSION = "2.0.0" 3 | end 4 | -------------------------------------------------------------------------------- /lib/zipline/zip_handler.rb: -------------------------------------------------------------------------------- 1 | module Zipline 2 | class ZipHandler 3 | # takes an array of pairs [[uploader, filename], ... ] 4 | def initialize(streamer, logger) 5 | @streamer = streamer 6 | @logger = logger 7 | end 8 | 9 | def handle_file(file, name, options) 10 | normalized_file = normalize(file) 11 | write_file(normalized_file, name, options) 12 | rescue => e 13 | # Since most APM packages do not trace errors occurring within streaming 14 | # Rack bodies, it can be helpful to print the error to the Rails log at least 15 | error_message = "zipline: an exception (#{e.inspect}) was raised when serving the ZIP body." 16 | error_message += " The error occurred when handling file #{name.inspect}" 17 | @logger.error(error_message) if @logger 18 | raise 19 | end 20 | 21 | # This extracts either a url or a local file from the provided file. 22 | # Currently support carrierwave and paperclip local and remote storage. 23 | # returns a hash of the form {url: aUrl} or {file: anIoObject} 24 | def normalize(file) 25 | if defined?(CarrierWave::Uploader::Base) && file.is_a?(CarrierWave::Uploader::Base) 26 | file = file.file 27 | end 28 | 29 | if defined?(Paperclip) && file.is_a?(Paperclip::Attachment) 30 | if file.options[:storage] == :filesystem 31 | {file: File.open(file.path)} 32 | else 33 | {url: file.expiring_url} 34 | end 35 | elsif defined?(CarrierWave::Storage::Fog::File) && file.is_a?(CarrierWave::Storage::Fog::File) 36 | {url: file.url} 37 | elsif defined?(CarrierWave::SanitizedFile) && file.is_a?(CarrierWave::SanitizedFile) 38 | {file: File.open(file.path)} 39 | elsif is_io?(file) 40 | {file: file} 41 | elsif defined?(ActiveStorage::Blob) && file.is_a?(ActiveStorage::Blob) 42 | {blob: file} 43 | elsif is_active_storage_attachment?(file) || is_active_storage_one?(file) 44 | {blob: file.blob} 45 | elsif file.respond_to? :url 46 | {url: file.url} 47 | elsif file.respond_to? :path 48 | {file: File.open(file.path)} 49 | elsif file.respond_to? :file 50 | {file: File.open(file.file)} 51 | elsif is_url?(file) 52 | {url: file} 53 | else 54 | raise(ArgumentError, 'Bad File/Stream') 55 | end 56 | end 57 | 58 | def write_file(file, name, options) 59 | @streamer.write_file(name, **options.slice(:modification_time)) do |writer_for_file| 60 | if file[:url] 61 | the_remote_uri = URI(file[:url]) 62 | 63 | Net::HTTP.get_response(the_remote_uri) do |response| 64 | response.read_body do |chunk| 65 | writer_for_file << chunk 66 | end 67 | end 68 | elsif file[:file] 69 | IO.copy_stream(file[:file], writer_for_file) 70 | file[:file].close 71 | elsif file[:blob] 72 | file[:blob].download { |chunk| writer_for_file << chunk } 73 | else 74 | raise(ArgumentError, 'Bad File/Stream') 75 | end 76 | end 77 | end 78 | 79 | private 80 | 81 | def is_io?(io_ish) 82 | io_ish.respond_to? :read 83 | end 84 | 85 | def is_active_storage_attachment?(file) 86 | defined?(ActiveStorage::Attachment) && file.is_a?(ActiveStorage::Attachment) 87 | end 88 | 89 | def is_active_storage_one?(file) 90 | defined?(ActiveStorage::Attached::One) && file.is_a?(ActiveStorage::Attached::One) 91 | end 92 | 93 | def is_url?(url) 94 | url = URI.parse(url) rescue false 95 | url.kind_of?(URI::HTTP) || url.kind_of?(URI::HTTPS) 96 | end 97 | end 98 | end 99 | -------------------------------------------------------------------------------- /spec/fakefile.txt: -------------------------------------------------------------------------------- 1 | hello world 2 | -------------------------------------------------------------------------------- /spec/lib/zipline/zip_handler_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'tempfile' 3 | 4 | module ActiveStorage 5 | class Attached 6 | class One < Attached 7 | end 8 | end 9 | class Attachment; end 10 | class Blob; end 11 | class Filename 12 | def initialize(name) 13 | @name = name 14 | end 15 | def to_s 16 | @name 17 | end 18 | end 19 | end 20 | 21 | describe Zipline::ZipHandler do 22 | before { Fog.mock! } 23 | let(:file_attributes){ { 24 | key: 'fog_file_tests', 25 | body: 'some body', 26 | public: true 27 | }} 28 | let(:directory_attributes){{ 29 | key: 'fog_directory' 30 | }} 31 | let(:storage_attributes){{ 32 | aws_access_key_id: 'fake_access_key_id', 33 | aws_secret_access_key: 'fake_secret_access_key', 34 | provider: 'AWS' 35 | }} 36 | let(:storage){ Fog::Storage.new(storage_attributes)} 37 | let(:directory){ storage.directories.create(directory_attributes) } 38 | let(:file){ directory.files.create(file_attributes) } 39 | 40 | describe '.normalize' do 41 | let(:handler){ Zipline::ZipHandler.new(_streamer = double(), _logger = nil)} 42 | context "CarrierWave" do 43 | context "Remote" do 44 | let(:file){ CarrierWave::Storage::Fog::File.new(nil,nil,nil) } 45 | it "extracts the url" do 46 | allow(file).to receive(:url).and_return('fakeurl') 47 | expect(File).not_to receive(:open) 48 | expect(handler.normalize(file)).to eq({url: 'fakeurl'}) 49 | end 50 | end 51 | context "Local" do 52 | let(:file){ CarrierWave::SanitizedFile.new(Tempfile.new('t')) } 53 | it "creates a File" do 54 | allow(file).to receive(:path).and_return('spec/fakefile.txt') 55 | normalized = handler.normalize(file) 56 | expect(normalized.keys).to include(:file) 57 | expect(normalized[:file]).to be_a File 58 | end 59 | end 60 | context "CarrierWave::Uploader::Base" do 61 | let(:uploader) { Class.new(CarrierWave::Uploader::Base).new } 62 | 63 | context "Remote" do 64 | let(:file){ CarrierWave::Storage::Fog::File.new(nil,nil,nil) } 65 | it "extracts the url" do 66 | allow(uploader).to receive(:file).and_return(file) 67 | allow(file).to receive(:url).and_return('fakeurl') 68 | expect(File).not_to receive(:open) 69 | expect(handler.normalize(uploader)).to eq({url: 'fakeurl'}) 70 | end 71 | end 72 | 73 | context "Local" do 74 | let(:file){ CarrierWave::SanitizedFile.new(Tempfile.new('t')) } 75 | it "creates a File" do 76 | allow(uploader).to receive(:file).and_return(file) 77 | allow(file).to receive(:path).and_return('spec/fakefile.txt') 78 | normalized = handler.normalize(uploader) 79 | expect(normalized.keys).to include(:file) 80 | expect(normalized[:file]).to be_a File 81 | end 82 | end 83 | end 84 | end 85 | context "Paperclip" do 86 | context "Local" do 87 | let(:file){ Paperclip::Attachment.new(:name, :instance) } 88 | it "creates a File" do 89 | allow(file).to receive(:path).and_return('spec/fakefile.txt') 90 | normalized = handler.normalize(file) 91 | expect(normalized.keys).to include(:file) 92 | expect(normalized[:file]).to be_a File 93 | end 94 | end 95 | context "Remote" do 96 | let(:file){ Paperclip::Attachment.new(:name, :instance, storage: :s3) } 97 | it "creates a URL" do 98 | allow(file).to receive(:expiring_url).and_return('fakeurl') 99 | expect(File).to_not receive(:open) 100 | expect(handler.normalize(file)).to include(url: 'fakeurl') 101 | end 102 | end 103 | end 104 | context "ActiveStorage" do 105 | context "Attached::One" do 106 | it "get blob" do 107 | attached = create_attached_one 108 | allow_any_instance_of(Object).to receive(:defined?).and_return(true) 109 | 110 | normalized = handler.normalize(attached) 111 | 112 | expect(normalized.keys).to include(:blob) 113 | expect(normalized[:blob]).to be_a(ActiveStorage::Blob) 114 | end 115 | end 116 | 117 | context "Attachment" do 118 | it "get blob" do 119 | attachment = create_attachment 120 | allow_any_instance_of(Object).to receive(:defined?).and_return(true) 121 | 122 | normalized = handler.normalize(attachment) 123 | 124 | expect(normalized.keys).to include(:blob) 125 | expect(normalized[:blob]).to be_a(ActiveStorage::Blob) 126 | end 127 | end 128 | 129 | context "Blob" do 130 | it "get blob" do 131 | blob = create_blob 132 | allow_any_instance_of(Object).to receive(:defined?).and_return(true) 133 | 134 | normalized = handler.normalize(blob) 135 | 136 | expect(normalized.keys).to include(:blob) 137 | expect(normalized[:blob]).to be_a(ActiveStorage::Blob) 138 | end 139 | end 140 | 141 | def create_attached_one 142 | attached = ActiveStorage::Attached::One.new 143 | blob = create_blob 144 | allow(attached).to receive(:blob).and_return(blob) 145 | attached 146 | end 147 | 148 | def create_attachment 149 | attachment = ActiveStorage::Attachment.new 150 | blob = create_blob 151 | allow(attachment).to receive(:blob).and_return(blob) 152 | attachment 153 | end 154 | 155 | def create_blob 156 | blob = ActiveStorage::Blob.new 157 | allow(blob).to receive(:service_url).and_return('fakeurl') 158 | filename = create_filename 159 | allow(blob).to receive(:filename).and_return(filename) 160 | blob 161 | end 162 | 163 | def create_filename 164 | # Rails wraps Blob#filname in this class since Rails 5.2 165 | ActiveStorage::Filename.new('test') 166 | end 167 | end 168 | context "Fog" do 169 | it "extracts url" do 170 | allow(file).to receive(:url).and_return('fakeurl') 171 | expect(File).not_to receive(:open) 172 | expect(handler.normalize(file)).to eq(url: 'fakeurl') 173 | end 174 | end 175 | context "IOStream" do 176 | let(:file){ StringIO.new('passthrough')} 177 | it "passes through" do 178 | expect(handler.normalize(file)).to eq(file: file) 179 | end 180 | end 181 | context "invalid" do 182 | let(:file){ Thread.new{} } 183 | it "raises error" do 184 | expect{handler.normalize(file)}.to raise_error(ArgumentError) 185 | end 186 | end 187 | end 188 | end 189 | -------------------------------------------------------------------------------- /spec/lib/zipline/zipline_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'action_controller' 3 | 4 | describe Zipline do 5 | before do 6 | Fog.mock! 7 | FakeController.logger = nil 8 | end 9 | 10 | class FakeController < ActionController::Base 11 | include Zipline 12 | def download_zip 13 | files = [ 14 | [StringIO.new("File content goes here"), "one.txt"], 15 | [StringIO.new("Some other content goes here"), "two.txt"] 16 | ] 17 | zipline(files, 'myfiles.zip', auto_rename_duplicate_filenames: false) 18 | end 19 | 20 | class FailingIO < StringIO 21 | def read(*) 22 | raise "Something wonky" 23 | end 24 | end 25 | 26 | def download_zip_with_error_during_streaming 27 | files = [ 28 | [StringIO.new("File content goes here"), "one.txt"], 29 | [FailingIO.new("This will fail half-way"), "two.txt"] 30 | ] 31 | zipline(files, 'myfiles.zip', auto_rename_duplicate_filenames: false) 32 | end 33 | end 34 | 35 | it 'passes keyword parameters to ZipKit::OutputEnumerator' do 36 | fake_rack_env = { 37 | "HTTP_VERSION" => "HTTP/1.0", 38 | "REQUEST_METHOD" => "GET", 39 | "SCRIPT_NAME" => "", 40 | "PATH_INFO" => "/download", 41 | "QUERY_STRING" => "", 42 | "SERVER_NAME" => "host.example", 43 | "rack.input" => StringIO.new, 44 | } 45 | expect(ZipKit::OutputEnumerator).to receive(:new).with(auto_rename_duplicate_filenames: false).and_call_original 46 | 47 | status, headers, body = FakeController.action(:download_zip).call(fake_rack_env) 48 | 49 | expect(headers['Content-Disposition']).to eq("attachment; filename=\"myfiles.zip\"; filename*=UTF-8''myfiles.zip") 50 | end 51 | 52 | it 'sends the exception raised in the streaming body to the Rails logger' do 53 | fake_rack_env = { 54 | "HTTP_VERSION" => "HTTP/1.0", 55 | "REQUEST_METHOD" => "GET", 56 | "SCRIPT_NAME" => "", 57 | "PATH_INFO" => "/download", 58 | "QUERY_STRING" => "", 59 | "SERVER_NAME" => "host.example", 60 | "rack.input" => StringIO.new, 61 | } 62 | fake_logger = double() 63 | allow(fake_logger).to receive(:warn) 64 | expect(fake_logger).to receive(:error).with(a_string_matching(/when serving the ZIP/)) 65 | 66 | FakeController.logger = fake_logger 67 | 68 | expect { 69 | status, headers, body = FakeController.action(:download_zip_with_error_during_streaming).call(fake_rack_env) 70 | body.each { } 71 | }.to raise_error(/Something wonky/) 72 | end 73 | end 74 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | require 'rspec' 2 | require 'active_support' 3 | require 'active_support/core_ext' 4 | require 'action_dispatch' 5 | 6 | require 'zipline' 7 | require 'paperclip' 8 | require 'fog-aws' 9 | require 'carrierwave' 10 | 11 | Dir["#{File.expand_path('..', __FILE__)}/support/**/*.rb"].sort.each { |f| require f } 12 | 13 | CarrierWave.configure do |config| 14 | config.fog_provider = 'fog/aws' 15 | config.fog_credentials = { 16 | provider: 'AWS', 17 | aws_access_key_id: 'dummy', 18 | aws_secret_access_key: 'data', 19 | region: 'us-west-2', 20 | } 21 | end 22 | 23 | RSpec.configure do |config| 24 | config.color = true 25 | config.order = :random 26 | config.run_all_when_everything_filtered = true 27 | end 28 | -------------------------------------------------------------------------------- /zipline.gemspec: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | require File.expand_path('../lib/zipline/version', __FILE__) 3 | 4 | Gem::Specification.new do |gem| 5 | gem.authors = ["Ram Dobson"] 6 | gem.email = ["ram.dobson@solsystemscompany.com"] 7 | gem.description = %q{a module for streaming dynamically generated zip files} 8 | gem.summary = %q{stream zip files from rails} 9 | gem.homepage = "http://github.com/fringd/zipline" 10 | 11 | gem.files = `git ls-files`.split($\) - %w{.gitignore} 12 | gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) } 13 | gem.test_files = gem.files.grep(%r{^(test|spec|features)/}) 14 | gem.name = "zipline" 15 | gem.require_paths = ["lib"] 16 | gem.version = Zipline::VERSION 17 | gem.licenses = ['MIT'] 18 | 19 | gem.required_ruby_version = ">= 2.7" 20 | 21 | gem.add_dependency 'actionpack', ['>= 6.0', '< 8.0'] 22 | gem.add_dependency 'content_disposition', '~> 1.0' 23 | gem.add_dependency 'zip_kit', ['~> 6', '>= 6.2.0', '< 7'] 24 | 25 | gem.add_development_dependency 'rspec', '~> 3' 26 | gem.add_development_dependency 'fog-aws' 27 | gem.add_development_dependency 'aws-sdk-s3' 28 | gem.add_development_dependency 'carrierwave' 29 | gem.add_development_dependency 'paperclip' 30 | gem.add_development_dependency 'rake' 31 | 32 | # https://github.com/rspec/rspec-mocks/issues/1457 33 | gem.add_development_dependency 'rspec-mocks', '~> 3.12' 34 | end 35 | --------------------------------------------------------------------------------