Google Apps ScriptでCloud Storageにファイルをアップする【GAS】

Google Apps Scriptは、パスワード付きPDFが作成できない事から、生成するためには外部のAPI等を利用しなければなりません。これ、Google Cloud Functionsでどうにかできないかなぁという考えている過程で、Hummus RecipeというNode.jsのモジュールが単体でPDF暗号化に対応しているので、これ使えないかなと実験中(Bufferから直接は暗号化できない為)。

そのHummus Recipeは通常はローカルのPDFファイルを受け取って、パスワード設定するのですが、クラウド上で実現するには、一旦Google Cloud Storageにアップしなければならいのではという事で、今回は、GASからGCSにアップする手段を作ります

今回使用するスプレッドシートその他

※Cloud Storageサービスは有料のサービスです。その為、利用するには事前に課金設定(請求アカウントの設定)が必要になります。

事前準備

OAuth2.0認証ライブラリの追加

今回のサービスは、OAuth2.0認証が必要です。以下の手順でOAuth2 for Apps Scriptライブラリを追加しましょう。

  1. スクリプトエディタを開きます。
  2. メニューより「リソース」⇒「ライブラリ」を開きます。
  3. ライブラリを追加欄に「1B7FSrk5Zi6L1rSxxTDgDEUsPzlukDsi4KGuTMorsTQHhGBzBkMun4iDF」を追加します。
  4. 現時点ではバージョンは30が最新ですので、それを選択しておきます。
  5. 保存ボタンを押して完了

これで、OAuth2.0認証にまつわる様々な関数を手軽に利用できるようになります。

図:ライブラリを追加した様子

プロジェクトを移動

今回の発表直前の2019年4月8日より、Google Apps ScriptからCloud Platform Projectへ直接アクセスが出来なくなりました。これまでにデプロイしてるものについては、これまで通り「リソース」⇒「Google Cloud Platform API ダッシュボード」からアクセスが可能です。

今回の変更はスプレッドシート上で動かすスクリプトやGoogleの拡張サービスを利用しないタイプのスクリプトであれば特に問題はありませんが、「Apps Script API」や「Google Picker API」、「Cloud SQL接続」などGCP上のAPIを利用する場合には以下の手順を踏んで、Google Apps Scriptにプロジェクトを連結する必要があります。これまでは、自動的にGCP上にGoogle Apps Script用のプロジェクトが生成されていたのですが、今後は自分の組織(もしくはGCPプロジェクト)上で作成されたプロジェクトでなければならないということです。詳細はこちらのページを見てください。

連結する手順は以下の通り

  1. Google Cloud Consoleを開く
  2. 左上にある▼をクリックする
  3. ダイアログが出てくるので、新規プロジェクトを作るか?既存のプロジェクトを選択する。この時、G Suiteであれば選択元は「自分のドメイン」を選択する必要があります。
  4. プロジェクト情報パネルから「プロジェクト番号」をコピーする
  5. 対象のGoogle Apps Scriptのスクリプトエディタを開く
  6. 「リソース」⇒「Cloud Platform プロジェクト」を開く
  7. 4.で入手した番号をプロジェクトを変更のテキストボックスに入れて、プロジェクトを設定ボタンをクリックする
  8. 無事に移動が完了すればメッセージが表示されます。
  9. この時、元の自動作成されたプロジェクトはシャットダウンされて消えます。これで設定完了です。

今回のこの変更だと1つ作ったプロジェクトに集約する必要があるので、クォータについてプロジェクト毎のカウントだったので問題なかったものが、集約されることで、クォータに引っ掛かる可能性があります。

図:プロジェクト番号をコピーしておきます

図:プロジェクトを他のプロジェクトに紐付けしました。

図:GCPの拡張サービスを使うには手順が必要になった

サービスアカウントの作成

今回のスクリプトの準備で最も面倒なのはこのサービスアカウントの作成です。サービスアカウントの作成自体は以前Google Cloud Consoleを弄ってみるの回で紹介しています。今回はサービスアカウント方式を利用しています。請求アカウントは事前に作ってある事としてここでは、サービスアカウントの作成のみを紹介します。

  1. スクリプトエディタを開き、「リソース」⇒「Googleの拡張サービス」を開く
  2. ダイアログ下にある「Google Cloud Platform API ダッシュボード」を開く
  3. APIとサービスにて、APIとサービスの有効化にするをクリックする。
  4. Cloud Storage JSON APIを有効化する。続けて認証情報の追加画面に移動し、Cloud Storage JSON APIを選択しておく。
  5. App Engineで使う予定の問いには、「いいえ」で答える。
  6. 次のステップではサービスアカウントの名前を入力。わかりやすい名前をつけましょう。キーのタイプはJSONを選択
  7. サービスアカウントの役割では、Project⇒編集者を選択します。
  8. JSONファイルがダウンロードされるので、これを誰とも共有しない形で、Google Driveにアップロードします。流出すると後で課金で痛い目を見るので絶対に共有はしないでください。
  9. アップロードしたJSONファイルの直URLを取得する。https://drive.google.com/open?id=に続けてファイルのIDをつなげればOKです。
  10. 次の項目のJSONキーファイルを取得して認証するにて、冒頭のfilelinkの場所にこのURLを入れてあげる。

図:Cloud Storage JSON APIを有効化する

図:APIの認証情報を作っておく

図:サービスアカウントの権限はできる限り最小で

JSONキーファイルを取得して認証する

//認証用各種変数
var tokenurl = "https://accounts.google.com/o/oauth2/token"
var jsonkey = "ここにJSONファイルのIDを指定する";   //JSON Keyファイルを指定

//OAuth2認証を実行する
function startoauth(){
  //UIを取得する
  var ui = SpreadsheetApp.getUi();
  
  //認証を実行する
  var service = checkOAuth();
  ui.alert("認証が完了し、Access Tokenを取得しました。")
}

//Google DriveにあるサービスアカウントキーのJSONファイルを取得する
function getServiceAccKey(){
  //JSONファイルの中身を取得する
  var content = DriveApp.getFileById(jsonkey).getAs("application/json").getDataAsString();
  return JSON.parse(content);
}

//OAuth2.0認証を実行する
function checkOAuth() {
  //JSONファイルの中身を取得する
  var privateKeys = getServiceAccKey();
  
  return OAuth2.createService('cloudstorage:' + Session.getActiveUser().getEmail())
  //アクセストークンの取得用URLをセット
  .setTokenUrl(tokenurl)
  
  //プライベートキーとクライアントIDをセットする
  .setPrivateKey(privateKeys['private_key'])
  .setIssuer(privateKeys['client_email'])
  
  //Access Tokenをスクリプトプロパティにセットする
  .setPropertyStore(PropertiesService.getScriptProperties())
  
  //スコープを設定する(スペースで分割して複数指定可能)
  .setScope('https://www.googleapis.com/auth/cloud-platform https://www.googleapis.com/auth/devstorage.read_write');
}
  • この認証方式はサービスアカウントが認証を行うので、いつものユーザが認証するものとは異なり、Access Token取得は自動で行われます。
  • 取得したアクセストークン等の塊は、OAuth2ライブラリ最新版より、スクリプトプロパティではなくユーザプロパティに格納されているので、より安全になっています。塊は、var service = checkOAuth();で呼び出せます。
  • スコープは半角スペースで切ると、複数付加することが可能です。今回はcloud-platformdevstorage.read_writeを繋げています。

Google Picker用の準備

今回のスクリプトでは、ユーザがDriveのファイルを指定し、Cloud StorageにアップできるようにPicker APIを利用しています。APIキーの取得に関しては、前回の記事が参考になります。

GAS側コード

function onOpen() {
    var ui = SpreadsheetApp.getUi();
    ui.createMenu('▶作業実行')
    .addItem('認証実行', 'startoauth')
    .addItem('ファイル選択', 'fileman')
    .addToUi();
}

//Pickerダイアログを表示するコード
function fileman() {
  var html = HtmlService.createHtmlOutputFromFile('Picker.html')
      .setWidth(600).setHeight(425);
  SpreadsheetApp.getUi().showModalDialog(html, 'ファイルの選択');
}

