s3_multipart 0.0.8 → 0.0.9
Sign up to get free protection for your applications and to get access to all the features.
- data/README.md +9 -2
- data/app/controllers/s3_multipart/uploads_controller.rb +3 -1
- data/app/models/s3_multipart/upload.rb +14 -7
- data/javascripts/s3mp.js +1 -0
- data/lib/generators/s3_multipart/templates/uploader.rb +3 -0
- data/lib/generators/s3_multipart/templates/uploads_table_migration.rb +2 -1
- data/lib/s3_multipart/railtie.rb +1 -1
- data/lib/s3_multipart/uploader/validations.rb +5 -1
- data/lib/s3_multipart/uploader.rb +0 -1
- data/lib/s3_multipart/version.rb +1 -1
- data/lib/s3_multipart.rb +4 -1
- data/spec/internal/app/uploaders/multipart/video_uploader.rb +3 -0
- data/spec/internal/db/schema.rb +1 -0
- data/vendor/assets/javascripts/s3_multipart/lib.js +1 -0
- data/vendor/assets/javascripts/s3_multipart/lib.min.js +1 -1
- metadata +3 -3
data/README.md
CHANGED
@@ -109,6 +109,9 @@ class VideoUploader < ApplicationController
|
|
109
109
|
# Only accept certain file types. Expects an array of valid extensions.
|
110
110
|
accept %w(wmv avi mp4 mkv mov mpeg)
|
111
111
|
|
112
|
+
# Define the minimum and maximum allowed file sizes (in bytes)
|
113
|
+
limit min: 5*1000*1000, max: 2*1000*1000*1000
|
114
|
+
|
112
115
|
# Takes in a block that will be evaluated when the upload has been
|
113
116
|
# successfully initiated. The block will be passed an instance of
|
114
117
|
# the upload object as well as the session hash when the callback is made.
|
@@ -141,7 +144,7 @@ To add the multipart uploader to a view, insert the following:
|
|
141
144
|
|
142
145
|
```ruby
|
143
146
|
<%= multipart_uploader_form(input_name: 'uploader',
|
144
|
-
uploader: 'VideoUploader'
|
147
|
+
uploader: 'VideoUploader',
|
145
148
|
button_class: 'submit-button',
|
146
149
|
button_text: 'Upload selected videos',
|
147
150
|
html: %Q{<button class="upload-button">Select videos</button>}) %>
|
@@ -210,12 +213,16 @@ S3_Multipart is very much a work in progress. If you squash a bug, make enhancem
|
|
210
213
|
|
211
214
|
The library is working on the latest version of IE, Firefox, Safari, and Chrome. Tests for over 100 browsers are currently being conducted.
|
212
215
|
|
216
|
+
## What's New
|
217
|
+
|
218
|
+
**0.0.9** - File type and size validations are now specified in the upload controller. Untested support for browsers that lack the FileBlob API
|
219
|
+
|
213
220
|
## To Do
|
214
221
|
|
215
222
|
* ~~If the FileBlob API is not supported on page load, the uploader should just send one giant chunk~~ (DONE)
|
216
223
|
* Handle network errors in the javascript client library
|
217
224
|
* ~~File type validations~~ (DONE)
|
218
|
-
* File size validations
|
225
|
+
* ~~File size validations~~ (DONE)
|
219
226
|
* More and better tests
|
220
227
|
* More browser testing
|
221
228
|
* Roll file signing and initiation into one request
|
@@ -6,7 +6,9 @@ module S3Multipart
|
|
6
6
|
upload = Upload.create(params)
|
7
7
|
upload.execute_callback(:begin, session)
|
8
8
|
response = upload.to_json
|
9
|
-
rescue
|
9
|
+
rescue FileTypeError, FileSizeError => e
|
10
|
+
response = {error: e.message}
|
11
|
+
rescue
|
10
12
|
response = {error: 'There was an error initiating the upload'}
|
11
13
|
ensure
|
12
14
|
render :json => response
|
@@ -2,12 +2,12 @@ module S3Multipart
|
|
2
2
|
class Upload < ::ActiveRecord::Base
|
3
3
|
extend S3Multipart::TransferHelpers
|
4
4
|
|
5
|
-
attr_accessible :key, :upload_id, :name, :location, :uploader
|
6
|
-
|
5
|
+
attr_accessible :key, :upload_id, :name, :location, :uploader, :size
|
6
|
+
before_create :validate_file_type, :validate_file_size
|
7
7
|
|
8
8
|
def self.create(params)
|
9
9
|
response = initiate(params)
|
10
|
-
super(key: response["key"], upload_id: response["upload_id"], name: response["name"], uploader: params["uploader"])
|
10
|
+
super(key: response["key"], upload_id: response["upload_id"], name: response["name"], uploader: params["uploader"], size: params["content_size"])
|
11
11
|
end
|
12
12
|
|
13
13
|
def execute_callback(stage, session)
|
@@ -23,12 +23,19 @@ module S3Multipart
|
|
23
23
|
|
24
24
|
private
|
25
25
|
|
26
|
-
def
|
27
|
-
|
28
|
-
|
26
|
+
def validate_file_size
|
27
|
+
size = self.size
|
28
|
+
limits = deserialize(self.uploader).size_limits
|
29
|
+
raise FileSizeError, "File size is too small" if limits[:min] > size
|
30
|
+
raise FileSizeError, "File size is too large" if limits[:max] < size
|
31
|
+
end
|
32
|
+
|
33
|
+
def validate_file_type
|
34
|
+
ext = self.name.match(/\.([a-zA-Z0-9]+)$/)[1]
|
35
|
+
controller = deserialize(self.uploader)
|
29
36
|
|
30
37
|
if !controller.file_types.include?(ext)
|
31
|
-
|
38
|
+
raise FileTypeError, "File type not supported"
|
32
39
|
end
|
33
40
|
end
|
34
41
|
|
data/javascripts/s3mp.js
CHANGED
@@ -160,6 +160,7 @@ S3MP.prototype.initiateMultipart = function(upload, cb) {
|
|
160
160
|
url = '/s3_multipart/uploads';
|
161
161
|
body = JSON.stringify({ object_name : upload.name,
|
162
162
|
content_type : upload.type,
|
163
|
+
content_size : upload.size,
|
163
164
|
uploader : $(this.fileInputElement).data("uploader")
|
164
165
|
});
|
165
166
|
|
@@ -8,6 +8,9 @@ class <%= model_constant %>Uploader < ApplicationController
|
|
8
8
|
# Only accept certain file types. Expects an array of valid extensions.
|
9
9
|
accept %w(wmv avi mp4 mkv mov mpeg)
|
10
10
|
|
11
|
+
# Define the minimum and maximum allowed file sizes (in bytes)
|
12
|
+
limit min: 5*1000*1000, max: 2*1000*1000*1000
|
13
|
+
|
11
14
|
# Takes in a block that will be evaluated when the upload has been
|
12
15
|
# successfully initiated. The block will be passed an instance of
|
13
16
|
# the upload object as well as the session hashwhen the callback is made.
|
data/lib/s3_multipart/railtie.rb
CHANGED
@@ -15,7 +15,7 @@ if defined?(Rails)
|
|
15
15
|
# Load all of the upload controllers in app/uploaders/multipart
|
16
16
|
initializer "s3_multipart.load_upload_controllers" do
|
17
17
|
begin
|
18
|
-
uploaders = Dir.entries(Rails.root.join('app', 'uploaders', 'multipart').to_s).keep_if {|n| n =~ /
|
18
|
+
uploaders = Dir.entries(Rails.root.join('app', 'uploaders', 'multipart').to_s).keep_if {|n| n =~ /uploader\.rb$/}
|
19
19
|
uploaders.each do |uploader|
|
20
20
|
require "#{Rails.root.join('app', 'uploaders', 'multipart')}/#{uploader}"
|
21
21
|
end
|
@@ -2,12 +2,16 @@ module S3Multipart
|
|
2
2
|
module Uploader
|
3
3
|
module Validations
|
4
4
|
|
5
|
-
attr_accessor :file_types
|
5
|
+
attr_accessor :file_types, :size_limits
|
6
6
|
|
7
7
|
def accept(types)
|
8
8
|
self.file_types = types
|
9
9
|
end
|
10
10
|
|
11
|
+
def limit(sizes)
|
12
|
+
self.size_limits = sizes
|
13
|
+
end
|
14
|
+
|
11
15
|
end
|
12
16
|
end
|
13
17
|
end
|
data/lib/s3_multipart/version.rb
CHANGED
data/lib/s3_multipart.rb
CHANGED
@@ -18,6 +18,9 @@ module S3Multipart
|
|
18
18
|
|
19
19
|
end
|
20
20
|
|
21
|
+
class FileTypeError < StandardError; end
|
22
|
+
class FileSizeError < StandardError; end
|
23
|
+
|
21
24
|
end
|
22
25
|
|
23
26
|
require 's3_multipart/config'
|
@@ -25,4 +28,4 @@ require 's3_multipart/railtie'
|
|
25
28
|
require 's3_multipart/engine'
|
26
29
|
require 's3_multipart/http/net_http'
|
27
30
|
require 's3_multipart/uploader'
|
28
|
-
require 's3_multipart/transfer_helpers'
|
31
|
+
require 's3_multipart/transfer_helpers'
|
@@ -8,6 +8,9 @@ class VideoUploader < ApplicationController
|
|
8
8
|
# Only accept certain file types. Expects an array of valid extensions.
|
9
9
|
accept %w(wmv avi mp4 mkv mov mpeg)
|
10
10
|
|
11
|
+
# Define the minimum and maximum allowed file sizes (in bytes)
|
12
|
+
limit min: 5*1000*1000, max: 2*1000*1000*1000
|
13
|
+
|
11
14
|
# Takes in a block that will be evaluated when the upload has been
|
12
15
|
# successfully initiated. The block will be passed an instance of
|
13
16
|
# the upload object when the callback is made.
|
data/spec/internal/db/schema.rb
CHANGED
@@ -174,6 +174,7 @@ S3MP.prototype.initiateMultipart = function(upload, cb) {
|
|
174
174
|
url = '/s3_multipart/uploads';
|
175
175
|
body = JSON.stringify({ object_name : upload.name,
|
176
176
|
content_type : upload.type,
|
177
|
+
content_size : upload.size,
|
177
178
|
uploader : $(this.fileInputElement).data("uploader")
|
178
179
|
});
|
179
180
|
|
@@ -1 +1 @@
|
|
1
|
-
(function(e){e.S3MP=function(){function t(t){var r,i=[],s=this;_.extend(this,t),this.uploadList=[],this.handler={beginUpload:function(){function t(t,n){var r=n.key,i=n.parts.length;typeof e[r]=="undefined"&&(e[r]=0),e[r]++;if(e[r]===i){for(var o=0;o<t;o++)n.parts[o].activate();s.handler.startProgressTimer(r),s.onStart(n)}}var e=[];return t}(),onError:function(e,t){},onPartSuccess:function(e,t){var n,r,i;n=e.parts,i=t.xhr.getResponseHeader("ETag"),e.Etags.push({ETag:i.replace(/\"/g,""),partNum:t.num}),e.uploaded+=t.size,e.inprogress[t.num]=0,r=_.indexOf(n,t),n.splice(r,1),n.length&&(r=_.findIndex(n,function(e,t,n){if(e.status!=="active")return!0}),r!==-1&&n[r].activate()),n.length||this.onComplete(e)},onComplete:function(e){var t=_.indexOf(s.uploadList,e);this.clearProgressTimer(t),s.completeMultipart(e,function(t){t.location&&s.onComplete(e)})},onProgress:function(e,t,n,r,i){s.onProgress(e,t,n,r,i)},startProgressTimer:function(){var t=[],n=function(n){i[n]=e.setInterval(function(){var e,r,i,o,u;typeof t[n]=="undefined"&&(t[n]=0),e=s.uploadList[n],r=e.size,i=e.uploaded,_.each(e.inprogress,function(e){i+=e}),o=i/r*100,u=i-t[n],t[n]=i,e.handler.onProgress(n,r,i,o,u)},1e3)};return n}(),clearProgressTimer:function(t){e.clearInterval(i[t])}},this.fileSelector?r=$(this.fileSelector).get(0).files:r=this.fileList,_.each(r,function(e,t){if(e.size<5e6)return s.onError({name:"FileSizeError",message:"File size is too small"});var r=new n(e,s,t);s.uploadList.push(r),r.init()})}function n(e,t,n){function i(){var t,i,s,o,u,a,f,l,c;t=this,this.key=n,this.file=e,this.name=e.name,this.size=e.size,this.type=e.type,this.Etags=[],this.inprogress=[],this.uploaded=0,this.status="",this.size>1e9?(num_segs=100,l=10):this.size>5e8?(num_segs=50,l=5):this.size>1e8?(num_segs=20,l=5):(num_segs=2,l=2),a=_.range(num_segs+1),f=_.map(a,function(t){return Math.round(t*(e.size/num_segs))}),t.sliceBlob=="Unsupported"?this.parts=[new r(e,0,t)]:(this.parts=_.map(f,function(n,i){return c=t.sliceBlob(e,n,f[i+1]),new r(c,i+1,t)}),this.parts.pop()),this.init=function(){t.initiateMultipart(t,function(e){var n=t.id=e.id,r=t.upload_id=e.upload_id,i=t.object_name=e.key,s=t.parts;t.signPartRequests(n,i,r,s,function(e){_.each(s,function(n,s){var o=n.xhr;o.open("PUT","http://"+t.bucket+".s3.amazonaws.com/"+i+"?partNumber="+n.num+"&uploadId="+r,!0),o.setRequestHeader("x-amz-date",e[s].date),o.setRequestHeader("Authorization",e[s].authorization),t.handler.beginUpload(l,t)})})})}}return i.prototype=t,new i}function r(e,t,n){var r,i;r=this,this.size=e.size,this.blob=e,this.num=t,this.xhr=i=n.createXhrRequest(),i.onload=function(){n.handler.onPartSuccess(n,r)},i.onerror=function(){n.handler.onError(n,r)},i.upload.onprogress=_.throttle(function(e){n.inprogress[t]=e.loaded},1e3)}return _.mixin({findIndex:function(e,t){for(var n=0;n<e.length;n++)if(t(e[n],n,e))return n;return-1}}),t.prototype.initiateMultipart=function(e,t){var n,r,i;n="/s3_multipart/uploads",r=JSON.stringify({object_name:e.name,content_type:e.type,uploader:$(this.fileInputElement).data("uploader")}),i=this.createXhrRequest("POST",n),this.deliverRequest(i,r,t)},t.prototype.signPartRequests=function(e,t,n,r,i){var s,o,u,a;s=_.reduce(_.rest(r),function(e,t){return e+"-"+t.size},r[0].size),o="s3_multipart/uploads/"+e,u=JSON.stringify({object_name:t,upload_id:n,content_lengths:s}),a=this.createXhrRequest("PUT",o),this.deliverRequest(a,u,i)},t.prototype.completeMultipart=function(e,t){var n,r,i;n="s3_multipart/uploads/"+e.id,r=JSON.stringify({object_name:e.object_name,upload_id:e.upload_id,content_length:e.size,parts:e.Etags}),i=this.createXhrRequest("PUT",n),this.deliverRequest(i,r,t)},t.prototype.deliverRequest=function(e,t,n){var r=this;e.onload=function(){response=JSON.parse(this.responseText);if(response.error)return r.onError({name:"ServerResponse",message:"The server responded with an error"});n(response)},e.onerror=function(){},e.setRequestHeader("Content-Type","application/json"),e.setRequestHeader("X-CSRF-Token",$('meta[name="csrf-token"]').attr("content")),e.send(t)},t.prototype.createXhrRequest=function(){var e;return typeof XMLHttpRequest.constructor=="function"?e=XMLHttpRequest:typeof XDomainRequest!="undefined"?e=XDomainRequest:e=null,function(t,n,r,i){var s,o,i=!0;return s=Array.prototype.slice.call(arguments),typeof s[0]=="undefined"&&(r=null,i=!1),o=new e,i&&o.open(t,n,!0),o.onreadystatechange=r,o}}(),t.prototype.sliceBlob=function(){try{var e=new Blob}catch(t){return"Unsupported"}return e.slice?function(e,t,n){return e.slice(t,n)}:e.mozSlice?function(e,t,n){return e.mozSlice(t,n)}:e.webkitSlice?function(e,t,n){return e.webkitSlice(t,n)}:"Unsupported"}(),t.prototype._returnUploadObj=function(e){var t=_.find(this.uploadList,function(t){return t.key===e});return t},t.prototype.cancel=function(e){var t,n;t=this._returnUploadObj(e),n=_.indexOf(this.uploadList,t),this.uploadList.splice(n,n+1),this.onCancel()},t.prototype.pause=function(e){var t=this._returnUploadObj(e);_.each(t.parts,function(e,t,n){e.status=="active"&&e.pause()}),this.onPause()},t.prototype.resume=function(e){var t=this._returnUploadObj(e);_.each(t.parts,function(e,t,n){e.status=="paused"&&e.activate()}),this.onResume()},r.prototype.activate=function(){this.xhr.send(this.blob),this.status="active"},r.prototype.pause=function(){this.xhr.abort(),this.status="paused"},t}()})(this);
|
1
|
+
(function(e){e.S3MP=function(){function t(t){var r,i=[],s=this;_.extend(this,t),this.uploadList=[],this.handler={beginUpload:function(){function t(t,n){var r=n.key,i=n.parts.length;typeof e[r]=="undefined"&&(e[r]=0),e[r]++;if(e[r]===i){for(var o=0;o<t;o++)n.parts[o].activate();s.handler.startProgressTimer(r),s.onStart(n)}}var e=[];return t}(),onError:function(e,t){},onPartSuccess:function(e,t){var n,r,i;n=e.parts,i=t.xhr.getResponseHeader("ETag"),e.Etags.push({ETag:i.replace(/\"/g,""),partNum:t.num}),e.uploaded+=t.size,e.inprogress[t.num]=0,r=_.indexOf(n,t),n.splice(r,1),n.length&&(r=_.findIndex(n,function(e,t,n){if(e.status!=="active")return!0}),r!==-1&&n[r].activate()),n.length||this.onComplete(e)},onComplete:function(e){var t=_.indexOf(s.uploadList,e);this.clearProgressTimer(t),s.completeMultipart(e,function(t){t.location&&s.onComplete(e)})},onProgress:function(e,t,n,r,i){s.onProgress(e,t,n,r,i)},startProgressTimer:function(){var t=[],n=function(n){i[n]=e.setInterval(function(){var e,r,i,o,u;typeof t[n]=="undefined"&&(t[n]=0),e=s.uploadList[n],r=e.size,i=e.uploaded,_.each(e.inprogress,function(e){i+=e}),o=i/r*100,u=i-t[n],t[n]=i,e.handler.onProgress(n,r,i,o,u)},1e3)};return n}(),clearProgressTimer:function(t){e.clearInterval(i[t])}},this.fileSelector?r=$(this.fileSelector).get(0).files:r=this.fileList,_.each(r,function(e,t){if(e.size<5e6)return s.onError({name:"FileSizeError",message:"File size is too small"});var r=new n(e,s,t);s.uploadList.push(r),r.init()})}function n(e,t,n){function i(){var t,i,s,o,u,a,f,l,c;t=this,this.key=n,this.file=e,this.name=e.name,this.size=e.size,this.type=e.type,this.Etags=[],this.inprogress=[],this.uploaded=0,this.status="",this.size>1e9?(num_segs=100,l=10):this.size>5e8?(num_segs=50,l=5):this.size>1e8?(num_segs=20,l=5):(num_segs=2,l=2),a=_.range(num_segs+1),f=_.map(a,function(t){return Math.round(t*(e.size/num_segs))}),t.sliceBlob=="Unsupported"?this.parts=[new r(e,0,t)]:(this.parts=_.map(f,function(n,i){return c=t.sliceBlob(e,n,f[i+1]),new r(c,i+1,t)}),this.parts.pop()),this.init=function(){t.initiateMultipart(t,function(e){var n=t.id=e.id,r=t.upload_id=e.upload_id,i=t.object_name=e.key,s=t.parts;t.signPartRequests(n,i,r,s,function(e){_.each(s,function(n,s){var o=n.xhr;o.open("PUT","http://"+t.bucket+".s3.amazonaws.com/"+i+"?partNumber="+n.num+"&uploadId="+r,!0),o.setRequestHeader("x-amz-date",e[s].date),o.setRequestHeader("Authorization",e[s].authorization),t.handler.beginUpload(l,t)})})})}}return i.prototype=t,new i}function r(e,t,n){var r,i;r=this,this.size=e.size,this.blob=e,this.num=t,this.xhr=i=n.createXhrRequest(),i.onload=function(){n.handler.onPartSuccess(n,r)},i.onerror=function(){n.handler.onError(n,r)},i.upload.onprogress=_.throttle(function(e){n.inprogress[t]=e.loaded},1e3)}return _.mixin({findIndex:function(e,t){for(var n=0;n<e.length;n++)if(t(e[n],n,e))return n;return-1}}),t.prototype.initiateMultipart=function(e,t){var n,r,i;n="/s3_multipart/uploads",r=JSON.stringify({object_name:e.name,content_type:e.type,content_size:e.size,uploader:$(this.fileInputElement).data("uploader")}),i=this.createXhrRequest("POST",n),this.deliverRequest(i,r,t)},t.prototype.signPartRequests=function(e,t,n,r,i){var s,o,u,a;s=_.reduce(_.rest(r),function(e,t){return e+"-"+t.size},r[0].size),o="s3_multipart/uploads/"+e,u=JSON.stringify({object_name:t,upload_id:n,content_lengths:s}),a=this.createXhrRequest("PUT",o),this.deliverRequest(a,u,i)},t.prototype.completeMultipart=function(e,t){var n,r,i;n="s3_multipart/uploads/"+e.id,r=JSON.stringify({object_name:e.object_name,upload_id:e.upload_id,content_length:e.size,parts:e.Etags}),i=this.createXhrRequest("PUT",n),this.deliverRequest(i,r,t)},t.prototype.deliverRequest=function(e,t,n){var r=this;e.onload=function(){response=JSON.parse(this.responseText);if(response.error)return r.onError({name:"ServerResponse",message:"The server responded with an error"});n(response)},e.onerror=function(){},e.setRequestHeader("Content-Type","application/json"),e.setRequestHeader("X-CSRF-Token",$('meta[name="csrf-token"]').attr("content")),e.send(t)},t.prototype.createXhrRequest=function(){var e;return typeof XMLHttpRequest.constructor=="function"?e=XMLHttpRequest:typeof XDomainRequest!="undefined"?e=XDomainRequest:e=null,function(t,n,r,i){var s,o,i=!0;return s=Array.prototype.slice.call(arguments),typeof s[0]=="undefined"&&(r=null,i=!1),o=new e,i&&o.open(t,n,!0),o.onreadystatechange=r,o}}(),t.prototype.sliceBlob=function(){try{var e=new Blob}catch(t){return"Unsupported"}return e.slice?function(e,t,n){return e.slice(t,n)}:e.mozSlice?function(e,t,n){return e.mozSlice(t,n)}:e.webkitSlice?function(e,t,n){return e.webkitSlice(t,n)}:"Unsupported"}(),t.prototype._returnUploadObj=function(e){var t=_.find(this.uploadList,function(t){return t.key===e});return t},t.prototype.cancel=function(e){var t,n;t=this._returnUploadObj(e),n=_.indexOf(this.uploadList,t),this.uploadList.splice(n,n+1),this.onCancel()},t.prototype.pause=function(e){var t=this._returnUploadObj(e);_.each(t.parts,function(e,t,n){e.status=="active"&&e.pause()}),this.onPause()},t.prototype.resume=function(e){var t=this._returnUploadObj(e);_.each(t.parts,function(e,t,n){e.status=="paused"&&e.activate()}),this.onResume()},r.prototype.activate=function(){this.xhr.send(this.blob),this.status="active"},r.prototype.pause=function(){this.xhr.abort(),this.status="paused"},t}()})(this);
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: s3_multipart
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0.
|
4
|
+
version: 0.0.9
|
5
5
|
prerelease:
|
6
6
|
platform: ruby
|
7
7
|
authors:
|
@@ -9,7 +9,7 @@ authors:
|
|
9
9
|
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date: 2013-
|
12
|
+
date: 2013-03-06 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: uuid
|
@@ -297,7 +297,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
297
297
|
version: '0'
|
298
298
|
requirements: []
|
299
299
|
rubyforge_project:
|
300
|
-
rubygems_version: 1.8.
|
300
|
+
rubygems_version: 1.8.25
|
301
301
|
signing_key:
|
302
302
|
specification_version: 3
|
303
303
|
summary: Upload directly to S3 using multipart uploading
|