zipline 1.0.2 → 1.3.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
- SHA1:
3
- metadata.gz: 9f78131919f81ca6a316cb1f1c7173515a230cee
4
- data.tar.gz: 5143037d6d038adf9af45b269f8653f4dd9c1d47
2
+ SHA256:
3
+ metadata.gz: 8de63c7a8518461b8fdbb249ce857b3f06ffc2bf3a7de69d6d5d6c5111f75015
4
+ data.tar.gz: edfc26eed9a1d52987a4d47330a015d2ebb0e3129f2c0791199a51b3b5abec70
5
5
  SHA512:
6
- metadata.gz: 641f00ccfa244d4a5d7e25cb99aaac1356069e9ed1bdf1ea52da7437f613ebf6cc0a31ba0c707777bf0772c899f24d556e657ee29a39a3672a8841ff5d7ef7d7
7
- data.tar.gz: bf7592dc54f890f4aadd6628d814048958308c8f20ea51ff3a34088312f497bc4b882b1b4a6fb8e8728dd03b130130cbb4c72617adc86484a738d9af810d22f6
6
+ metadata.gz: cc54e592ebd7c24cad03669786938a3662430389e81322ceef258898ec5d8e9949db956a7433ab7c4d139b4671100772c062c525f8d20b6782f3a221b80ea9f3
7
+ data.tar.gz: 2482828adb931c9630c209b7a9a06c913b43d577232dea8d877b88acb1fa3b60834c35a92175608610da0d7e80ad36d6a1f8cd2379b9af5a249ce2b1f7456990
@@ -1,4 +1,8 @@
1
1
  language: ruby
2
+ cache: bundler
2
3
  rvm:
3
4
  - 2.4
4
-
5
+ - 2.5
6
+ - 2.6
7
+ - 2.7
8
+ - 3.0
data/Gemfile CHANGED
@@ -5,12 +5,10 @@ gemspec
5
5
 
6
6
  group :development, :test do
7
7
  gem 'rspec', '~> 3'
8
- gem 'fog'
9
8
  gem 'fog-aws'
10
9
  gem 'activesupport'
11
- gem 'aws-sdk'
10
+ gem 'aws-sdk-s3'
12
11
  gem 'carrierwave'
13
12
  gem 'paperclip'
14
13
  gem 'rake'
15
- gem 'rake-rspec'
16
14
  end
data/README.md CHANGED
@@ -1,4 +1,6 @@
1
1
  # Zipline
