zipline 0.0.7
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/Gemfile +4 -0
- data/LICENSE +22 -0
- data/README.md +53 -0
- data/Rakefile +2 -0
- data/lib/zipline.rb +28 -0
- data/lib/zipline/fake_stream.rb +43 -0
- data/lib/zipline/output_stream.rb +51 -0
- data/lib/zipline/version.rb +3 -0
- data/lib/zipline/zip_generator.rb +113 -0
- data/zipline.gemspec +21 -0
- metadata +102 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 3dced1a62ccd13637dce8896c6809922661aa25d
|
4
|
+
data.tar.gz: 32b3a8cb4809dedd83248501f8ac2eb973482a39
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 89bf143bc7f27eddf9cba7064d2b43a248b594a35363643796ece7a51bdf96dcc1a61fa2f5fcd946f3b505b5afb2b1be8c5e189d107ddbfa44251f7b355bd414
|
7
|
+
data.tar.gz: 032abbeebc338a1959f95d35a3fd21e43d1a06190445bf2861387b53e5b2916f4b56d0157b299b9670f432f47e5983d6c28ed4655d0bdc19ba0e7f4cc3dd269e
|
data/Gemfile
ADDED
data/LICENSE
ADDED
@@ -0,0 +1,22 @@
|
|
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.
|
data/README.md
ADDED
@@ -0,0 +1,53 @@
|
|
1
|
+
# Zipline
|
2
|
+
|
3
|
+
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
|
+
|
5
|
+
- Removes need for large disk space or memory allocation to generate zips, even huge zips. So it works on Heroku.
|
6
|
+
- The user begins downloading immediately, which decreaceses latency, download time, and timeouts on Heroku.
|
7
|
+
|
8
|
+
## Installation
|
9
|
+
|
10
|
+
Add this line to your application's Gemfile:
|
11
|
+
|
12
|
+
gem 'zipline'
|
13
|
+
|
14
|
+
And then execute:
|
15
|
+
|
16
|
+
$ bundle
|
17
|
+
|
18
|
+
## Usage
|
19
|
+
|
20
|
+
set up some models with [carrierwave](https://github.com/jnicklas/carrierwave) or [paperclip](https://github.com/thoughtbot/paperclip).
|
21
|
+
Right now only plain file storage and S3 are supported in the case of [carrierwave](https://github.com/jnicklas/carrierwave) and only plain file storage, not S3 in the case of [paperclip](https://github.com/thoughtbot/paperclip). Alternatively, you can pass in plain old File objects. Not to be judgy, but plain old file objects are probably not the best option most of the time.
|
22
|
+
|
23
|
+
You'll need to be using [unicorn](http://unicorn.bogomips.org/) or rainbows or some other server that supports streaming output.
|
24
|
+
|
25
|
+
class MyController < ApplicationController
|
26
|
+
# enable streaming responses
|
27
|
+
include ActionController::Streaming
|
28
|
+
# enable zipline
|
29
|
+
include Zipline
|
30
|
+
|
31
|
+
def index
|
32
|
+
users= User.all
|
33
|
+
files = users.map{ |user| [user.avatar, "#{user.username}.png"] }
|
34
|
+
zipline( files, 'avatars.zip')
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
For directories, just give the files names like "directory/file".
|
39
|
+
|
40
|
+
## Contributing
|
41
|
+
|
42
|
+
1. Fork it
|
43
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
44
|
+
3. Commit your changes (`git commit -am 'Added some feature'`)
|
45
|
+
4. Push to the branch (`git push origin my-new-feature`)
|
46
|
+
5. Create new Pull Request
|
47
|
+
|
48
|
+
## TODO (possible contributions?)
|
49
|
+
|
50
|
+
* tests!
|
51
|
+
* support rails 4.0 streaming
|
52
|
+
* extract library for plain ruby streaming zips, which this will depend on.
|
53
|
+
* get my changes to support streaming zips checked in to the rubyzip library.
|
data/Rakefile
ADDED
data/lib/zipline.rb
ADDED
@@ -0,0 +1,28 @@
|
|
1
|
+
require "zipline/version"
|
2
|
+
|
3
|
+
require 'zip'
|
4
|
+
require 'curb'
|
5
|
+
|
6
|
+
require "zipline/fake_stream"
|
7
|
+
require "zipline/output_stream"
|
8
|
+
require "zipline/zip_generator"
|
9
|
+
|
10
|
+
# class MyController < ApplicationController
|
11
|
+
# include Zipline
|
12
|
+
# def index
|
13
|
+
# users= User.all
|
14
|
+
# files = users.map{ |user| [user.avatar, "#{user.username}.png"] }
|
15
|
+
# zipline( files, 'avatars.zip')
|
16
|
+
# end
|
17
|
+
# end
|
18
|
+
module Zipline
|
19
|
+
def zipline(files, zipname = 'zipline.zip')
|
20
|
+
zip_generator = ZipGenerator.new(files)
|
21
|
+
headers['Content-Disposition'] = "attachment; filename=#{zipname}"
|
22
|
+
headers['Content-Type'] = Mime::Type.lookup_by_extension('zip').to_s
|
23
|
+
response.sending_file = true
|
24
|
+
response.cache_control[:public] ||= false
|
25
|
+
self.response_body = zip_generator
|
26
|
+
self.response.headers['Last-Modified'] = Time.now.to_s
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,43 @@
|
|
1
|
+
#this is a class that acts like an IO::Stream, but really puts to the browser
|
2
|
+
module Zipline
|
3
|
+
class FakeStream
|
4
|
+
|
5
|
+
# &block is the block that each gets from rails... we pass it strings to send data
|
6
|
+
def initialize(&block)
|
7
|
+
@block = block
|
8
|
+
@pos = 0
|
9
|
+
end
|
10
|
+
|
11
|
+
def tell
|
12
|
+
@pos
|
13
|
+
end
|
14
|
+
|
15
|
+
def pos
|
16
|
+
@pos
|
17
|
+
end
|
18
|
+
|
19
|
+
def seek
|
20
|
+
throw :fit
|
21
|
+
end
|
22
|
+
|
23
|
+
def pos=
|
24
|
+
throw :fit
|
25
|
+
end
|
26
|
+
|
27
|
+
def to_s
|
28
|
+
throw :fit
|
29
|
+
end
|
30
|
+
|
31
|
+
def <<(x)
|
32
|
+
return if x.nil?
|
33
|
+
throw "bad class #{x.class}" unless x.class == String
|
34
|
+
@pos += x.bytesize
|
35
|
+
@block.call(x.to_s)
|
36
|
+
end
|
37
|
+
|
38
|
+
def close
|
39
|
+
nil
|
40
|
+
end
|
41
|
+
|
42
|
+
end
|
43
|
+
end
|
@@ -0,0 +1,51 @@
|
|
1
|
+
# a ZipOutputStream that never rewinds output
|
2
|
+
# in order for that to be possible we store only uncompressed files
|
3
|
+
module Zipline
|
4
|
+
class OutputStream < Zip::OutputStream
|
5
|
+
|
6
|
+
#we need to be able to hand out own custom output in order to stream to browser
|
7
|
+
def initialize(io)
|
8
|
+
# Create an io stream thing
|
9
|
+
super StringIO.new, true
|
10
|
+
# Overwrite it with my own
|
11
|
+
@output_stream = io
|
12
|
+
end
|
13
|
+
|
14
|
+
def stream
|
15
|
+
@output_stream
|
16
|
+
end
|
17
|
+
|
18
|
+
def put_next_entry(entry_name, size)
|
19
|
+
new_entry = Zip::Entry.new(@file_name, entry_name)
|
20
|
+
new_entry.size = size
|
21
|
+
|
22
|
+
#THIS IS THE MAGIC, tells zip to look after data for size, crc
|
23
|
+
new_entry.gp_flags = new_entry.gp_flags | 0x0008
|
24
|
+
|
25
|
+
super(new_entry)
|
26
|
+
end
|
27
|
+
|
28
|
+
# just reset state, no rewinding required
|
29
|
+
def finalize_current_entry
|
30
|
+
if current_entry
|
31
|
+
entry = current_entry
|
32
|
+
super
|
33
|
+
write_local_footer(entry)
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
def write_local_footer(entry)
|
38
|
+
@output_stream << [ 0x08074b50, entry.crc, entry.compressed_size, entry.size].pack('VVVV')
|
39
|
+
end
|
40
|
+
|
41
|
+
#never need to do this because we set correct sizes up front
|
42
|
+
def update_local_headers
|
43
|
+
nil
|
44
|
+
end
|
45
|
+
|
46
|
+
# helper to deal with difference between rubyzip 1.0 and 1.1
|
47
|
+
def current_entry
|
48
|
+
@currentEntry || @current_entry
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
@@ -0,0 +1,113 @@
|
|
1
|
+
# this class acts as a streaming body for rails
|
2
|
+
# initialize it with an array of the files you want to zip
|
3
|
+
# right now only carrierwave is supported with file storage or S3
|
4
|
+
module Zipline
|
5
|
+
class ZipGenerator
|
6
|
+
# takes an array of pairs [[uploader, filename], ... ]
|
7
|
+
def initialize(files)
|
8
|
+
@files = files
|
9
|
+
end
|
10
|
+
|
11
|
+
#this is supposed to be streamed!
|
12
|
+
def to_s
|
13
|
+
throw "stop!"
|
14
|
+
end
|
15
|
+
|
16
|
+
def each(&block)
|
17
|
+
output = new_output(&block)
|
18
|
+
OutputStream.open(output) do |zip|
|
19
|
+
@files.each do |file, name|
|
20
|
+
file = file.file if file.respond_to? :file
|
21
|
+
|
22
|
+
#normalize file
|
23
|
+
if file.class.to_s == 'CarrierWave::Storage::Fog::File'
|
24
|
+
file = file.send(:file)
|
25
|
+
end
|
26
|
+
if file.class.to_s == 'CarrierWave::SanitizedFile'
|
27
|
+
path = file.send(:file)
|
28
|
+
file = File.open(path)
|
29
|
+
end
|
30
|
+
if file.class.to_s == 'Paperclip::Attachment'
|
31
|
+
path = file.send(:path)
|
32
|
+
file = File.open(path)
|
33
|
+
end
|
34
|
+
throw "bad_file" unless %w{Fog::Storage::AWS::File File}.include? file.class.to_s
|
35
|
+
|
36
|
+
name = uniquify_name(name)
|
37
|
+
write_file(zip, file, name)
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
def new_output(&block)
|
43
|
+
FakeStream.new(&block)
|
44
|
+
end
|
45
|
+
|
46
|
+
def write_file(zip, file, name)
|
47
|
+
size = get_size(file)
|
48
|
+
|
49
|
+
zip.put_next_entry name, size
|
50
|
+
|
51
|
+
if file.is_a? File
|
52
|
+
while buffer = file.read(2048)
|
53
|
+
zip << buffer
|
54
|
+
end
|
55
|
+
else
|
56
|
+
the_remote_url = file.url(Time.now + 1.minutes)
|
57
|
+
c = Curl::Easy.new(the_remote_url) do |curl|
|
58
|
+
curl.on_body do |data|
|
59
|
+
zip << data
|
60
|
+
data.bytesize
|
61
|
+
end
|
62
|
+
end
|
63
|
+
c.perform
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
def get_size(file)
|
68
|
+
case file.class.to_s
|
69
|
+
when 'File'
|
70
|
+
file.size
|
71
|
+
when 'Fog::Storage::AWS::FILE'
|
72
|
+
file.content_length
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
def uniquify_name(name)
|
77
|
+
@used_names ||= Set.new
|
78
|
+
|
79
|
+
|
80
|
+
if @used_names.include?(name)
|
81
|
+
|
82
|
+
#remove suffix e.g. ".foo"
|
83
|
+
parts = name.split '.'
|
84
|
+
name, extension =
|
85
|
+
if parts.length == 1
|
86
|
+
#no suffix, e.g. README
|
87
|
+
parts << ''
|
88
|
+
else
|
89
|
+
extension = parts.pop
|
90
|
+
[parts.join('.'), ".#{extension}"]
|
91
|
+
end
|
92
|
+
|
93
|
+
#trailing _#{number}
|
94
|
+
pattern = /_(\d+)$/
|
95
|
+
|
96
|
+
unless name.match pattern
|
97
|
+
name = "#{name}_1"
|
98
|
+
end
|
99
|
+
|
100
|
+
while @used_names.include? name + extension
|
101
|
+
#increment trailing number
|
102
|
+
name = name.sub( pattern ) { |x| "_#{$1.to_i + 1}" }
|
103
|
+
end
|
104
|
+
|
105
|
+
#reattach suffix
|
106
|
+
name += extension
|
107
|
+
end
|
108
|
+
|
109
|
+
@used_names << name
|
110
|
+
name
|
111
|
+
end
|
112
|
+
end
|
113
|
+
end
|
data/zipline.gemspec
ADDED
@@ -0,0 +1,21 @@
|
|
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{this is a giant pile of hax that may let you stream 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
|
+
|
18
|
+
gem.add_dependency 'rubyzip', ['>= 1.0', '<= 1.1.2']
|
19
|
+
gem.add_dependency 'rails', '>= 3.2.1'
|
20
|
+
gem.add_dependency 'curb'
|
21
|
+
end
|
metadata
ADDED
@@ -0,0 +1,102 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: zipline
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.7
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Ram Dobson
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2014-03-24 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: rubyzip
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - '>='
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '1.0'
|
20
|
+
- - <=
|
21
|
+
- !ruby/object:Gem::Version
|
22
|
+
version: 1.1.2
|
23
|
+
type: :runtime
|
24
|
+
prerelease: false
|
25
|
+
version_requirements: !ruby/object:Gem::Requirement
|
26
|
+
requirements:
|
27
|
+
- - '>='
|
28
|
+
- !ruby/object:Gem::Version
|
29
|
+
version: '1.0'
|
30
|
+
- - <=
|
31
|
+
- !ruby/object:Gem::Version
|
32
|
+
version: 1.1.2
|
33
|
+
- !ruby/object:Gem::Dependency
|
34
|
+
name: rails
|
35
|
+
requirement: !ruby/object:Gem::Requirement
|
36
|
+
requirements:
|
37
|
+
- - '>='
|
38
|
+
- !ruby/object:Gem::Version
|
39
|
+
version: 3.2.1
|
40
|
+
type: :runtime
|
41
|
+
prerelease: false
|
42
|
+
version_requirements: !ruby/object:Gem::Requirement
|
43
|
+
requirements:
|
44
|
+
- - '>='
|
45
|
+
- !ruby/object:Gem::Version
|
46
|
+
version: 3.2.1
|
47
|
+
- !ruby/object:Gem::Dependency
|
48
|
+
name: curb
|
49
|
+
requirement: !ruby/object:Gem::Requirement
|
50
|
+
requirements:
|
51
|
+
- - '>='
|
52
|
+
- !ruby/object:Gem::Version
|
53
|
+
version: '0'
|
54
|
+
type: :runtime
|
55
|
+
prerelease: false
|
56
|
+
version_requirements: !ruby/object:Gem::Requirement
|
57
|
+
requirements:
|
58
|
+
- - '>='
|
59
|
+
- !ruby/object:Gem::Version
|
60
|
+
version: '0'
|
61
|
+
description: this is a giant pile of hax that may let you stream dynamically generated
|
62
|
+
zip files
|
63
|
+
email:
|
64
|
+
- ram.dobson@solsystemscompany.com
|
65
|
+
executables: []
|
66
|
+
extensions: []
|
67
|
+
extra_rdoc_files: []
|
68
|
+
files:
|
69
|
+
- Gemfile
|
70
|
+
- LICENSE
|
71
|
+
- README.md
|
72
|
+
- Rakefile
|
73
|
+
- lib/zipline.rb
|
74
|
+
- lib/zipline/fake_stream.rb
|
75
|
+
- lib/zipline/output_stream.rb
|
76
|
+
- lib/zipline/version.rb
|
77
|
+
- lib/zipline/zip_generator.rb
|
78
|
+
- zipline.gemspec
|
79
|
+
homepage: http://github.com/fringd/zipline
|
80
|
+
licenses: []
|
81
|
+
metadata: {}
|
82
|
+
post_install_message:
|
83
|
+
rdoc_options: []
|
84
|
+
require_paths:
|
85
|
+
- lib
|
86
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
87
|
+
requirements:
|
88
|
+
- - '>='
|
89
|
+
- !ruby/object:Gem::Version
|
90
|
+
version: '0'
|
91
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
92
|
+
requirements:
|
93
|
+
- - '>='
|
94
|
+
- !ruby/object:Gem::Version
|
95
|
+
version: '0'
|
96
|
+
requirements: []
|
97
|
+
rubyforge_project:
|
98
|
+
rubygems_version: 2.0.0.rc.2
|
99
|
+
signing_key:
|
100
|
+
specification_version: 4
|
101
|
+
summary: stream zip files from rails
|
102
|
+
test_files: []
|