s3_multipart 0.0.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (42) hide show
  1. data/.gitignore +6 -0
  2. data/.rspec +1 -0
  3. data/Gemfile +26 -0
  4. data/Gemfile.lock +177 -0
  5. data/LICENSE.txt +20 -0
  6. data/README.md +106 -0
  7. data/Rakefile +0 -0
  8. data/app/controllers/s3_multipart/application_controller.rb +4 -0
  9. data/app/controllers/s3_multipart/uploads_controller.rb +56 -0
  10. data/app/models/s3_multipart/upload.rb +10 -0
  11. data/config/routes.rb +3 -0
  12. data/db/migrate/20110727184726_create_s3_multipart_uploads.rb +12 -0
  13. data/lib/s3_multipart.rb +55 -0
  14. data/lib/s3_multipart/action_view_helpers/form_helper.rb +15 -0
  15. data/lib/s3_multipart/engine.rb +5 -0
  16. data/lib/s3_multipart/http/net_http.rb +49 -0
  17. data/lib/s3_multipart/railtie.rb +13 -0
  18. data/lib/s3_multipart/uploader.rb +85 -0
  19. data/lib/s3_multipart/uploader/config.rb +15 -0
  20. data/lib/s3_multipart/version.rb +3 -0
  21. data/s3_multipart-0.0.1.gem +0 -0
  22. data/s3_multipart.gemspec +27 -0
  23. data/spec/integration/uploads_controller_spec.rb +9 -0
  24. data/spec/internal/app/assets/javascripts/application.js +91 -0
  25. data/spec/internal/app/assets/javascripts/jquery.js +2 -0
  26. data/spec/internal/app/assets/javascripts/underscore.js +1 -0
  27. data/spec/internal/app/assets/stylesheets/application.css +0 -0
  28. data/spec/internal/app/controllers/application_controller.rb +3 -0
  29. data/spec/internal/app/controllers/pages_controller.rb +9 -0
  30. data/spec/internal/app/views/layouts/application.html.erb +17 -0
  31. data/spec/internal/app/views/pages/upload.html.erb +21 -0
  32. data/spec/internal/config/database.yml +4 -0
  33. data/spec/internal/config/routes.rb +4 -0
  34. data/spec/internal/db/combustion_test.sqlite3 +0 -0
  35. data/spec/internal/db/schema.rb +14 -0
  36. data/spec/internal/log/.gitignore +1 -0
  37. data/spec/internal/public/favicon.ico +0 -0
  38. data/spec/requests/uploader_spec.rb +48 -0
  39. data/spec/spec_helper.rb +18 -0
  40. data/vendor/assets/javascripts/s3_multipart/index.js +1 -0
  41. data/vendor/assets/javascripts/s3_multipart/s3_multipart.js +478 -0
  42. metadata +214 -0
@@ -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));