☁️

フロントエンドJavascriptのみでCloud Storageにファイルをアップロードする

2022/03/13に公開

やりたいこと

タイトルの通り、フロントエンドのJavascriptのみを使って、GCP(Google Cloud Platform)のCloud Storageにファイルをアップロードしたい。

下調べ(読み飛ばしても大丈夫だと思います)

Cloud Storageは各種ライブラリが用意されています。しかし、どれもバックエンド、つまりサービス提供者にリソースのアクセス権限を与え、ファイル操作を行うライブラリです。例えば、バケットにオブジェクトをアップロードするライブラリは、こちらで確認できます。ライブラリは、Node.jsやPythonなどありますが、フロントエンドのJavascriptから、直接Cloud Storageにアップロードする方法は紹介されていません。ウェブで調べると、断片的に情報がありますが、認証からアップロードまでを一貫して紹介した記事は見つけられませんでした。そこで、今回の記事では、フロントエンドのJavascriptのみでOAuth2.0でアクセストークンを受け取り、Cloud Storageへファイルをアップロードするまでを行うプログラムを紹介します。

実装方針

Cloud Storageでは、Cloud Storageを操作するためのREST APIが用意されています。今回は、このREST APIを利用します。

認証

OAuth2.0を使います。

API

REST APIにはJSON APIとXML APIが用意されていますが、今回はJSON APIを利用します。

実装

ベースとなる参考記事はGCPの公式リファレンスです[1]

認証(OAuth2)

このsection「認証」のゴールは、Cloud Storageを操作するために必要なaccess tokenを取得することです。 そのために、Google OAuth2.0を使って認証を行います。認証に必要なソースコードは『OAuth 2.0 for Client-side Web Applications』[2]に記載されています。書くソースコードの量は大したこと無いです(ありがたい…)。
この記事を参考に、実装していきます。

認証のコードを書きたくない場合(とりあえずアクセストークンが欲しい場合)

もしも、認証のコードを書きたくないということであれば、OAuth2.0 Playgroundからアクセストークンを取得することも可能です。OAuth2.0 Playgroundを使ったアクセストークンの具体的な発行方法は、リファレンス[1:1]を参照してください。

認証情報の準備

Cloud Storageを使うために必要な認証情報を取得します。必要な認証情報は

  • client id
  • redirect URL
  • scope
    の3つです。

Google Cloud Platformの「APIとサービス」から、「ライブラリ」を選択して、Cloud Storageを追加します。認証情報で、「認証情報を作成」を選択して、OAuth2.0 クライアントIDを作成してください。ここで得られたクライアントIDが必要になります。また、OAuth同意画面も設定してください。scopeの設定はこちらを参考にしてください。
詳しくは、『Cloud Storageの認証』[1:2]をご覧ください。

アクセストークンの取得(ソースコードと動作)

ここでは、アクセストークンを取得するだけのソースコードを書きます。
このソースコードで出来るのは、ユーザーが「Get Access Token」をクリックしたら、Googleの認証画面にジャンプして、操作すれば、Access Tokenを取得できるというものです。

ソースコード

index.html
<!DOCTYPE html>
<html lang="en">
  <head>
    <script type="text/javascript" src="/index.js" defer></script>
  </head>
  <body>
    <button onclick="oauthSignIn()">Get Access Token</button>
    <form id="formElem">
      <input type="file" id="file-upload" name="file-upload" />
      <input type="submit" id="submit-button" disabled />
    </form>
  </body>
</html>
index.js
/**
 * URLから、access tokenを切り出す
 */
for (value of location.hash.split("&")) {
  if (value.indexOf("access_token=") === 0) {
    const access_token = value.split("=")[1];
    const ele = document.createElement("p");
    ele.textContent = "Your access token is '" + access_token + "'.";
    document.body.appendChild(ele);
  }
}
/**
 * ここから下のコードは、ほとんどそのまま
 * https://developers.google.com/identity/protocols/oauth2/javascript-implicit-flow#oauth-2.0-endpoints
 * から引用しています。
 *
 */
