Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Save plcgi1/83e34054310391d9f041f8c297c96e36 to your computer and use it in GitHub Desktop.
Save plcgi1/83e34054310391d9f041f8c297c96e36 to your computer and use it in GitHub Desktop.

Revisions

  1. @DrPaulBrewer DrPaulBrewer revised this gist Jan 23, 2018. 1 changed file with 3 additions and 1 deletion.
    4 changes: 3 additions & 1 deletion UploaderForGoogleDrive.js
    Original file line number Diff line number Diff line change
    @@ -140,7 +140,9 @@ window.UploaderForGoogleDrive = (function(){
    params.uploadType = 'resumable';
    this.url = this.buildUrl_(options.fileId, params, options.baseUrl);
    }
    this.httpMethod = options.fileId ? 'PUT' : 'POST';
    // PJB 2018.01.22 Change "update/replace" method from "PUT" to "PATCH"
    // per https://developers.google.com/drive/v3/reference/files/update
    this.httpMethod = options.fileId ? 'PATCH' : 'POST';
    };

    /**
  2. @DrPaulBrewer DrPaulBrewer revised this gist Nov 7, 2017. 1 changed file with 23 additions and 2 deletions.
    25 changes: 23 additions & 2 deletions UploaderForGoogleDrive.js
    Original file line number Diff line number Diff line change
    @@ -11,7 +11,10 @@
    // 2. wrap code in a "use strict" IIFE, only exposing MediaUploader
    // 3. explicitly export MediaUploader as window.UploaderForGoogleDrive
    // 4. if options.token undefined, get access token from existing window.gapi instance, if any

    //
    // Nov. 7, 2017
    // 5. Change file metadata "title" to "name" in line with Drive API v2-->v3 migration advisory
    // 6. export promise wrapper as window.pUploaderForGoogleDrive

    // jshint browser:true, strict:true

    @@ -101,7 +104,7 @@ window.UploaderForGoogleDrive = (function(){
    this.file = options.file;
    this.contentType = options.contentType || this.file.type || 'application/octet-stream';
    this.metadata = options.metadata || {
    'title': this.file.name,
    'name': this.file.name,
    'mimeType': this.contentType
    };
    // if options.token omitted, get access_token from existing window.gapi instance, if any : PJB 2017-11-01
    @@ -310,3 +313,21 @@ window.UploaderForGoogleDrive = (function(){
    return MediaUploader;

    })();

    window.pUploaderForGoogleDrive = function(options){
    "use strict";
    return new Promise(function(resolve, reject){
    options.onComplete = function(r){
    var response;
    try {
    response = (typeof(r)==='string')? JSON.parse(r): r;
    } catch(e){
    response = r;
    }
    resolve(response);
    };
    options.onError = function(e){ reject(e); };
    var uploader = new window.UploaderForGoogleDrive(options);
    uploader.upload();
    });
    };
  3. @DrPaulBrewer DrPaulBrewer created this gist Nov 2, 2017.
    312 changes: 312 additions & 0 deletions UploaderForGoogleDrive.js
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,312 @@
    // upload.js, from https://github.com/googledrive/cors-upload-sample
    // Contributors Steve Bazyl, Mike Procopio, Jeffrey Posnick, Renaud Sauvain
    // License: Apache 2.0 http://www.apache.org/licenses/LICENSE-2.0
    //
    // Implements Resumable Upload for Google Drive as described by
    // https://developers.google.com/drive/v3/web/resumable-upload
    //
    // Modified by Paul Brewer, Economic and Financial Technology Consulting LLC
    // Nov. 1 2017
    // 1. use Google Drive API V3 instead of V2
    // 2. wrap code in a "use strict" IIFE, only exposing MediaUploader
    // 3. explicitly export MediaUploader as window.UploaderForGoogleDrive
    // 4. if options.token undefined, get access token from existing window.gapi instance, if any


    // jshint browser:true, strict:true

    window.UploaderForGoogleDrive = (function(){
    "use strict";

    /**
    * 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
    };
    // if options.token omitted, get access_token from existing window.gapi instance, if any : PJB 2017-11-01
    // see https://developers.google.com/api-client-library/javascript/reference/referencedocs
    if (options.token){
    this.token = options.token;
    } else {
    try {
    this.token = (window.gapi
    .auth2
    .getAuthInstance()
    .currentUser
    .get()
    .getAuthResponse(true)
    .access_token
    );
    } catch(e){
    console.log(e);
    throw new Error("Uploader: missing Google OAuth2 access_token");
    }
    }
    // end access_token patch -- PJB 2017-11-01
    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) {
    // modified next line to use v3, not v2 -- PJB, 2017-11-01
    var url = baseUrl || 'https://www.googleapis.com/upload/drive/v3/files/';
    if (id) {
    url += id;
    }
    var query = this.buildQuery_(params);
    if (query) {
    url += '?' + query;
    }
    return url;
    };


    return MediaUploader;

    })();