s3_multipart 0.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/.gitignore +6 -0
- data/.rspec +1 -0
- data/Gemfile +26 -0
- data/Gemfile.lock +177 -0
- data/LICENSE.txt +20 -0
- data/README.md +106 -0
- data/Rakefile +0 -0
- data/app/controllers/s3_multipart/application_controller.rb +4 -0
- data/app/controllers/s3_multipart/uploads_controller.rb +56 -0
- data/app/models/s3_multipart/upload.rb +10 -0
- data/config/routes.rb +3 -0
- data/db/migrate/20110727184726_create_s3_multipart_uploads.rb +12 -0
- data/lib/s3_multipart.rb +55 -0
- data/lib/s3_multipart/action_view_helpers/form_helper.rb +15 -0
- data/lib/s3_multipart/engine.rb +5 -0
- data/lib/s3_multipart/http/net_http.rb +49 -0
- data/lib/s3_multipart/railtie.rb +13 -0
- data/lib/s3_multipart/uploader.rb +85 -0
- data/lib/s3_multipart/uploader/config.rb +15 -0
- data/lib/s3_multipart/version.rb +3 -0
- data/s3_multipart-0.0.1.gem +0 -0
- data/s3_multipart.gemspec +27 -0
- data/spec/integration/uploads_controller_spec.rb +9 -0
- data/spec/internal/app/assets/javascripts/application.js +91 -0
- data/spec/internal/app/assets/javascripts/jquery.js +2 -0
- data/spec/internal/app/assets/javascripts/underscore.js +1 -0
- data/spec/internal/app/assets/stylesheets/application.css +0 -0
- data/spec/internal/app/controllers/application_controller.rb +3 -0
- data/spec/internal/app/controllers/pages_controller.rb +9 -0
- data/spec/internal/app/views/layouts/application.html.erb +17 -0
- data/spec/internal/app/views/pages/upload.html.erb +21 -0
- data/spec/internal/config/database.yml +4 -0
- data/spec/internal/config/routes.rb +4 -0
- data/spec/internal/db/combustion_test.sqlite3 +0 -0
- data/spec/internal/db/schema.rb +14 -0
- data/spec/internal/log/.gitignore +1 -0
- data/spec/internal/public/favicon.ico +0 -0
- data/spec/requests/uploader_spec.rb +48 -0
- data/spec/spec_helper.rb +18 -0
- data/vendor/assets/javascripts/s3_multipart/index.js +1 -0
- data/vendor/assets/javascripts/s3_multipart/s3_multipart.js +478 -0
- metadata +214 -0
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,18 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'bundler'
|
3
|
+
|
4
|
+
Bundler.require :development
|
5
|
+
|
6
|
+
require 'capybara/rspec'
|
7
|
+
|
8
|
+
Combustion.initialize!
|
9
|
+
|
10
|
+
require 'rspec/rails'
|
11
|
+
require 'capybara/rails'
|
12
|
+
|
13
|
+
# Engine config initializer
|
14
|
+
require 'setup_credentials.rb'
|
15
|
+
|
16
|
+
RSpec.configure do |config|
|
17
|
+
#config.use_transactional_fixtures = true
|
18
|
+
end
|
@@ -0,0 +1 @@
|
|
1
|
+
//=require s3_multipart
|
@@ -0,0 +1,478 @@
|
|
1
|
+
(function(global) {
|
2
|
+
global.S3MP = (function() {
|
3
|
+
|
4
|
+
// Wrap this into underscore library extension
|
5
|
+
_.mixin({
|
6
|
+
findIndex : function (collection, filter) {
|
7
|
+
for (var i = 0; i < collection.length; i++) {
|
8
|
+
if (filter(collection[i], i, collection)) {
|
9
|
+
return i;
|
10
|
+
}
|
11
|
+
}
|
12
|
+
return -1;
|
13
|
+
}
|
14
|
+
});
|
15
|
+
|
16
|
+
// S3MP Constructor
|
17
|
+
function S3MP(options) {
|
18
|
+
var files
|
19
|
+
, progress_timer = []
|
20
|
+
, S3MP = this;
|
21
|
+
|
22
|
+
// User defined options + callbacks
|
23
|
+
options = options || {
|
24
|
+
fileSelector: null,
|
25
|
+
bucket: null,
|
26
|
+
onStart: function(num) {
|
27
|
+
console.log("File "+num+" has started uploading")
|
28
|
+
},
|
29
|
+
onComplete: function(num) {
|
30
|
+
console.log("File "+num+" successfully uploaded")
|
31
|
+
},
|
32
|
+
onPause: function(num) {
|
33
|
+
console.log("File "+num+" has been paused")
|
34
|
+
},
|
35
|
+
onCancel: function(num) {
|
36
|
+
console.log("File upload "+num+" was canceled")
|
37
|
+
},
|
38
|
+
onError: function(num) {
|
39
|
+
console.log("There was an error")
|
40
|
+
},
|
41
|
+
onProgress: function(num, size, done, percent, speed) {
|
42
|
+
console.log("File %d is %f percent done (%f of %f total) and uploading at %s", num, percent, done, size, speed);
|
43
|
+
}
|
44
|
+
}
|
45
|
+
_.extend(this, options);
|
46
|
+
|
47
|
+
this.uploadList = [];
|
48
|
+
|
49
|
+
// Handles all of the user input, success/failure events, and
|
50
|
+
// progress notifiers
|
51
|
+
this.handler = {
|
52
|
+
|
53
|
+
// utility function to return an upload object given a file
|
54
|
+
_returnUploadObj: function(file) {
|
55
|
+
var uploadObj = _.find(S3MP.fileList, function(uploadObj) {
|
56
|
+
return uploadObj.file === file;
|
57
|
+
});
|
58
|
+
return uploadObj;
|
59
|
+
},
|
60
|
+
|
61
|
+
// Activate an appropriate number of parts (number = pipes)
|
62
|
+
// when all of the parts have been successfully initialized
|
63
|
+
beginUpload: function() {
|
64
|
+
var i = [];
|
65
|
+
function beginUpload(pipes, uploadObj) {
|
66
|
+
var key = uploadObj.key
|
67
|
+
, num_parts = uploadObj.parts.length
|
68
|
+
|
69
|
+
if (typeof i[key] === "undefined") {
|
70
|
+
i[key] = 0;
|
71
|
+
}
|
72
|
+
|
73
|
+
i[key]++;
|
74
|
+
|
75
|
+
if (i[key] === num_parts) {
|
76
|
+
for (var j=0; j<pipes; j++) {
|
77
|
+
uploadObj.parts[j].activate();
|
78
|
+
}
|
79
|
+
S3MP.handler.startProgressTimer(key);
|
80
|
+
S3MP.onStart(key); // This probably needs to go somewhere else.
|
81
|
+
}
|
82
|
+
}
|
83
|
+
return beginUpload;
|
84
|
+
}(),
|
85
|
+
|
86
|
+
// cancel a given file upload
|
87
|
+
cancel: function(file) {
|
88
|
+
var uploadObj, i;
|
89
|
+
|
90
|
+
uploadObj = this._returnUploadObj(file);
|
91
|
+
i = _.indexOf(S3MP.uploadList, uploadObj);
|
92
|
+
|
93
|
+
S3MP.uploadList.splice(i,i+1);
|
94
|
+
S3MP.onCancel();
|
95
|
+
},
|
96
|
+
|
97
|
+
// pause a given file upload
|
98
|
+
pause: function(file) {
|
99
|
+
var uploadObj = this._returnUploadObj(file);
|
100
|
+
|
101
|
+
_.each(uploadObj.activeParts, function(part, key, list) {
|
102
|
+
part.xhr.abort();
|
103
|
+
});
|
104
|
+
|
105
|
+
S3MP.onPause();
|
106
|
+
},
|
107
|
+
|
108
|
+
// called when an upload is paused or the network connection cuts out
|
109
|
+
onError: function(uploadObj, part) {
|
110
|
+
// To-do
|
111
|
+
},
|
112
|
+
|
113
|
+
// called when a single part has successfully uploaded
|
114
|
+
onPartSuccess: function(uploadObj, finished_part) {
|
115
|
+
var parts, i, ETag;
|
116
|
+
|
117
|
+
parts = uploadObj.parts;
|
118
|
+
|
119
|
+
// Append the ETag (in the response header) to the ETags array
|
120
|
+
ETag = finished_part.xhr.getResponseHeader("ETag");
|
121
|
+
uploadObj.Etags.push({ ETag: ETag.replace(/\"/g, ''), partNum: finished_part.num });
|
122
|
+
|
123
|
+
// Increase the uploaded count and delete the finished part
|
124
|
+
uploadObj.uploaded += finished_part.size;
|
125
|
+
uploadObj.inprogress[finished_part.num] = 0;
|
126
|
+
i = _.indexOf(parts, finished_part);
|
127
|
+
parts.splice(i,1);
|
128
|
+
|
129
|
+
// activate one of the remaining parts
|
130
|
+
if (parts.length) {
|
131
|
+
i = _.findIndex(parts, function(el, index, collection) {
|
132
|
+
if (el.status !== "active") {
|
133
|
+
return true;
|
134
|
+
}
|
135
|
+
});
|
136
|
+
if (i !== -1){
|
137
|
+
parts[i].activate();
|
138
|
+
}
|
139
|
+
}
|
140
|
+
|
141
|
+
// If no parts remain then the upload has finished
|
142
|
+
if (!parts.length) {
|
143
|
+
this.onComplete(uploadObj);
|
144
|
+
}
|
145
|
+
},
|
146
|
+
|
147
|
+
// called when all parts have successfully uploaded
|
148
|
+
onComplete: function(uploadObj) {
|
149
|
+
var key = _.indexOf(S3MP.uploadList, uploadObj);
|
150
|
+
|
151
|
+
// Stop the onprogress timer
|
152
|
+
this.clearProgressTimer(key);
|
153
|
+
|
154
|
+
// Tell the server to put together the pieces
|
155
|
+
S3MP.completeMultipart(uploadObj, function(obj) {
|
156
|
+
// Notify the client that the upload has succeeded when we
|
157
|
+
// get confirmation from the server
|
158
|
+
if (obj.location) {
|
159
|
+
S3MP.onComplete(key);
|
160
|
+
}
|
161
|
+
});
|
162
|
+
|
163
|
+
},
|
164
|
+
|
165
|
+
// Called by progress_timer
|
166
|
+
onProgress: function(key, size, done, percent, speed) {
|
167
|
+
S3MP.onProgress(key, size, done, percent, speed);
|
168
|
+
},
|
169
|
+
|
170
|
+
startProgressTimer: function() {
|
171
|
+
var last_upload_chunk = [];
|
172
|
+
var fn = function(key) {
|
173
|
+
progress_timer[key] = global.setInterval(function() {
|
174
|
+
var upload, size, done, percent, speed;
|
175
|
+
|
176
|
+
if (typeof last_upload_chunk[key] === "undefined") {
|
177
|
+
last_upload_chunk[key] = 0;
|
178
|
+
}
|
179
|
+
|
180
|
+
upload = S3MP.uploadList[key];
|
181
|
+
size = upload.size;
|
182
|
+
done = upload.uploaded;
|
183
|
+
|
184
|
+
_.each(upload.inprogress,function(val) {
|
185
|
+
done += val;
|
186
|
+
});
|
187
|
+
|
188
|
+
percent = done/size * 100;
|
189
|
+
speed = done - last_upload_chunk[key];
|
190
|
+
last_upload_chunk[key] = done;
|
191
|
+
|
192
|
+
upload.handler.onProgress(key, size, done, percent, speed);
|
193
|
+
}, 1000);
|
194
|
+
};
|
195
|
+
return fn;
|
196
|
+
}(),
|
197
|
+
|
198
|
+
clearProgressTimer: function(key) {
|
199
|
+
global.clearInterval(progress_timer[key]);
|
200
|
+
}
|
201
|
+
|
202
|
+
};
|
203
|
+
|
204
|
+
files = $(this.fileSelector).get(0).files;
|
205
|
+
|
206
|
+
_.each(files, function(file, key) {
|
207
|
+
//Do validation for each file before creating a new upload object
|
208
|
+
// if (!file.type.match(/video/)) {
|
209
|
+
// return false;
|
210
|
+
// }
|
211
|
+
if (file.size < 5000000) {
|
212
|
+
return false;
|
213
|
+
}
|
214
|
+
|
215
|
+
var upload = new Upload(file, S3MP, key);
|
216
|
+
S3MP.uploadList.push(upload);
|
217
|
+
upload.init();
|
218
|
+
});
|
219
|
+
|
220
|
+
};
|
221
|
+
|
222
|
+
// Upload constructor
|
223
|
+
function Upload(file, o, key) {
|
224
|
+
function Upload() {
|
225
|
+
var upload, id, parts, part, segs, chunk_segs, chunk_lens, pipes, blob;
|
226
|
+
|
227
|
+
upload = this;
|
228
|
+
|
229
|
+
this.key = key;
|
230
|
+
this.file = file;
|
231
|
+
this.name = file.name;
|
232
|
+
this.size = file.size;
|
233
|
+
this.type = file.type;
|
234
|
+
this.Etags = [];
|
235
|
+
this.inprogress = [];
|
236
|
+
this.uploaded = 0;
|
237
|
+
this.status = "";
|
238
|
+
|
239
|
+
// Break the file into an appropriate amount of chunks
|
240
|
+
// This needs to be optimized for various browsers types/versions
|
241
|
+
if (this.size > 1000000000) { // size greater than 1gb
|
242
|
+
num_segs = 100;
|
243
|
+
pipes = 10;
|
244
|
+
} else if (this.size > 500000000) { // greater than 500mb
|
245
|
+
num_segs = 50;
|
246
|
+
pipes = 5;
|
247
|
+
} else if (this.size > 100000000) { // greater than 100 mb
|
248
|
+
num_segs = 20;
|
249
|
+
pipes = 5;
|
250
|
+
} else if (this.size > 10000000) { // greater than 10 mb
|
251
|
+
num_segs = 2;
|
252
|
+
pipes = 2;
|
253
|
+
} else { // Do not use multi-part uploader
|
254
|
+
num_segs = 10;
|
255
|
+
pipes = 3;
|
256
|
+
}
|
257
|
+
|
258
|
+
chunk_segs = _.range(num_segs + 1);
|
259
|
+
chunk_lens = _.map(chunk_segs, function(seg) {
|
260
|
+
return Math.round(seg * (file.size/num_segs));
|
261
|
+
});
|
262
|
+
|
263
|
+
this.parts = _.map(chunk_lens, function(len, i) {
|
264
|
+
blob = upload.sliceBlob(file, len, chunk_lens[i+1]);
|
265
|
+
return new UploadPart(blob, i+1, upload);
|
266
|
+
});
|
267
|
+
|
268
|
+
this.parts.pop(); // Remove the empty blob at the end of the array
|
269
|
+
|
270
|
+
// init function will initiate the multipart upload, sign all the parts, and
|
271
|
+
// start uploading some parts in parallel
|
272
|
+
this.init = function() {
|
273
|
+
upload.initiateMultipart(upload, function(obj) {
|
274
|
+
console.log('Multipart initiation successful');
|
275
|
+
var id = upload.id = obj.id
|
276
|
+
, upload_id = upload.upload_id = obj.upload_id
|
277
|
+
, object_name = upload.object_name = obj.name
|
278
|
+
, parts = upload.parts;
|
279
|
+
|
280
|
+
upload.signPartRequests(id, object_name, upload_id, parts, function(response) {
|
281
|
+
_.each(parts, function(part, key) {
|
282
|
+
var xhr = part.xhr;
|
283
|
+
|
284
|
+
xhr.open('PUT', 'http://'+upload.bucket+'.s3.amazonaws.com/'+object_name+'?partNumber='+part.num+'&uploadId='+upload_id, true);
|
285
|
+
xhr.setRequestHeader('x-amz-date', response[key].date);
|
286
|
+
xhr.setRequestHeader('Authorization', response[key].authorization);
|
287
|
+
|
288
|
+
// Notify handler that an xhr request has been opened
|
289
|
+
upload.handler.beginUpload(pipes, upload);
|
290
|
+
});
|
291
|
+
});
|
292
|
+
});
|
293
|
+
}
|
294
|
+
};
|
295
|
+
// Inherit the properties and prototype methods of the S3MP instance object
|
296
|
+
Upload.prototype = o;
|
297
|
+
return new Upload();
|
298
|
+
}
|
299
|
+
|
300
|
+
// Upload part constructor
|
301
|
+
function UploadPart(blob, key, upload) {
|
302
|
+
var part, xhr;
|
303
|
+
|
304
|
+
part = this;
|
305
|
+
|
306
|
+
this.size = blob.size;
|
307
|
+
this.blob = blob;
|
308
|
+
this.num = key;
|
309
|
+
|
310
|
+
this.xhr = xhr = upload.createXhrRequest();
|
311
|
+
xhr.onload = function() {
|
312
|
+
upload.handler.onPartSuccess(upload, part);
|
313
|
+
};
|
314
|
+
xhr.onerror = function() {
|
315
|
+
upload.handler.onError(upload, part);
|
316
|
+
};
|
317
|
+
xhr.upload.onprogress = _.throttle(function(e) {
|
318
|
+
upload.inprogress[key] = e.loaded;
|
319
|
+
}, 1000);
|
320
|
+
|
321
|
+
};
|
322
|
+
|
323
|
+
UploadPart.prototype.activate = function() {
|
324
|
+
this.status = "active";
|
325
|
+
this.xhr.send(this.blob);
|
326
|
+
};
|
327
|
+
|
328
|
+
S3MP.prototype.initiateMultipart = function(upload, cb) {
|
329
|
+
var url, body, request, response;
|
330
|
+
|
331
|
+
url = '/s3_multipart/uploads';
|
332
|
+
body = JSON.stringify({ object_name : upload.name,
|
333
|
+
content_type : upload.type
|
334
|
+
});
|
335
|
+
|
336
|
+
request = this.createXhrRequest('POST', url, function(xhr) {
|
337
|
+
if (this.readyState !== 4) {
|
338
|
+
return false;
|
339
|
+
}
|
340
|
+
if (this.status !== 200) {
|
341
|
+
throw {
|
342
|
+
name: "ServerResponse",
|
343
|
+
message: "The server responded with an error"
|
344
|
+
};
|
345
|
+
}
|
346
|
+
|
347
|
+
response = JSON.parse(this.responseText);
|
348
|
+
cb(response);
|
349
|
+
});
|
350
|
+
|
351
|
+
request.setRequestHeader('Content-Type', 'application/json');
|
352
|
+
request.setRequestHeader('X-CSRF-Token', $('meta[name="csrf-token"]').attr('content'));
|
353
|
+
|
354
|
+
request.send(body);
|
355
|
+
|
356
|
+
};
|
357
|
+
|
358
|
+
S3MP.prototype.signPartRequests = function(id, object_name, upload_id, parts, cb) {
|
359
|
+
var content_lengths, url, body, request, response;
|
360
|
+
|
361
|
+
content_lengths = _.reduce(_.rest(parts), function(memo, part) {
|
362
|
+
return memo + "-" + part.size;
|
363
|
+
}, parts[0].size);
|
364
|
+
|
365
|
+
url = "s3_multipart/uploads/"+id;
|
366
|
+
body = JSON.stringify({ object_name : object_name,
|
367
|
+
upload_id : upload_id,
|
368
|
+
content_lengths : content_lengths
|
369
|
+
});
|
370
|
+
|
371
|
+
request = this.createXhrRequest('PUT', url, function(xhr) {
|
372
|
+
if (this.readyState !== 4) {
|
373
|
+
// Retry this chunk and give an error message
|
374
|
+
return false;
|
375
|
+
}
|
376
|
+
if (this.status !== 200) {
|
377
|
+
// Retry this chunk and give an error message
|
378
|
+
return false;
|
379
|
+
}
|
380
|
+
|
381
|
+
response = JSON.parse(this.responseText);
|
382
|
+
cb(response);
|
383
|
+
});
|
384
|
+
|
385
|
+
request.setRequestHeader('Content-Type', 'application/json');
|
386
|
+
request.setRequestHeader('X-CSRF-Token', $('meta[name="csrf-token"]').attr('content'));
|
387
|
+
|
388
|
+
request.send(body);
|
389
|
+
|
390
|
+
};
|
391
|
+
|
392
|
+
S3MP.prototype.completeMultipart = function(uploadObj, cb) {
|
393
|
+
var url, body, request, response;
|
394
|
+
|
395
|
+
url = 's3_multipart/uploads/'+uploadObj.id;
|
396
|
+
body = JSON.stringify({ object_name : uploadObj.object_name,
|
397
|
+
upload_id : uploadObj.upload_id,
|
398
|
+
content_length : uploadObj.size,
|
399
|
+
parts : uploadObj.Etags
|
400
|
+
});
|
401
|
+
|
402
|
+
request = this.createXhrRequest('PUT', url, function(xhr) {
|
403
|
+
if (this.readyState !== 4) {
|
404
|
+
// Retry this chunk and give an error message
|
405
|
+
return false;
|
406
|
+
}
|
407
|
+
if (this.status !== 200) {
|
408
|
+
// Retry this chunk and give an error message
|
409
|
+
return false;
|
410
|
+
}
|
411
|
+
|
412
|
+
response = JSON.parse(this.responseText);
|
413
|
+
cb(response);
|
414
|
+
})
|
415
|
+
|
416
|
+
request.setRequestHeader('Content-Type', 'application/json');
|
417
|
+
request.setRequestHeader('X-CSRF-Token', $('meta[name="csrf-token"]').attr('content'));
|
418
|
+
|
419
|
+
request.send(body);
|
420
|
+
};
|
421
|
+
|
422
|
+
S3MP.prototype.createXhrRequest = function() {
|
423
|
+
var xhrRequest;
|
424
|
+
|
425
|
+
// Sniff for xhr object
|
426
|
+
if (typeof XMLHttpRequest.constructor === "function") {
|
427
|
+
xhrRequest = XMLHttpRequest;
|
428
|
+
} else if (typeof XDomainRequest !== "undefined") {
|
429
|
+
xhrRequest = XDomainRequest;
|
430
|
+
} else {
|
431
|
+
xhrRequest = null; // Error out to the client
|
432
|
+
}
|
433
|
+
|
434
|
+
return function(method, url, cb, open) { // open defaults to true
|
435
|
+
var args, xhr, open = true;
|
436
|
+
|
437
|
+
args = Array.prototype.slice.call(arguments);
|
438
|
+
if (typeof args[0] === "undefined") {
|
439
|
+
cb = null;
|
440
|
+
open = false;
|
441
|
+
}
|
442
|
+
|
443
|
+
xhr = new xhrRequest();
|
444
|
+
if (open) { // open the request unless specified otherwise
|
445
|
+
xhr.open(method, url, true);
|
446
|
+
}
|
447
|
+
xhr.onreadystatechange = cb;
|
448
|
+
|
449
|
+
return xhr;
|
450
|
+
};
|
451
|
+
|
452
|
+
}();
|
453
|
+
|
454
|
+
S3MP.prototype.sliceBlob = function() {
|
455
|
+
var test_blob = new Blob();
|
456
|
+
|
457
|
+
if (test_blob.slice) {
|
458
|
+
return function(blob, start, end) {
|
459
|
+
return blob.slice(start, end);
|
460
|
+
}
|
461
|
+
} else if (test_blob.mozSlice) {
|
462
|
+
return function(blob, start, end) {
|
463
|
+
return blob.mozSlice(start, end);
|
464
|
+
}
|
465
|
+
} else if (test_blob.webkitSlice) {
|
466
|
+
return function(blob, start, end) {
|
467
|
+
return blob.webkitSlice(start, end);
|
468
|
+
}
|
469
|
+
} else {
|
470
|
+
throw new Error("File API not supported");
|
471
|
+
}
|
472
|
+
}();
|
473
|
+
|
474
|
+
return S3MP;
|
475
|
+
|
476
|
+
}());
|
477
|
+
|
478
|
+
}(this));
|