/*
 * Create form to request access token from Google's OAuth 2.0 server.
 */
function oauthSignIn() {
  // Google's OAuth 2.0 endpoint for requesting an access token
  var oauth2Endpoint = "https://accounts.google.com/o/oauth2/v2/auth";

  // Create <form> element to submit parameters to OAuth 2.0 endpoint.
  var form = document.createElement("form");
  form.setAttribute("method", "GET"); // Send as a GET request.
  form.setAttribute("action", oauth2Endpoint);

  // Parameters to pass to OAuth 2.0 endpoint.
  var params = {
    client_id: [先程取得したクライアントID],
    redirect_uri: [リダイレクトするURL、例えば"http://localhost:3000"],
    response_type: "token",
    scope: [先程設定したscope、例えば"https://www.googleapis.com/auth/devstorage.read_write"],
    include_granted_scopes: "true",
    state: "pass-through value",
  };

  // Add form parameters as hidden input values.
  for (var p in params) {
    var input = document.createElement("input");
    input.setAttribute("type", "hidden");
    input.setAttribute("name", p);
    input.setAttribute("value", params[p]);
    form.appendChild(input);
  }

  // Add form to page and submit it to open the OAuth 2.0 endpoint.
  document.body.appendChild(form);
  form.submit();
}

動作

以下のように動作します。

Cloud Storageへのアップロード

それでは、前のsectionで得られたaccess tokenを用いて、Cloud Storageへファイルをアップロードします。Cloud Storageではファイルを置く場所を「Bucket」、データ(ファイル)を「Object」と呼びます。
今回は、予め、Bucketは作成されている前提で、以下の記事を書いています。
もしも、Bucketを作成していない方は、『ストレージ バケットの作成』[3]を参考に、作成してください。ちなみにですが、今回の記事の延長線上で、おそらくストレージバケットもAPI叩くだけで作成できてしまうはずです。Cloud Storageのバケット名が必要なので用意してください。

Cloud Storageにファイルをアップロード

では、本題ですが、以下に上で作成したソースコードと組み合わせて、フロントエンドのJavascriptだけで、Cloud Storageのバケットにファイルを送信するソースコードを示します。

ソースコード

htmlは、先程示したのと同じで、Javascriptのコードだけ以下のように書き換えます。

index.js
const bucketName = [作成したBucket名];
const clientID = [前のsectionで得たclient ID];
const redirectURL = [前のsectionで設定したredirect URL];
const scope = [前のsectionで設定したscope];
let accessToken = null;

/**
 * リダイレクトされた URLから、access tokenを切り出す
 */
for (value of location.hash.split("&")) {
  if (value.indexOf("access_token=") === 0) {
    const access_token = value.split("=")[1];
    const ele = document.createElement("p");
    ele.textContent = "Your access token is '" + access_token + "'.";
    accessToken = access_token;
    document.body.appendChild(ele);
    document.getElementById("submit-button").disabled = false;
  }
}

/**
 * ファイルのアップロード
 */
formElem.onsubmit = async (e) => {
  e.preventDefault();
  const file = document.getElementById("file-upload");
  const filename = file.value.split("\\").slice(-1)[0];
  console.log("File Name: " + JSON.stringify(filename));
  const extension = filename.split(".").slice(-1)[0].toLocaleLowerCase();
  let contentType = null;
  // content-typeの判別
  if (extension === "png") {
    contentType = "image/png";
  } else if (extension === "jpg" || extension === "jpeg") {
    contentType = "image/jpeg";
  } else if (extension === "svg") {
    contentType = "image/svg+xml";
  } else if (extension === "mpeg") {
    contentType = "video/mpeg";
  } else if (extension === "webm") {
    contentType = "video/webm";
  } else {
    alert("このファイルは対応していません。");
  }
  // ファイルの読み込みと、アップロード
  if (contentType) {
    const reader = new FileReader();
    reader.addEventListener("load", async (event) => {
      const bytes = event.target.result;
      let response = await fetch(
        `https://storage.googleapis.com/upload/storage/v1/b/${bucketName}/o?uploadType=media&name=${filename}`,
        {
          method: "POST",
          headers: {
            "Content-Type": contentType,
            Authorization: `Bearer ${accessToken}`,
          },
          body: bytes,
        }
      );

      let result = await response.json();
      if (result.mediaLink) {
        alert(
          `Success to upload ${filename}. You can access it to ${result.mediaLink}`
        );
      } else {
        alert(`Failed to upload ${filename}`);
      }
    });

    reader.readAsArrayBuffer(file.files[0]);
  }
};