//Origin設定を取得する
function getPickerInfo() {
  //originを取得する
  var origin = "https://script.google.com";
  try{
    SitesApp.getActivePage().getUrl();
    origin = "https://sites.google.com";
  }catch(e) {
  }
  
  //デベロッパーキーを取得する
  var devkey = "ここにPickerのAPIキーを入力してください。";
  
  //Access Token, 親フォルダID, Developer Key, ダイアログのサイズ, originなどをセットし、return
  DriveApp.getRootFolder();
  return {
    token: ScriptApp.getOAuthToken(),
    origin: origin,
    developerKey: devkey,
    dialogDimensions: {width: 600, height: 425}  
  };
}
  • 必ず1回、startoauthにて認証の実行が必要です。

HTML側コード

<link rel="stylesheet" href="https://ssl.gstatic.com/docs/script/css/add-ons.css">
<script type="text/javascript" src="https://apis.google.com/js/api.js"></script>

<script type="text/javascript">
  var pickerApiLoaded = false;
  var origin = google.script.host.origin;
  
  //Google Picker API呼び出し
  gapi.load('picker', {'callback': function() {
    pickerApiLoaded = true;
  }});

  //OAuthにて認証作業
  function getOAuthToken() {
    google.script.run.withSuccessHandler(createPicker)
                     .withFailureHandler(showError).getPickerInfo();
  }

  //Picker Dialogを表示する
  function createPicker(data) {
    if (pickerApiLoaded && data.token) {
      var docsView = new google.picker.DocsView()
          .setIncludeFolders(false) 
          //.setMimeTypes('application/vnd.google-apps.spreadsheet')
          .setSelectFolderEnabled(false);
    
      //Pickerに値をセットする
      var picker = new google.picker.PickerBuilder()
          .addView(docsView)
          .enableFeature(google.picker.Feature.NAV_HIDDEN)
          .hideTitleBar()
          .setLocale('ja')
          .setOAuthToken(data.token)
          .setOrigin(origin)
          .setDeveloperKey(data.developerKey)
          .setCallback(pickerCallback)
          .setSize(data.dialogDimensions.width - 2,
                   data.dialogDimensions.height - 2)
          .build();
          
      //Pickerを表示する
      picker.setVisible(true);
    } else {
      document.getElementById("main").innerHTML = 'Pickerをロード出来ませんでした。';
    }
  }

  //Callbackデータを受け取る
  function pickerCallback(data) {
    var action = data[google.picker.Response.ACTION];
    if (action == google.picker.Action.PICKED) {
      var doc = data[google.picker.Response.DOCUMENTS][0];
      var id = doc[google.picker.Document.ID];
      var url = doc[google.picker.Document.URL];
      var title = doc[google.picker.Document.NAME];
      document.getElementById('result').innerHTML =
          '<b>You chose:</b><br>Name: <a href="' + url + '">' + title + '</a><br>ID: ' + id;
      google.script.run.uploadgcs(id);
    } else if (action == google.picker.Action.CANCEL) {
      document.getElementById('result').innerHTML = 'Picker canceled.';
    }
  }

  //エラー表示用
  function showError(message) {
    document.getElementById('main').innerHTML = 'Error: ' + message;
  }
</script>

<div id="main">
  <button onclick='getOAuthToken()' class="action">ファイルを選択</button>
  <p id='result'></p>
</div>

Cloud Storageにバケットを用意する

ここまで準備ができたら、あとはCloud Storage側にバケットを用意します。以下の手順でバケットを用意して、アップロード用コードの為のバケット名を取得しておきます。

  1. Cloud Consoleにて、Cloud Storageを開き、バケットの作成をクリック
  2. 名前に半角英数字で命名する。これがバケット名になる。
  3. 作成をクリックして完了
  4. 今回は更に、publicという名前のフォルダを用意しました。

図:名前は世界でユニークな名前をつけなければならない

ソースコード

