Source: backend/GoogleDrive.js

/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */

/** @module */

import {AbstractBackend, addBackend} from "./AbstractBackend";

/** Google Drive backend.
 *
 * @extends module:backend/AbstractBackend.AbstractBackend
 */
export class GoogleDrive extends AbstractBackend {

    /** Initialize a Sozi  backend based on the Google Drive API.
     *
     * @param {module:Controller.Controller} controller - A controller instance.
     * @param {HTMLElement} container - The element that will contain the menu for choosing a backend.
     */
    constructor(controller, container) {
        const _ = controller.gettext;

        super(controller, container, "sozi-editor-backend-GoogleDrive-input", _("Open an SVG file from Google Drive"));

        this.clickToAuth = () => this.authorize(false);

        gapi.client.setApiKey(GoogleDrive.apiKey);
        this.authorize(true);
    }

    /** @inheritdoc */
    openFileChooser() {
        this.picker.setVisible(true);
    }

    /** Authorize access to the Google Drive API.
     *
     * @private
     * @param {boolean} onInit - `true` if this is the first authorization request in this session.
     */
    authorize(onInit) {
        gapi.auth.authorize({
            client_id: GoogleDrive.clientId,
            scope: "https://www.googleapis.com/auth/drive",
            immediate: onInit
        }, authResult => this.onAuthResult(onInit, authResult));
    }

    /** Process a Google Drive API authorization result.
     *
     * Called on completion of the authorization request.
     *
     * @private
     * @param {boolean} onInit - `true` if this is the first authorization request in this session.
     * @param {object} authResult - The authorization result.
     */
    onAuthResult(onInit, authResult) {
        const inputButton = document.getElementById("sozi-editor-backend-GoogleDrive-input");

        if (authResult && !authResult.error) {
            this.accessToken = authResult.access_token;
            // Access granted: create a file picker and show the "Load" button.
            gapi.client.load("drive", "v2");
            gapi.load("picker", {
                callback: () => {
                    this.createPicker();
                    inputButton.removeEventListener("click", this.clickToAuth);
                    inputButton.addEventListener("click", () => this.openFileChooser());
                    if (!onInit) {
                        this.openFileChooser();
                    }
                }
            });
        }
        else {
            // No access token could be retrieved, show the button to start the authorization flow.
            inputButton.addEventListener("click", this.clickToAuth);
        }
    }

    /** Create a Google Drive file picker.
     *
     * @private
     */
    createPicker() {
        const view = new google.picker.View(google.picker.ViewId.DOCS);
        view.setMimeTypes("image/svg+xml");

        this.picker = new google.picker.PickerBuilder().
            addView(view).
            setOAuthToken(this.accessToken).
            setCallback(data => {
                if (data[google.picker.Response.ACTION] === google.picker.Action.PICKED) {
                    gapi.client.drive.files.get({fileId: data.docs[0].id}).execute(response => {
                        if (!response.error) {
                            this.controller.storage.setSVGFile(response, this);
                        }
                        else {
                            console.log(response.error.message);
                        }
                    });
                }
            }).
            build();
    }

    /** @inheritdoc */
    getName(fileDescriptor) {
        return fileDescriptor.title;
    }

    /** @inheritdoc */
    getLocation(fileDescriptor) {
        return fileDescriptor.parents;
    }

    /** @inheritdoc */
    sameFile(fd1, fd2) {
        return fd1.id === fd2.id;
    }

    /** @inheritdoc */
    find(name, location) {
        return new Promise((resolve, reject) => {
            function findInParent(index) {
                gapi.client.drive.files.list({
                    q: "title = '" + name + "' and " +
                       "'" + location[index].id + "' in parents"
                }).execute(response => {
                    if (response.items && response.items.length) {
                        resolve(response.items[0]);
                    }
                    else if (index < location.length - 1) {
                        findInParent(index + 1);
                    }
                    else {
                        reject("Not found");
                    }
                });
            }
            findInParent(0);
        });
    }

    /** @inheritdoc */
    load(fileDescriptor) {
        // TODO implement the "change" event
        // The file is loaded using an AJAX GET operation.
        // The data type is forced to "text" to prevent parsing it.
        const xhr = new XMLHttpRequest();
        xhr.open("GET", fileDescriptor.downloadUrl);
        xhr.setRequestHeader("Content-Type", fileDescriptor.mimeType);
        xhr.setRequestHeader("Authorization", "Bearer " + this.accessToken);
        return new Promise((resolve, reject) => {
            xhr.addEventListener("readystatechange", () => {
                if (xhr.readyState === 4) {
                    if (xhr.status === 200) {
                        resolve(xhr.responseText);
                    }
                    else {
                        reject(xhr.status);
                    }
                }
            });
            xhr.send();
        });
    }

    /** @inheritdoc */
    create(name, location, mimeType, data) {
        const boundary = "-------314159265358979323846";
        const delimiter = "\r\n--" + boundary + "\r\n";
        const closeDelimiter = "\r\n--" + boundary + "--";

        const metadata = {
            title: name,
            parents: location,
            mimeType
        };

        const multipartRequestBody =
            delimiter +
            "Content-Type: application/json\r\n\r\n" + JSON.stringify(metadata) +
            delimiter +
            "Content-Type: " + mimeType + "\r\n" +
            "Content-Transfer-Encoding: base64\r\n\r\n" +
            toBase64(data) + // Force UTF-8 encoding
            closeDelimiter;

        return new Promise((resolve, reject) => {
            gapi.client.request({
                path: "/upload/drive/v2/files",
                method: "POST",
                params: {
                    uploadType: "multipart"
                },
                headers: {
                  "Content-Type": 'multipart/mixed; boundary="' + boundary + '"'
                },
                body: multipartRequestBody
            }).execute(response => {
                if (response.error) {
                    reject(response.error.message);
                }
                else {
                    resolve(response);
                }
            });
        });
    }

    /** @inheritdoc */
    save(fileDescriptor, data) {
        const base64Data = toBase64(data); // Force UTF-8 encoding
        return new Promise((resolve, reject) => {
            gapi.client.request({
                path: "/upload/drive/v2/files/" + fileDescriptor.id,
                method: "PUT",
                params: {
                    uploadType: "media"
                },
                headers: {
                    "Content-Type": fileDescriptor.mimeType,
                    "Content-Length": base64Data.length,
                    "Content-Encoding": "base64"
                },
                body: base64Data
            }).execute(response => {
                if (response.error) {
                    reject(response.error.message);
                }
                else {
                    this.controller.storage.onSave(fileDescriptor);
                    resolve(fileDescriptor);
                }
            });
        });
    }
}

/** Encode data to base64.
 *
 * @private
 * @param {string} data - The data to encode.
 * @returns {string} The encoded data.
 */
function toBase64(data) {
    return btoa(unescape(encodeURIComponent(data)));
}

/** The Google Drive OAuth cliend Id.
 *
 * Override the value of this attribute in `GoogleDrive.config.js`.
 *
 * @static
 * @type {string} */
GoogleDrive.clientId = "Your OAuth client Id";

/** The Google Drive API key.
 *
 * Override the value of this attribute in `GoogleDrive.config.js`.
 *
 * @static
 * @type {string} */
GoogleDrive.apiKey = "Your developer API key";

addBackend(GoogleDrive);