/**
 * ここから下のコードは、ほとんどそのまま
 * https://developers.google.com/identity/protocols/oauth2/javascript-implicit-flow#oauth-2.0-endpoints
 * から引用しています。
 *
 */
/*
 * Create form to request access token from Google's OAuth 2.0 server.
 */
function oauthSignIn() {
  // Google's OAuth 2.0 endpoint for requesting an access token
  var oauth2Endpoint = "https://accounts.google.com/o/oauth2/v2/auth";

  // Create <form> element to submit parameters to OAuth 2.0 endpoint.
  var form = document.createElement("form");
  form.setAttribute("method", "GET"); // Send as a GET request.
  form.setAttribute("action", oauth2Endpoint);

  // Parameters to pass to OAuth 2.0 endpoint.
  var params = {
    client_id: clientID,
    redirect_uri: redirectURL,
    response_type: "token",
    scope: scope,
    include_granted_scopes: "true",
    state: "pass-through value",
  };

  // Add form parameters as hidden input values.
  for (var p in params) {
    var input = document.createElement("input");
    input.setAttribute("type", "hidden");
    input.setAttribute("name", p);
    input.setAttribute("value", params[p]);
    form.appendChild(input);
  }

  // Add form to page and submit it to open the OAuth 2.0 endpoint.
  document.body.appendChild(form);
  form.submit();
}

コードは読んでいただければ分かるかと思いますが、以下に、参考記事を紹介します。

  • 拡張子とMIMEの対応関係

https://developer.mozilla.org/ja/docs/Web/HTTP/Basics_of_HTTP/MIME_types/Common_types

  • curlでCloud Storageにファイルをアップロードする

https://cloud.google.com/storage/docs/uploading-objects#rest-upload-objects

  • <input type="file">からBlobを作成

https://note.kiriukun.com/entry/20190404-dump-file-contents-selected-with-input-type-file-in-hexadecimal

  • fetchの使い方

https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API/Using_Fetch

  • input fileの使い方

https://developer.mozilla.org/en-US/docs/Web/API/File/Using_files_from_web_applications#accessing_selected_files

https://developer.mozilla.org/ja/docs/Web/HTML/Element/input/file

動作

以下のように動きます。画像が小さく、見えにくくて、ごめんなさい。

  • アクセストークンを取得したあとの実行結果 *

さいごに

アクセストークンの取得方法が沢山あり、目的にあったものを探すのに苦労しました。
記事に関して不明な点や「ここ、まずいのでは…」というような点がありましたら、コメントまたは akira.kashihara[at]hotmail.com までメールをお願い致します。

脚注
  1. Cloud Storage の認証 / Google https://cloud.google.com/storage/docs/authentication (2022-03-12閲覧) ↩︎ ↩︎ ↩︎

  2. OAuth 2.0 for Client-side Web Applications / Google https://developers.google.com/identity/protocols/oauth2/javascript-implicit-flow#oauth-2.0-endpoints (2022-03-12閲覧) ↩︎

  3. ストレージ バケットの作成 / Google https://cloud.google.com/storage/docs/creating-buckets#storage-create-bucket-console ↩︎

Discussion