2
+ [![Build Status](https://travis-ci.org/fringd/zipline.svg?branch=master)](https://travis-ci.org/fringd/zipline)
3
+ [![Gem Version](https://badge.fury.io/rb/zipline.svg)](https://badge.fury.io/rb/zipline)
2
4
 
3
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:
4
6
 
@@ -28,37 +30,75 @@ supported with [shrine](https://github.com/janko-m/shrine).
28
30
 
29
31
  You'll need to be using puma or some other server that supports streaming output.
30
32
 
31
- class MyController < ApplicationController
32
- # enable streaming responses
33
- include ActionController::Streaming
34
- # enable zipline
35
- include Zipline
36
-
37
- def index
38
- users = User.all
39
- # you can replace user.avatar with any stream or any object that
40
- # responds to :url
41
- files = users.map{ |user| [user.avatar, "#{user.username}.png"] }
42
- zipline( files, 'avatars.zip')
43
- end
44
- end
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
+ zipline(files, 'avatars.zip')
45
+ end
46
+ end
47
+ ```
48
+
49
+ ### ActiveStorage
50
+
51
+ ```Ruby
52
+ users = User.all
53
+ files = users.map{ |user| [user.avatar, user.avatar.filename] }
54
+ zipline(files, 'avatars.zip')
55
+ ```
56
+
57
+ ### Carrierwave
58
+
59
+ ```Ruby
60
+ users = User.all
61
+ files = users.map{ |user| [user.avatar, user.avatar_identifier] }
62
+ zipline(files, 'avatars.zip')
63
+ ```
64
+
65
+ ### Paperclip ([deprecated](https://thoughtbot.com/blog/closing-the-trombone))
66
+
67
+ ```Ruby
68
+ users = User.all
69
+ files = users.map{ |user| [user.avatar, user.avatar_file_name] }
70
+ zipline(files, 'avatars.zip')
71
+ ```
72
+
73
+ ### Url
74
+
75
+ If you know the URL of the remote file you want to include, you can just pass in the
76
+ URL directly in place of the attachment object.
77
+ ```Ruby
78
+ avatars = [
79
+ ['http://www.example.com/user1.png', 'user1.png']
80
+ ['http://www.example.com/user2.png', 'user2.png']
81
+ ['http://www.example.com/user3.png', 'user3.png']
82
+ ]
83
+ zipline(avatars, 'avatars.zip')
84
+ ```
85
+
86
+ ### Directories
45
87
 
46
88
  For directories, just give the files names like "directory/file".
47
89
 
48
- To stream files from a remote URL, use open-uri with a [lazy enumerator](http://ruby-doc.org/core-2.0.0/Enumerator/Lazy.html):
49
-
50
- require 'open-uri'
51
- avatars = [
52
- # remote_url zip_path
53
- [ 'http://www.example.com/user1.png', 'avatars/user1.png' ]
54
- [ 'http://www.example.com/user2.png', 'avatars/user2.png' ]
55
- [ 'http://www.example.com/user3.png', 'avatars/user3.png' ]
56
- ]
57
- file_mappings = avatars
58
- .lazy # Lazy allows us to begin sending the download immediately instead of waiting to download everything
59
- .map { |url, path| [open(url), path] }
60
- zipline(file_mappings, 'avatars.zip')
61
-
90
+
91
+ ```Ruby
92
+ avatars = [
93
+ # remote_url zip_path zip_tricks_options
94
+ [ 'http://www.example.com/user1.png', 'avatars/user1.png', modification_time: Time.now.utc ]
95
+ [ 'http://www.example.com/user2.png', 'avatars/user2.png', modification_time: 1.day.ago ]
96
+ [ 'http://www.example.com/user3.png', 'avatars/user3.png' ]
97
+ ]
98
+
99
+ zipline(avatars, 'avatars.zip')
100
+ ```
101
+
62
102
  ## Contributing
63
103
 
64
104
  1. Fork it
data/Rakefile CHANGED
@@ -1,4 +1,12 @@
1
1
  #!/usr/bin/env rake
2
2
  require "bundler/gem_tasks"
3
- require "rake/rspec"
4
- task :default => :spec
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
@@ -1,14 +1,13 @@
1
1
  require "zipline/version"
2
- require 'curb'
3
2
  require 'zip_tricks'
4
3
  require "zipline/zip_generator"
5
4
 
6
5
  # class MyController < ApplicationController
7
6
  # include Zipline
8
7
  # def index
9
- # users= User.all
10
- # files = users.map{ |user| [user.avatar, "#{user.username}.png"] }
11
- # zipline( files, 'avatars.zip')
8
+ # users = User.all
9
+ # files = users.map{ |user| [user.avatar, "#{user.username}.png", modification_time: 1.day.ago] }
10
+ # zipline(files, 'avatars.zip')
12
11
  # end
13
12
  # end
14
13
  module Zipline
@@ -1,3 +1,3 @@
1
1
  module Zipline
2
- VERSION = "1.0.2"
2
+ VERSION = "1.3.0"
3
3
  end
@@ -14,14 +14,32 @@ module Zipline
14
14
 
15
15
  def each(&block)
16
16
  fake_io_writer = ZipTricks::BlockWrite.new(&block)
17
- ZipTricks::Streamer.open(fake_io_writer) do |streamer|
18
- @files.each {|file, name| handle_file(streamer, file, name) }
17
+ # ZipTricks outputs lots of strings in rapid succession, and with
18
+ # servers it can be beneficial to avoid doing too many tiny writes so that
19
+ # the number of syscalls is minimized. See https://github.com/WeTransfer/zip_tricks/issues/78
20
+ # There is a built-in facility for this in ZipTricks which can be used to implement
21
+ # some cheap buffering here (it exists both in version 4 and version 5). The buffer is really
22
+ # tiny and roughly equal to the medium Linux socket buffer size (16 KB). Although output
23
+ # will be not so immediate with this buffering the overall performance will be better,
24
+ # especially with multiple clients being serviced at the same time.
25
+ # Note that the WriteBuffer writes the same, retained String object - but the contents
26
+ # of that object changes between calls. This should work fine with servers where the
27
+ # contents of the string gets written to a socket immediately before the execution inside
28
+ # the WriteBuffer resumes), but if the strings get retained somewhere - like in an Array -
29
+ # this might pose a problem. Unlikely that it will be an issue here though.
30
+ write_buffer_size = 16 * 1024
31
+ write_buffer = ZipTricks::WriteBuffer.new(fake_io_writer, write_buffer_size)
32
+ ZipTricks::Streamer.open(write_buffer) do |streamer|
33
+ @files.each do |file, name, options = {}|
34
+ handle_file(streamer, file, name.to_s, options)
35
+ end
19
36
  end
37
+ write_buffer.flush! # for any remaining writes
20
38
  end
21
39
 
22
- def handle_file(streamer, file, name)
40
+ def handle_file(streamer, file, name, options)
23
41
  file = normalize(file)
24
- write_file(streamer, file, name)
42
+ write_file(streamer, file, name, options)
25
43
  end
26
44
 
27
45
  # This extracts either a url or a local file from the provided file.
@@ -45,32 +63,37 @@ module Zipline
45
63
  elsif is_io?(file)
46
64
  {file: file}
47
65
  elsif defined?(ActiveStorage::Blob) && file.is_a?(ActiveStorage::Blob)
48
- {url: file.service_url}
66
+ {blob: file}
67
+ elsif is_active_storage_attachment?(file) || is_active_storage_one?(file)
68
+ {blob: file.blob}
49
69
  elsif file.respond_to? :url
50
70
  {url: file.url}
51
71
  elsif file.respond_to? :path
52
72
  {file: File.open(file.path)}
53
73
  elsif file.respond_to? :file
54
74
  {file: File.open(file.file)}
75
+ elsif is_url?(file)
76
+ {url: file}
55
77
  else
56
78
  raise(ArgumentError, 'Bad File/Stream')
57
79
  end
58
80
  end
59
81
 
60
- def write_file(streamer, file, name)
61
- streamer.write_deflated_file(name) do |writer_for_file|
82
+ def write_file(streamer, file, name, options)
83
+ streamer.write_deflated_file(name, options) do |writer_for_file|
62
84
  if file[:url]
63
- the_remote_url = file[:url]
64
- c = Curl::Easy.new(the_remote_url) do |curl|
65
- curl.on_body do |data|
66
- writer_for_file << data
67
- data.bytesize
85
+ the_remote_uri = URI(file[:url])
86
+
87
+ Net::HTTP.get_response(the_remote_uri) do |response|
88
+ response.read_body do |chunk|
89
+ writer_for_file << chunk
68
90
  end
69
91
  end
70
- c.perform
71
92
  elsif file[:file]
72
93
  IO.copy_stream(file[:file], writer_for_file)
73
94
  file[:file].close
95
+ elsif file[:blob]
96
+ file[:blob].download { |chunk| writer_for_file << chunk }
74
97
  else
75
98
  raise(ArgumentError, 'Bad File/Stream')
76
99
  end
@@ -80,5 +103,20 @@ module Zipline
80
103
  def is_io?(io_ish)
81
104
  io_ish.respond_to? :read
82
105
  end
106
+
107
+ private
108
+
109
+ def is_active_storage_attachment?(file)
110
+ defined?(ActiveStorage::Attachment) && file.is_a?(ActiveStorage::Attachment)
111
+ end
112
+
113
+ def is_active_storage_one?(file)
114
+ defined?(ActiveStorage::Attached::One) && file.is_a?(ActiveStorage::Attached::One)
115
+ end
116
+
117
+ def is_url?(url)
118
+ url = URI.parse(url) rescue false
119
+ url.kind_of?(URI::HTTP) || url.kind_of?(URI::HTTPS)
120
+ end
83
121
  end
84
122
  end
@@ -1,6 +1,23 @@
1
1
  require 'spec_helper'
2
2
  require 'tempfile'
3
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
+
4
21
  describe Zipline::ZipGenerator do
5
22
  before { Fog.mock! }
6
23
  let(:file_attributes){ {
@@ -84,29 +101,68 @@ describe Zipline::ZipGenerator do
84
101
  end
85
102
  end
86
103
  end
87
- context "ActiveStorage::Blob" do
88
- module ActiveStorage
89
- class Filename; end
90
- class Blob; end
91
- end
92
-
93
- let(:tempfile){ Tempfile.new('t').read }
94
- let(:filename) do
95
- fn = ActiveStorage::Filename.new()
96
- allow(fn).to receive(:to_s).and_return('spec/fakefile.txt')
97
- fn
98
- end
99
- let(:file) do
100
- f = ActiveStorage::Blob.new()
101
- allow(f).to receive(:filename).and_return(filename)
102
- allow(f).to receive(:service_url).and_return('fakeurl')
103
- f
104
- end
105
- it "creates a File" do
106
- allow_any_instance_of(Object).to receive(:defined?).and_return(true)
107
- normalized = generator.normalize(file)
108
- expect(normalized.keys).to include(:url)
109
- expect(normalized[:url]).to eq('fakeurl')
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 = generator.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 = generator.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 = generator.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')
110
166
  end
111
167
  end
112
168
  context "Fog" do
@@ -130,4 +186,65 @@ describe Zipline::ZipGenerator do
130
186
  end
131
187
  end
132
188
 
189
+ describe '.write_file' do
190
+ let(:file) { StringIO.new('passthrough') }
191
+
192
+ context 'when passing an ActiveStorage::Filename object as filename' do
193
+ let(:filename) { ActiveStorage::Filename.new('test') }
194
+
195
+ let(:generator) do
196
+ Zipline::ZipGenerator.new([[file, filename]])
197
+ end
198
+
199
+ it 'passes a string as filename to ZipTricks' do
200
+ allow(file).to receive(:url).and_return('fakeurl')
201
+ expect_any_instance_of(ZipTricks::Streamer).to receive(:write_deflated_file)
202
+ .with('test', {})
203
+ generator.each { |_| 'Test' }
204
+ end
205
+ end
206
+ end
207
+
208
+ describe 'passing an options hash' do
209
+ let(:file) { StringIO.new('passthrough') }
210
+
211
+ context 'with optional arguments' do
212
+ let(:mtime) { 1.day.ago }
213
+ let(:generator) do
214
+ Zipline::ZipGenerator.new([[file, 'test', modification_time: mtime]])
215
+ end
216
+
217
+ it 'passes the options hash through handle_file' do
218
+ expect(generator).to receive(:handle_file)
219
+ .with(anything, anything, anything, modification_time: mtime)
220
+ generator.each { |_| 'Test' }
221
+ end
222
+
223
+ it 'passes the options hash to ZipTricks' do
224
+ allow(file).to receive(:url).and_return('fakeurl')
225
+ expect_any_instance_of(ZipTricks::Streamer).to receive(:write_deflated_file)
226
+ .with(anything, modification_time: mtime)
227
+ generator.each { |_| 'Test' }
228
+ end
229
+ end
230
+
231
+ context 'without optional arguments' do
232
+ let(:generator) do
233
+ Zipline::ZipGenerator.new([[file, 'test']])
234
+ end
235
+
236
+ it 'passes the options hash through handle_file' do
237
+ expect(generator).to receive(:handle_file)
238
+ .with(anything, anything, anything, {})
239
+ generator.each { |_| 'Test' }
240
+ end
241
+
242
+ it 'passes the options hash to ZipTricks' do
243
+ allow(file).to receive(:url).and_return('fakeurl')
244
+ expect_any_instance_of(ZipTricks::Streamer).to receive(:write_deflated_file)
245
+ .with(anything, {})
246
+ generator.each { |_| 'Test' }
247
+ end
248
+ end
249
+ end
133
250
  end
@@ -2,9 +2,7 @@ require 'rspec'
2
2
  require 'active_support'
3
3
  require 'active_support/core_ext'
4
4
  require 'zipline'
5
- require 'aws-sdk'
6
5
  require 'paperclip'
7
- require 'fog'
8
6
  require 'fog-aws'
9
7
  require 'carrierwave'
10
8
 
@@ -21,8 +19,6 @@ CarrierWave.configure do |config|
21
19
 
22
20
  end
23
21
 
24
-
25
-
26
22
  RSpec.configure do |config|
27
23
  config.color = true
28
24
  config.order = :random
@@ -14,8 +14,8 @@ Gem::Specification.new do |gem|
14
14
  gem.name = "zipline"
15
15
  gem.require_paths = ["lib"]
16
16
  gem.version = Zipline::VERSION
17
+ gem.licenses = ['MIT']
17
18
 
18
- gem.add_dependency 'zip_tricks', ['>= 4.2.1', '<= 5.0.0']
19
- gem.add_dependency 'rails', ['>= 3.2.1', '< 5.3']
20
- gem.add_dependency 'curb', ['>= 0.8.0', '< 0.10']
19
+ gem.add_dependency 'actionpack', ['>= 3.2.1', '< 7.0']
20
+ gem.add_dependency 'zip_tricks', ['>= 4.2.1', '< 6.0']
21
21
  end
metadata CHANGED
@@ -1,37 +1,17 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: zipline
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.2
4
+ version: 1.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ram Dobson
8
- autorequire:
8
+ autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2018-04-24 00:00:00.000000000 Z
11
+ date: 2021-01-25 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
- name: zip_tricks
15
- requirement: !ruby/object:Gem::Requirement
16
- requirements:
17
- - - ">="
18
- - !ruby/object:Gem::Version
19
- version: 4.2.1
20
- - - "<="
21
- - !ruby/object:Gem::Version
22
- version: 5.0.0
23
- type: :runtime
24
- prerelease: false
25
- version_requirements: !ruby/object:Gem::Requirement
26
- requirements:
27
- - - ">="
28
- - !ruby/object:Gem::Version
29
- version: 4.2.1
30
- - - "<="
31
- - !ruby/object:Gem::Version
32
- version: 5.0.0
33
- - !ruby/object:Gem::Dependency
34
- name: rails
14
+ name: actionpack
35
15
  requirement: !ruby/object:Gem::Requirement
36
16
  requirements:
37
17
  - - ">="
@@ -39,7 +19,7 @@ dependencies:
39
19
  version: 3.2.1
40
20
  - - "<"
41
21
  - !ruby/object:Gem::Version
42
- version: '5.3'
22
+ version: '7.0'
43
23
  type: :runtime
44
24
  prerelease: false
45
25
  version_requirements: !ruby/object:Gem::Requirement
@@ -49,27 +29,27 @@ dependencies:
49
29
  version: 3.2.1
50
30
  - - "<"
51
31
  - !ruby/object:Gem::Version
52
- version: '5.3'
32
+ version: '7.0'
53
33
  - !ruby/object:Gem::Dependency
54
- name: curb
34
+ name: zip_tricks
55
35
  requirement: !ruby/object:Gem::Requirement
56
36
  requirements:
57
37
  - - ">="
58
38
  - !ruby/object:Gem::Version
59
- version: 0.8.0
39
+ version: 4.2.1
60
40
  - - "<"
61
41
  - !ruby/object:Gem::Version
62
- version: '0.10'
42
+ version: '6.0'
63
43
  type: :runtime
64
44
  prerelease: false
65
45
  version_requirements: !ruby/object:Gem::Requirement
66
46
  requirements:
67
47
  - - ">="
68
48
  - !ruby/object:Gem::Version
69
- version: 0.8.0
49
+ version: 4.2.1
70
50
  - - "<"
71
51
  - !ruby/object:Gem::Version
72
- version: '0.10'
52
+ version: '6.0'
73
53
  description: a module for streaming dynamically generated zip files
74
54
  email:
75
55
  - ram.dobson@solsystemscompany.com
@@ -90,9 +70,10 @@ files:
90
70
  - spec/spec_helper.rb
91
71
  - zipline.gemspec
92
72
  homepage: http://github.com/fringd/zipline
93
- licenses: []
73
+ licenses:
74
+ - MIT
94
75
  metadata: {}
95
- post_install_message:
76
+ post_install_message:
96
77
  rdoc_options: []
97
78
  require_paths:
98
79
  - lib
@@ -107,9 +88,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
107
88
  - !ruby/object:Gem::Version
108
89
  version: '0'
109
90
  requirements: []
110
- rubyforge_project:
111
- rubygems_version: 2.6.14
112
- signing_key:
91
+ rubygems_version: 3.2.3
92
+ signing_key:
113
93
  specification_version: 4
114
94
  summary: stream zip files from rails
115
95
  test_files: