JavaScirpt+Google Photos APIsで、Googleフォトに画像をアップロードする方法

2020年10月29日木曜日

GAPI Javascirpt

t f B! P L

Google API Client Library for JavaScript(gapi) を使って、JavaScriptだけでGoogleフォトへ画像をアップロードする方法を紹介します。

全体の流れ

Googleフォトに画像をアップロードする流れは、だいだい次のような流れになります。

  1. Google API Client Library for JavaScript(gapi)の読み込み
  2. gapiの初期化
  3. OAuth認証(Googleへのログインと権限の要求)
  4. Media upload APIで画像をアップロード
  5. Media Batch Create APIでGoogleフォトへ登録

ポイントは、「Media upload API」で画像をアップロード後、さらに「Media Batch Create API」を呼び、2段階のAPI呼び出し画像を登録する必要があるところです。

必要な権限

当たり前このかもしれませんが、承認なしにユーザーのGoogleフォトにアクセスすることは出来ません。
今回のサンプルコード実行には、OAuth認証時に、次のいずれかの権限(scope)を取得しておく必要があります。

  • https://www.googleapis.com/auth/photoslibrary
  • https://www.googleapis.com/auth/photoslibrary.appendonly
  • https://www.googleapis.com/auth/photoslibrary.sharing

※ 今回紹介するサンプルをそのまま貼り付ければ、権限(scope)の取得は次のとおり行われます。

スポンサーリンク

準備

Photo Library APIの有効化

GCP(Google Cloud Platform)に登録して、Photo Library APIを有効化しておく必要があます。

  1. サイドメニューから「APIとサービス」→「ライブラリ」の順に選択
  2. 検索バーに「Photos Library」と入力し、Photos Library APIを選択
  3. 「有効化」をクリック

APIキー、OAuthクライアントの作成

JavaScriptからAアクセスするために、APIキー、WEBクライアント、OAutsクライアントの作成をしておきます。

今回作成するサンプル

今回はシンプルに、Googleへの認証を行うボタンと、画像を選択するコントロールを配置した次のような画面をつくります。

Googleへの認証後、ファイル選択から画像を選択するとGoogleフォトへのアップロードする動きにします。

スポンサーリンク

実装

まず、HTMLを作成します。
HTMLには、Googleで認証するための「ログイン」「ログアウト」ボタンと、ファイル選択コントロールを配置します。

<h1>Upload Sample</h1>
<section>
  <button id="login" onclick="login()" style="display: none;">ログイン</button>
  <button id="logout" onclick="logout()" style="display: none;">ログアウト</button>
</section>

<section>
  <div id="file" style="margin-top: 20px; display: none;">
    <input id="file-input" type="file" onchange="upload(this.files[0])" />
  </div>
</section>

続いて、次の1〜6の流れでJavaScirptの実装をしていきます。

1. Google API Client Library for JavaScript(gapi)の読み込み

<script>タグを置いて、Google API Client Library for JavaScript(gapi)を読み込みます。
また、スクリプトのロード完了時にhandleClientLoad()関数を呼び出すためにonloadおよびonreadystatechangeイベントを設定します。

<script async defer src="https://apis.google.com/js/api.js" 
  onload="this.onload=function(){};handleClientLoad()" 
  onreadystatechange="if (this.readyState === 'complete') this.onload()">
</script>

2. gapiの初期化

handleClientLoad関数を作成し、APIキー・OAuthクライアントおよび、使用するスコープを指定して、gapiを初期化します。

const apiKey = '<APIキー>';
const clientId = '<OAuthクライアントID>.apps.googleusercontent.com';
const discoveryDocs = [];
const scopes = 'https://www.googleapis.com/auth/photoslibrary';

// Google API Client Library for JavaScript ロード時のイベント
function handleClientLoad() {
  gapi.load('client:auth2', () => {
    gapi.client.init({
      apiKey: apiKey,
      discoveryDocs: discoveryDocs,
      clientId: clientId,
      scope: scopes
    }).then(function () {
      // サインイン状態を監視し、状態に変化があったときに「updateSigninStatus」を呼ぶ
      gapi.auth2.getAuthInstance().isSignedIn.listen(updateSigninStatus);
      // 初期起動時のサインイン状態で画面制御
      updateSigninStatus(gapi.auth2.getAuthInstance().isSignedIn.get());
    });
  });
}

続いてログイン・ログアウトによって認証状態が変化したときに呼ばれるコールバック関数(今回はupdateSigninStatus)も作成します。

