summaryrefslogtreecommitdiff
path: root/public/js/google-drive-upload.js
diff options
context:
space:
mode:
authorCheng-Han, Wu2016-03-04 23:17:35 +0800
committerCheng-Han, Wu2016-03-04 23:17:35 +0800
commit845ef9bad6b9f5ff471b62505f9e39300297a3a4 (patch)
tree8ee0bcd9a2b0ba22330d7ca4015c681bf4ad2bcf /public/js/google-drive-upload.js
parentc183002c14397c8b6e1ef26c1367197d987d7c62 (diff)
Support export to and import from Google Drive
Diffstat (limited to 'public/js/google-drive-upload.js')
-rw-r--r--public/js/google-drive-upload.js269
1 files changed, 269 insertions, 0 deletions
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;
+};
+
+
+