銀の弾丸

プログラミングに関して、いろいろ書き残していければと思っております。

WebアプリからGoogleドライブにファイルを保存する方法

f:id:takamints:20180729003439j:plain

前回記事の読み込みに引き続いて、WEBアプリからGoogleドライブにファイルを保存するサンプルコードを掲載します。

公式ガイドにWEBブラウザJavaScript向けサンプルコードは有りませんでした。 しかし前回懲りて学習しました。 XHRで直接APIを叩けばOK。 今回は、あまり停滞せずに0.5人日でなんとかなった。 いくつになってもお勉強です。

takamints.hatenablog.jp



ここに書いてる機能を、npmとしてまとめましたので是非どうぞー

takamints.hatenablog.jp


目次

シンプルアップロードを使います

Googleドライブへのファイルのアップロードには3つのやり方がありますが、ここで説明しているのは最も単純な「シンプルアップロード」という方法です。 他2つ「マルチパートアップロード」と「リジューマブルアップロード」には言及しません。

シンプルアップロードを使う条件は以下:

  • 通信が切断したりしてアップロードが失敗した場合に、再度アップロードするのが苦にならない程度にファイルが小さい。
  • メタデータを同時に送信しない。アップロードとは別リクエストでメタデータを送信するか、メタデータを利用しないならOK。

新規作成はAPIを二度叩く

シンプルアップロードでファイルを新規作成するのは以下の手順。

  1. ファイル名を指定してリソースを作成(API drive.files.create)し、ファイルIDを入手(この時点で中身が空のファイルが生成されている)
  2. 上で得たファイルIDで、コンテンツを上書きする(API drive.files.update)。

既存ファイルへの上書き保存は簡単です

一方、既にファイルIDが分かっているなら、上記2の「コンテンツの上書き」と同じことを行うだけです。

ということでコードはこちら

以下、少々長くなりますが、関数単位に分割してお送りします。

2つのクラス GdfsUi, Gdfs がありまして、以下の関連になっています。

f:id:takamints:20180729002042p:plain

GdfsUiクラスは、カレントディレクトリとそのファイルリストを管理してます。 GdfsクラスはGoogle Drive APIの低レベルインターフェースクラスです(APIに対して直接的)。

そのほか、OAuth2での認証取得やUIの生成については、以下のGitHubリポジトリを見てください。 API キーを http://localhost:8080/ に対して公開していますので、ローカルのウェブサーバーで動作確認できるはずです。

github.com

Googleドライブにファイルを保存する関数

指定された名称のファイルがファイルリスト中にあるなら上書き保存し、無ければ新規作成します。

/**
 * Create or overwrite a file to current directory.
 * @param {string} filename The file name.
 * @param {any} data The file content.
 * @param {string} contentType The content type.
 * @returns {Promise<object>} The response of update.
 */
GdfsUi.prototype.writeFile = async function (
    filename, data, contentType)
{
    // Find same file on current directory
    let fileIds = this._files.filter( file => {
        return file.name === filename;
    }).map( file => file.id );

    let fileId = (fileIds.length > 0 ? fileIds[0] : null);

    //Create new file
    if(fileId == null) {
        let response = await this._gdfs.createFile(
            this._folderId, filename, contentType);
        let file = JSON.parse(response);
        console.log(JSON.stringify(file, null, "    "));
        fileId = file.id;
    }

    // Write file
    return await this._gdfs.updateFile(
        fileId, data, contentType);
};

ファイルリソースを新規作成する関数

この関数は、ファイルリソースを作成します。 レスポンスに作成されたファイルのファイルIDが返されますので、 これを使ってファイルコンテンツを上書きできます。

/**
 * Create a new file's resource.
 * @param {string} folderId The folder id where the file is created.
 * @param {string} filename The file name.
 * @param {string} mimeType The mime type for the new file.
 * @returns {Promise<object>} The response of the API.
 */
Gdfs.prototype.createFile = function(folderId, filename, mimeType) {
    return this.requestWithAuth("POST",
        "https://www.googleapis.com/drive/v3/files", {},
        { "Content-Type": "application/json", },
        JSON.stringify({
            name: filename,
            mimeType: mimeType,
            parents: [folderId],
        }));
};

既存ファイルへコンテンツを上書きする関数

ファイルIDを指定して既存ファイルに上書きします。 APIガイドでは「既存ファイルの更新にはPUTメソッドを使え」と書いてありましたがPUTはエラーになりました。 drive.files.update APIのリファレンスを参照すると「PATCHメソッドだ」と書いてあった・・・ドナイヤネン。

/**
 * Upload a file content to update a existing file.
 * @param {string} fileId The file id to update.
 * @param {any} data The file content.
 * @param {string} contentType The content type of the file.
 * @returns {Promise<object>} The response of the API.
 */
Gdfs.prototype.updateFile = function(fileId, data, contentType) {
    return this.requestWithAuth("PATCH",
        "https://www.googleapis.com/upload/drive/v3/files/"+fileId,
        { uploadType: "media" },
        { "Content-Type": contentType },
        data);
};

共通関数

以下、上記パブリックメソッドから呼ばれている関数です。

XHRでGoogle Drive APIのリクエス

XHRで指定されたリクエストを行う関数。 認証情報(アクセストークン)をリクエストヘッダに設定します。

/**
 * @param {string} method The request method.
 * @param {string} endpoint The endpoint of API.
 * @param {object} queryParams The query parameters.
 * @param {object} headers The request headers.
 * @param {any} body The request body.
 * @returns {Promise<object>} The response of the request.
 */
Gdfs.prototype.requestWithAuth = function(
    method, endpoint, queryParams, headers, body)
{
    let xhr = new XMLHttpRequest();
    xhr.open(method, this.createUrl(endpoint, queryParams), true);
    headers = headers || {};
    Object.keys(headers).forEach( name => {
        xhr.setRequestHeader(name, headers[name]);
    });
    xhr.setRequestHeader("Authorization",
        "Bearer " + this.getAccessToken());
    xhr.timeout = 30000;
    return new Promise( (resolve, reject) => {
        xhr.onload = e => { resolve(xhr.responseText); };
        xhr.onerror = e => { reject(new Error(xhr.statusText)); };
        xhr.ontimeout = () => { reject(new Error("request timeout")); };
        xhr.send(body);
    });
};

アクセストークンを取得する

現セッションのアクセストークンを取得する。 gapiはDirve APIをロードしたときから使えるグローバルオブジェクト。

/**
 * Get access-token on current session.
 * @returns {string} The access token.
 */
Gdfs.prototype.getAccessToken = function() {
    let googleUser = gapi.auth2.getAuthInstance().currentUser.get();
    let authResponse = googleUser.getAuthResponse(true);
    let accessToken = authResponse.access_token;
    return accessToken;
};

APIのURLを作成する

エンドポイントとクエリパラメータからURLを生成。

/**
 * Create URI including query parameters.
 * @param {string} endpoint The endpoint of API.
 * @param {object|null} params The query parameters.
 * @returns {string} The URI.
 */
Gdfs.prototype.createUrl = function(endpoint, params) {
    if(params == null) {
        return endpoint;
    }
    let keys = Object.keys(params).filter(
        key => (key !== ""));
    if(keys.length == 0) {
        return endpoint;
    }
    let queryString = keys.map( key => {
        let value = params[key];
        return (value == null ? null : `${key}=${encodeURI(value)}`);
    }).join("&");
    return `${endpoint}?${queryString}`;
};