// ログイン・ログアウト状態に変更が発生した時に呼ばれる関数
function updateSigninStatus(isSignedIn) {
  if (isSignedIn) {
    //ログイン状態
    document.getElementById("login").style.display = 'none';
    document.getElementById("logout").style.display = 'block';
    document.getElementById("file").style.display = 'block';
  } else {
    //ログアウト
    document.getElementById("login").style.display = 'block';
    document.getElementById("logout").style.display = 'none';
    document.getElementById("file").style.display = 'none';
  }
}

3 OAuth認証

ログインボタンのクリック時に、Google認証を行うポップアップを表示する処理を行います。
gapiを使えば、たった1行コードを書くだけで簡単にGoogleへのログイン処理が実装できます。

// ログイン
function login() {
  gapi.auth2.getAuthInstance().signIn();
}

認証が成功すると、「2. gapiの初期化」で設定しておいたコールバック関数のupdateSigninStatusが呼ばれるため、認証が成功したらファイル選択コントロールを画面に表示します。

スポンサーリンク

4 Media upload APIで画像をアップロード

続いて、ファイルが選択がされた時に、Googleフォトへ画像をアップロードするupload関数を実装します。

Googleフォトへの画像・動画ファイルのUPは、Media Upload APIを使用します。

ファイルのアップロードは、gapi非対応のため、JavaScript標準のFetch APIを使用してファイルをアップロードします。

// 選択したファイルをGoogleフォトへアップルードする
function upload(file) {
  var accessToken = gapi.auth.getToken().access_token; // OAuthアクセスキーを取得

  // Media Upload APIでファイルをアップロード
  fetch('https://photoslibrary.googleapis.com/v1/uploads', {
    method: 'POST',
    headers: new Headers({ 
      'Authorization': 'Bearer ' + accessToken,
      "Content-type": "application/octet-stream",
      "X-Goog-Upload-Content-Type": file.type,
      "X-Goog-Upload-Protocol": "raw"
    }),
    body: file,
  })
  .then(res => res.text())
  .then(token => {
    // レスポンスのUploadTokenをパラメータにbatchCreateを呼ぶ
    return batchCreate(token);
  });
}

アップロードが完了すると、Media Upload APIからレスポンス本文に「UploadToken」が返却されます。
この、UploadTokenは次に実行するMedia Batch Create APIで使用するため、batchCreate関数の引数に入れて渡します。

5. Media Batch Create APIでGoogleフォトへ登録

最後は、Media Batch Create APIで、前の手順でアップロードしたファイルを、Googleフォトへ登録するbatchCreate関数を作成します。

Media Upload APIで画像をアップロードしただけでは、Googleフォトへ登録されないので注意が必要です。

// Media Batch Create APIを呼び出し、Googleフォトへアップロードしたファイルを登録
function batchCreate(uploadToken) {
  // gapiで、Media Batch Create API呼び出しリクエストの作成
  var restRequest = gapi.client.request({
    'path': 'https://photoslibrary.googleapis.com/v1/mediaItems:batchCreate',
    'method': 'POST',
    'body': {
      'newMediaItems' : [
        {
          'description': "test batch create", // ファイルの概要
          'simpleMediaItem': {
            'uploadToken': uploadToken,  // Media Upload APIのレスポンスで取得したUploadTokenをセット
            'fileName': 'test file name' // ファイル名
          }
        }
      ]
    }
  });

  // リクエストの実行
  restRequest.execute((resp) => {
    console.log(resp);
  });
}

以上で、実装は完了です。実際にファイルを選択して、Googleへの認証とファイルがアップロードされるか確認をしてみてください。

スポンサーリンク

コード全文

部分部分でコードを解説してきたため、ここまで紹介したコードの全文を載せておきます。
コピペして、APIキー・OAuthクライアントIDを書き換えれば動くと思います。

