From 845ef9bad6b9f5ff471b62505f9e39300297a3a4 Mon Sep 17 00:00:00 2001 From: Cheng-Han, Wu Date: Fri, 4 Mar 2016 23:17:35 +0800 Subject: Support export to and import from Google Drive --- public/js/google-drive-upload.js | 269 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 269 insertions(+) create mode 100644 public/js/google-drive-upload.js (limited to 'public/js/google-drive-upload.js') diff --git a/public/js/google-drive-upload.js b/public/js/google-drive-upload.js new file mode 100644 index 00000000..2e503af9 --- /dev/null +++ b/public/js/google-drive-upload.js @@ -0,0 +1,269 @@ +/** + * Helper for implementing retries with backoff. Initial retry + * delay is 1 second, increasing by 2x (+jitter) for subsequent retries + * + * @constructor + */ +var RetryHandler = function() { + this.interval = 1000; // Start at one second + this.maxInterval = 60 * 1000; // Don't wait longer than a minute +}; + +/** + * Invoke the function after waiting + * + * @param {function} fn Function to invoke + */ +RetryHandler.prototype.retry = function(fn) { + setTimeout(fn, this.interval); + this.interval = this.nextInterval_(); +}; + +/** + * Reset the counter (e.g. after successful request.) + */ +RetryHandler.prototype.reset = function() { + this.interval = 1000; +}; + +/** + * Calculate the next wait time. + * @return {number} Next wait interval, in milliseconds + * + * @private + */ +RetryHandler.prototype.nextInterval_ = function() { + var interval = this.interval * 2 + this.getRandomInt_(0, 1000); + return Math.min(interval, this.maxInterval); +}; + +/** + * Get a random int in the range of min to max. Used to add jitter to wait times. + * + * @param {number} min Lower bounds + * @param {number} max Upper bounds + * @private + */ +RetryHandler.prototype.getRandomInt_ = function(min, max) { + return Math.floor(Math.random() * (max - min + 1) + min); +}; + + +/** + * Helper class for resumable uploads using XHR/CORS. Can upload any Blob-like item, whether + * files or in-memory constructs. + * + * @example + * var content = new Blob(["Hello world"], {"type": "text/plain"}); + * var uploader = new MediaUploader({ + * file: content, + * token: accessToken, + * onComplete: function(data) { ... } + * onError: function(data) { ... } + * }); + * uploader.upload(); + * + * @constructor + * @param {object} options Hash of options + * @param {string} options.token Access token + * @param {blob} options.file Blob-like item to upload + * @param {string} [options.fileId] ID of file if replacing + * @param {object} [options.params] Additional query parameters + * @param {string} [options.contentType] Content-type, if overriding the type of the blob. + * @param {object} [options.metadata] File metadata + * @param {function} [options.onComplete] Callback for when upload is complete + * @param {function} [options.onProgress] Callback for status for the in-progress upload + * @param {function} [options.onError] Callback if upload fails + */ +var MediaUploader = function(options) { + var noop = function() {}; + this.file = options.file; + this.contentType = options.contentType || this.file.type || 'application/octet-stream'; + this.metadata = options.metadata || { + 'title': this.file.name, + 'mimeType': this.contentType + }; + this.token = options.token; + this.onComplete = options.onComplete || noop; + this.onProgress = options.onProgress || noop; + this.onError = options.onError || noop; + this.offset = options.offset || 0; + this.chunkSize = options.chunkSize || 0; + this.retryHandler = new RetryHandler(); + + this.url = options.url; + if (!this.url) { + var params = options.params || {}; + params.uploadType = 'resumable'; + this.url = this.buildUrl_(options.fileId, params, options.baseUrl); + } + this.httpMethod = options.fileId ? 'PUT' : 'POST'; +}; + +/** + * Initiate the upload. + */ +MediaUploader.prototype.upload = function() { + var self = this; + var xhr = new XMLHttpRequest(); + + xhr.open(this.httpMethod, this.url, true); + xhr.setRequestHeader('Authorization', 'Bearer ' + this.token); + xhr.setRequestHeader('Content-Type', 'application/json'); + xhr.setRequestHeader('X-Upload-Content-Length', this.file.size); + xhr.setRequestHeader('X-Upload-Content-Type', this.contentType); + + xhr.onload = function(e) { + if (e.target.status < 400) { + var location = e.target.getResponseHeader('Location'); + this.url = location; + this.sendFile_(); + } else { + this.onUploadError_(e); + } + }.bind(this); + xhr.onerror = this.onUploadError_.bind(this); + xhr.send(JSON.stringify(this.metadata)); +}; + +/** + * Send the actual file content. + * + * @private + */ +MediaUploader.prototype.sendFile_ = function() { + var content = this.file; + var end = this.file.size; + + if (this.offset || this.chunkSize) { + // Only bother to slice the file if we're either resuming or uploading in chunks + if (this.chunkSize) { + end = Math.min(this.offset + this.chunkSize, this.file.size); + } + content = content.slice(this.offset, end); + } + + var xhr = new XMLHttpRequest(); + xhr.open('PUT', this.url, true); + xhr.setRequestHeader('Content-Type', this.contentType); + xhr.setRequestHeader('Content-Range', "bytes " + this.offset + "-" + (end - 1) + "/" + this.file.size); + xhr.setRequestHeader('X-Upload-Content-Type', this.file.type); + if (xhr.upload) { + xhr.upload.addEventListener('progress', this.onProgress); + } + xhr.onload = this.onContentUploadSuccess_.bind(this); + xhr.onerror = this.onContentUploadError_.bind(this); + xhr.send(content); +}; + +/** + * Query for the state of the file for resumption. + * + * @private + */ +MediaUploader.prototype.resume_ = function() { + var xhr = new XMLHttpRequest(); + xhr.open('PUT', this.url, true); + xhr.setRequestHeader('Content-Range', "bytes */" + this.file.size); + xhr.setRequestHeader('X-Upload-Content-Type', this.file.type); + if (xhr.upload) { + xhr.upload.addEventListener('progress', this.onProgress); + } + xhr.onload = this.onContentUploadSuccess_.bind(this); + xhr.onerror = this.onContentUploadError_.bind(this); + xhr.send(); +}; + +/** + * Extract the last saved range if available in the request. + * + * @param {XMLHttpRequest} xhr Request object + */ +MediaUploader.prototype.extractRange_ = function(xhr) { + var range = xhr.getResponseHeader('Range'); + if (range) { + this.offset = parseInt(range.match(/\d+/g).pop(), 10) + 1; + } +}; + +/** + * Handle successful responses for uploads. Depending on the context, + * may continue with uploading the next chunk of the file or, if complete, + * invokes the caller's callback. + * + * @private + * @param {object} e XHR event + */ +MediaUploader.prototype.onContentUploadSuccess_ = function(e) { + if (e.target.status == 200 || e.target.status == 201) { + this.onComplete(e.target.response); + } else if (e.target.status == 308) { + this.extractRange_(e.target); + this.retryHandler.reset(); + this.sendFile_(); + } else { + this.onContentUploadError_(e); + } +}; + +/** + * Handles errors for uploads. Either retries or aborts depending + * on the error. + * + * @private + * @param {object} e XHR event + */ +MediaUploader.prototype.onContentUploadError_ = function(e) { + if (e.target.status && e.target.status < 500) { + this.onError(e.target.response); + } else { + this.retryHandler.retry(this.resume_.bind(this)); + } +}; + +/** + * Handles errors for the initial request. + * + * @private + * @param {object} e XHR event + */ +MediaUploader.prototype.onUploadError_ = function(e) { + this.onError(e.target.response); // TODO - Retries for initial upload +}; + +/** + * Construct a query string from a hash/object + * + * @private + * @param {object} [params] Key/value pairs for query string + * @return {string} query string + */ +MediaUploader.prototype.buildQuery_ = function(params) { + params = params || {}; + return Object.keys(params).map(function(key) { + return encodeURIComponent(key) + '=' + encodeURIComponent(params[key]); + }).join('&'); +}; + +/** + * Build the drive upload URL + * + * @private + * @param {string} [id] File ID if replacing + * @param {object} [params] Query parameters + * @return {string} URL + */ +MediaUploader.prototype.buildUrl_ = function(id, params, baseUrl) { + var url = baseUrl || 'https://www.googleapis.com/upload/drive/v2/files/'; + if (id) { + url += id; + } + var query = this.buildQuery_(params); + if (query) { + url += '?' + query; + } + return url; +}; + + + -- cgit v1.2.3