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.
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));