// console.log("Loading ShardedUpload..."); Spontaneous.ShardedUpload = (function($, S) { var dom = S.Dom; var upload_id = (new Date()).valueOf(); var Shard = new JS.Class({ initialize: function(uploader, index, blob) { this.uploader = uploader; this.index = index; this.blob = blob; this.progress = 0; this.size = this.blob.size; }, start: function() { var reader = new FileReader(); reader.onload = function(event) { this.compute_hash(reader.result); }.bind(this); reader.onerror = function(event) { console.error('error', event); }; reader.readAsArrayBuffer(this.blob); }, // callback from reader compute_hash: function(array_buffer) { var bytes = new Uint8Array(array_buffer); var sha = Crypto.SHA1(bytes); this.hash = sha this.begin_upload(); this.failure_count = 0; }, remote_path: function() { return S.Ajax.request_url(['shard', this.hash].join('/'), true); }, begin_upload: function() { // test for existance of shard on server S.Ajax.get(['/shard', this.hash].join('/'), this.status_complete.bind(this)); }, status_complete: function(data, status, xhr) { if (status === "success") { // we're actually done this.complete(); } else { this.upload(); } }, retry: function() { if (this.hash) { this.upload(); } else { this.start(); } }, upload: function() { // create the form and post it using the calculated hash // assigning the callbacks to myself. var form = new FormData(), path = S.Ajax.request_url(['/shard', this.hash].join('/'), true); form.append('file', this.blob); var xhr = new XMLHttpRequest(), upload = xhr.upload; xhr.open("POST", path, true); upload.onprogress = this.onprogress.bind(this); upload.onload = this.onload.bind(this); upload.onloadend = this.onloadend.bind(this); upload.onerror = this.onerror.bind(this); xhr.onreadystatechange = this.onreadystatechange.bind(this); this.started = (new Date()).valueOf(); xhr.send(form); }, complete: function() { this.blob = null; this.uploader.shard_complete(this); }, failed: function() { this.failure_count += 1; this.uploader.shard_failed(this); }, onprogress: function(event) { var progress = event.position; this.progress = progress; this.time = (new Date()).valueOf() - this.started; this.uploader.upload_progress(this); }, onload: function(event) { }, onloadend: function(event) { console.log('Shard#onloadend: shard upload complete', event); }, onreadystatechange: function(event) { var xhr = event.currentTarget; if (xhr.readyState == 4) { if (xhr.status === 200) { this.complete(); } else { this.failed(); } } }, onerror: function(event) { console.error('Shard#onerror: shard upload error', event); } }); var ShardedUpload = new JS.Class(Spontaneous.Upload, { slice_size: 524288, initialize: function(manager, target, file) { this.callSuper(); this.completed = []; this.failed = []; this.current = null; }, start: function() { this.started = (new Date()).valueOf(); this.start_with_index(0); }, start_with_index: function(index) { if (index < this.shard_count()) { var shard = new Shard(this, index, this.slice(index)); this.current = shard; shard.start(); } else { if (this.failed.length === 0) { this.finalize(); } else { var shard = this.failed.pop(); console.warn('retrying failed shard', shard) this.current = shard; shard.retry(); } } }, finalize: function() { var form = new FormData(); form.append('field', this.field_name); form.append('version', this.target_version); form.append('shards', this.hashes().join(',')); form.append('mime_type', this.mime_type()); form.append('filename', File.filename(this.file)); this.post(this.path(), form); }, path: function() { return ["/shard/replace", this.target_id].join('/'); }, hashes: function() { var hashes = []; for (var i = 0, ii = this.completed.length; i < ii; i++) { hashes.push(this.completed[i].hash); } return hashes; }, upload_progress: function(shard) { this.time = (new Date()).valueOf() - this.started; this.manager.upload_progress(this); }, position: function() { var _position = 0; for (var i = 0, cc = this.completed, ii = cc.length; i < ii; i++) { if (cc[i]) { // failed shards will leave a blank space _position += cc[i].size; } } _position += this.current.progress; _position = Math.min(_position, this.total()); return _position; }, mime_type: function() { return this.file.type; }, total: function() { return this.file.size; }, shard_complete: function(shard) { // update the progress // and launch the next shard this.manager.upload_progress(this); this.completed[shard.index] = shard; this.start_with_index(shard.index + 1); }, shard_failed: function(shard) { console.error('shard failed', shard, shard.index); this.failed.push(shard); this.start_with_index(shard.index + 1); }, shard_count: function() { return Math.ceil(this.file.size / this.slice_size); }, slice: function(n) { // file slicing methods have been normalised by compatibility.js return this.file.slice(n * this.slice_size, (n+1) * this.slice_size); } }); ShardedUpload.extend({ supported: function() { return ((typeof window.File.prototype.slice === 'function') && (typeof window.FileReader !== 'undefined') && (typeof window.Uint8Array !== 'undefined')); } }); return ShardedUpload; }(jQuery, Spontaneous));