//HTML側でチョイスしたファイルのIDを元に処理を開始する
function uploadgcs(targetid,filename){
  //バケット名の設定
  var bucket = "drive2gcs";
  
  //アップロード先フォルダを指定
  var folders = "public/" + filename;
  
  //Access Tokenを取得
  var service = checkOAuth();
  if (!service.hasAccess()) {
    Logger.log("まずは認証を実行してください。 %s", service.getAuthorizationUrl());
    return;
  }
  
  //指定ファイルを取得
  var blob = DriveApp.getFileById(targetid).getBlob();
  var bytes = blob.getBytes();
  
  //アップロードURLを組み立て
  var url = "https://www.googleapis.com/upload/storage/v1/b/" + bucket + "/o?uploadType=media&name=" + encodeURIComponent(folders);
  
  //UrlfetchAppでアクセス
  var res = UrlFetchApp.fetch(
    url,
    {
      method:"POST",
      contentLength: bytes.length,
      contentType: blob.getContentType(),
      payload:bytes,
      headers: {
        Authorization: "Bearer " + service.getAccessToken()
      }
    }
  );
  
  //レスポンスを取得
  //var result = JSON.parse(res.getContentText());
  var ui = SpreadsheetApp.getUi();
  ui.alert("アップ完了");
}
  • バケットの名前は、drive2gcsにしてあります。
  • Pickerからは、ファイルのIDとファイル名を引数で取得しています。
  • publicフォルダ以下にファイル名を付けてアップロードするようにしています。
  • URL組み立てでバケット名と格納先+ファイル名を繋げています。
  • POST通信で取得したByteデータをGCS側へと渡しています。
  • DELETEメソッドを使って、https://www.googleapis.com/storage/v1/b/バケット名/o/ファイル名にて、ファイルを削除する事も可能です。

ちなみに、レスポンスで取得した内容は以下のような感じ。getContentTextで取得できます。これで、Cloud Function側からファイルにアクセスできるようになったはず!!多分。

{
  generation=xxxxxx, 
  metageneration=1, 
  kind=storage#object, 
  selfLink=https://www.googleapis.com/storage/v1/b/drive2gcs/o/public%2F%E3%83%95%E3%82%A1%E3%82%A4%E3%83%AB_001.png, 
  mediaLink=https://www.googleapis.com/download/storage/v1/b/drive2gcs/o/public%2F%E3%83%95%E3%82%A1%E3%82%A4%E3%83%AB_001.png?generation=xxxxx&alt=media, 
  bucket=drive2gcs, 
  storageClass=MULTI_REGIONAL, 
  size=5392197, 
  md5Hash=xxxxxxxxx, 
  crc32c=/VYijA==, 
  timeStorageClassUpdated=2019-05-26T19:32:32.735Z, 
  name=public/ファイル_001.png, 
  timeCreated=2019-05-26T19:32:32.735Z, 
  etag=xxxxxxx, 
  id=drive2gcs/public/ファイル_001.png/1558938752736055, 
  contentType=image/png, 
  updated=2019-05-27T06:32:32.735Z
}

図:無事にアップロードできました。

関連リンク

Google Apps ScriptでCloud Storageにファイルをアップする【GAS】” に対して2件のコメントがあります。

  1. 市川 より:

    こんにちは
    本記事を参考に日々役立たせていただいていたのですが、
    Error(‘Access not granted or expired.’);
    というエラーが出るようになってしまいました。

    何か解決方法をご存知ではないかと思いコメントさせていただきました。
    お手数おかけしますが、ご確認いただけますでしょうか。

    1. akanemaru2017 より:

      サービスアカウントなので本来は、自動でAccess Tokenが取得されるはずですが、たびたびGoogle Apps Scriptに於いて、「再度認証を求められた」というケースが主に6月のGoogleイベント後に発生したりしてるので、その影響下と思います。主に機能拡張に伴って、認証内容が無くなってしまい、再認証をすれば動くと思います。

      以下のエントリーに自分が解答していますが、Google Apps Script APIのスイッチをオンにして再認証をしてみてください。
      https://teratail.com/questions/278164

市川 へ返信する コメントをキャンセル

メールアドレスが公開されることはありません。 が付いている欄は必須項目です

日本語が含まれない投稿は無視されますのでご注意ください。(スパム対策)