<!DOCTYPE html>
<html>
  <head>
    <meta name="viewport" content="width=device-width, initial-scale=1">
      <title>Google Photo Album List</title>
      <meta charset='utf-8' />
    </head>

    <body>
      <h1>Upload Sample</h1>
      <section>
        <button id="login" onclick="login()" style="display: none;">ログイン</button>
        <button id="logout" onclick="logout()" style="display: none;">ログアウト</button>
      </section>

      <section>
        <div id="file" style="margin-top: 20px; display: none;">
          <input id="file-input" type="file" onchange="upload(this.files[0])" />
        </div>
      </section>

      <script async defer src="https://apis.google.com/js/api.js" onload="this.onload=function(){};handleClientLoad()" onreadystatechange="if (this.readyState === 'complete') this.onload()">
      </script>
      <script>
    const apiKey = '<APIキー>';
    const clientId = '<OAuthクライアントID>.apps.googleusercontent.com';
    const discoveryDocs = [];
    const scopes = 'https://www.googleapis.com/auth/photoslibrary';

    // Google API Client Library for JavaScript ロード時のイベント
    function handleClientLoad() {
      gapi.load('client:auth2', () => {
        gapi.client.init({
          apiKey: apiKey,
          discoveryDocs: discoveryDocs,
          clientId: clientId,
          scope: scopes
        }).then(function () {
          // サインイン状態を監視し、状態に変化があったときに「updateSigninStatus」を呼ぶ
          gapi.auth2.getAuthInstance().isSignedIn.listen(updateSigninStatus);
          // 初期起動時のサインイン状態で画面制御
          updateSigninStatus(gapi.auth2.getAuthInstance().isSignedIn.get());
        });
      });
    }

    // ログイン
    function login() {
      gapi.auth2.getAuthInstance().signIn();
    }

    // ログアウト
    function logout() {
      gapi.auth2.getAuthInstance().signOut();
      gapi.auth2.getAuthInstance().disconnect();
    }

    // ログイン・ログアウト状態に変更が発生した時に呼ばれる関数
    function updateSigninStatus(isSignedIn) {
      if (isSignedIn) {
        //ログイン状態
        document.getElementById("login").style.display = 'none';
        document.getElementById("logout").style.display = 'block';
        document.getElementById("file").style.display = 'block';
      } else {
        //ログアウト
        document.getElementById("login").style.display = 'block';
        document.getElementById("logout").style.display = 'none';
        document.getElementById("file").style.display = 'none';
      }
    }
    
    // 選択したファイルをGoogleフォトへアップルードする
    function upload(file) {
      var accessToken = gapi.auth.getToken().access_token; // OAuthアクセスキーを取得

      // Media Upload APIでファイルをアップロード
      fetch('https://photoslibrary.googleapis.com/v1/uploads', {
        method: 'POST',
        headers: new Headers({ 
          'Authorization': 'Bearer ' + accessToken,
          "Content-type": "application/octet-stream",
          "X-Goog-Upload-Content-Type": file.type,
          "X-Goog-Upload-Protocol": "raw"
        }),
        body: file,
      })
      .then(res => res.text())
      .then(token => {
        // レスポンスのUploadTokenをパラメータにbatchCreateを呼ぶ
        return batchCreate(token);
      });
    }

    // Media Batch Create APIを呼び出し、Googleフォトへアップロードしたファイルを登録
    function batchCreate(uploadToken) {
      // gapiで、Media Batch Create API呼び出しリクエストの作成
      var restRequest = gapi.client.request({
        'path': 'https://photoslibrary.googleapis.com/v1/mediaItems:batchCreate',
        'method': 'POST',
        'body': {
          'newMediaItems' : [
            {
              'description': "test batch create", // ファイルの概要
              'simpleMediaItem': {
                'uploadToken': uploadToken,  // Media Upload APIのレスポンスで取得したUploadTokenをセット
                'fileName': 'test file name' // ファイル名
              }
            }
          ]
        }
      });

      // リクエストの実行
      restRequest.execute((resp) => {
        console.log(resp);
      });
    }
      </script>
    </body>
  </html>

注意事項

下記引用のとおり「Google Photos API」を使ってアップロードされた画像は、元の品質(サイズ)の画像で保存されるため、ストレージの容量を消費します。

引用元:https://developers.google.com/photos/library/guides/upload-media

Note: All media items uploaded to Google Photos through the API are stored in full resolution at original quality. If your uploads exceed 25MB per user, your application should remind the user that these uploads will count towards storage in their Google Account.

Googleフォトの魅力の1つに、高解像度圧縮モードで画像をアップすれば「容量無制限」で使えるメリットがありますが、今回の方法では難しいようです。

また、1ユーザーあたりの画像アップロード容量(おそらく1回のアップロードではなく、全アップロード画像の総量)が25MBを超える場合、ユーザーに通知する必要があると書かれているため、「Google Photos API」を使ってサービスを提供する場合は、その辺りの注意書き等も掲示する必要がありそうです。

さいごに

Google API Client Library for JavaScript(gapi) を使って、Googleフォトに写真・動画をアップロードする方法を紹介しました。

スポンサーリンク

QooQ