SpontaneousCMS/spontaneous

View on GitHub
application/js/sharded_upload.js

Summary

Maintainability
D
1 day
Test Coverage
// 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('/'));
        },
        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('/'));
            form.append('file', this.blob);
            var xhr = S.Ajax.authenticatedRequest();
            var 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) {
            var shard;
            if (index < this.shard_count()) {
                shard = new Shard(this, index, this.slice(index));
                this.current = shard;
                shard.start();
            } else {
                if (this.failed.length === 0) {
                    this.finalize();
                } else {
                    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.request(this.method, this.path(), form);
        },
        path: function() {
            return ['/shard', this.target_id].join('/');
        },
        method: 'PUT',
        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));