2018年3月31日土曜日

Google Apps Scriptで設定ファイルの保存・読込

内容
スクリプトの設定情報やスクリプトの実行状況などを保存する方法としてClass PropertiesServiceが提供されている。しかし、これには少し欠点があるので別の方法を考えてみたのでそれを紹介する。紹介といっても自分用の備忘程度なので細かい説明は書きません。

Class PropertiesServiceについて
データの共有範囲毎にメソッドが用意されている。非常に簡単にデータを保存できるので何かと便利です。後に述べる問題点に該当しなければジャンジャン使いましょう。

メソッド 共有範囲/編集閲覧
getDocumentProperties() すべてのユーザーで共有されるドキュメント(document,spreadsheet,form)のプロパティオブジェクトを取得する。
[T.B.D.]閲覧編集方法不明(調査未実施)
getScriptProperties() すべてのユーザーで共有されるスクリプトのプロパティオブジェクトを取得する。
スクリプトエディタの 「ファイル」>「プロジェクトのプロパティ」>「スクリプトプロパティ」 で閲覧編集可能。
getUserProperties() ユーザー毎でスクリプト毎のプロパティオブジェクトを取得する。
スクリプトエディタの 「ファイル」>「プロジェクトのプロパティ」>「ユーザープロパティ」 で閲覧編集可能。

詳細はClass PropertiesServiceを参照ください。

プロパティの保存方法
var properties = PropertiesService.getUserProperties();
  properties.setProperty("Target File ID","205Ps_mAhEFs.......Ugm1m");

プロパティの参照方法
var properties = PropertiesService.getUserProperties();
  var TargetFileId = properties.getProperty("Target File ID");

問題点
  • 複数のスクリプトで共有することができない。
  • 保存できるサイズに制限があり、実験的に9217文字以上を指定すると関数が失敗する事がわかっている。
    • 9216文字までと確定している場合には問題にならないが、不定長の場合には注意が必要。

解決策
ファイルにプロパティを保存できるようにします。用意している関数は以下の4つ。
  • getProperty(key)
  • setProperty(key,value)
  • deleteProperties()
  • deletePropertu(key)
使い方の例は以下の通り。
function myFunction() {
  // スクリプトを起動した回数をプロパティから取得
  var count = getProperty("count");
  if( count != null )
  {
    count++;
  }
  else
  {
    count=0;
  }
  // プロパティに値を保存
  setProperty("count",count);
  setProperty("hoge","123");
  setProperty("hogehoge","456");
  if( count > 1 )
  {
    // 指定したプロパティを削除
    deleteProperty("hogehoge");
  }
  if( count > 4 )
  {
    // すべてのプロパティを削除
    deleteProperties();
  }
}

上記を実行するとマイドライブの下に、[スクリプト名]_[スクリプト実行ユーザーメールアドレス]という名前のファイルが生成され、そこにプロパティが保存されている。 いかが一回スクリプトを実行したときに生成されるファイルの内容。
{"count":0,"hoge":"123","hogehoge":"456"}
実行するたびにスクリプトの内容が変化するので内容を確認しながら実行すると動作がわかると思う。

最後にコード
// 使い方
//  var properties = getProperty("count");
//  setProperty("count",12345);
//  deleteProperties();
//  deleteProperty("count");

// プロパティのファイル
var PropertiesFile=null;

// プロパティを取得する。
function getProperty(key)
{
  try{
    return JSON.parse(getPropertiesFile().getBlob().getDataAsString())[key];
  }
  catch(e)
  {
    return null;
  }
}

// プロパティをセットする。
function setProperty(key,obj)
{
  try{
    // エラーに備えてからのオブジェクトを生成
    var property = {};
    // ファイルからテキスト読み込み
    var text =  getPropertiesFile().getBlob().getDataAsString();
    // オブジェクト生成
    property =  JSON.parse(text);
  }
  finally
  {
    // 指定されたプロパティを追加
    property[key] = obj;
    // 最終的にオブジェクトをファイルに保存
    getPropertiesFile().setContent(JSON.stringify(property));
  }
}

// すべてのプロパティを削除する。
function deleteProperties()
{
  // PropertiesServiceに保存してあるファイルIDを取得
  var key = "PropertiesFileID";
  var id = PropertiesService.getUserProperties().getProperty(key);
  // PropertiesServiceを削除
  PropertiesService.getUserProperties().deleteProperty(key);

  try{
    if( PropertiesFile==null )
    {
      if( id != null ){
        DriveApp.getFileById(id).setTrashed(true);
      }
    }
    else
    {
      PropertiesFile.setTrashed(true);
    }
  }
  finally{}
}

// プロパティを削除する。
function deleteProperty(key)
{
  // エラーに備えてからのオブジェクト生成
  var property = {};
  try{
    // ファイルにアクセス
    var text =  getPropertiesFile().getBlob().getDataAsString();
    // ファイルの内容からオブジェクト生成
    property =  JSON.parse(text);
    // 指定されたプロパティ削除
    delete property[key];
  }
  finally
  {
    // 最終的にオブジェクトをファイルに保存
    getPropertiesFile().setContent(JSON.stringify(property));
  }  
}


// プロパティファイルを取得する。
function getPropertiesFile()
{
  if( PropertiesFile!=null )
  {
    return PropertiesFile;
  }
  else
  {
    var userProperties =  PropertiesService.getUserProperties();
    var id = userProperties.getProperty("PropertiesFileID");
    try{
      if( id!=null )
      {
        PropertiesFile = DriveApp.getFileById(id);
        if( PropertiesFile.isTrashed() )
        {
          PropertiesFile=null;;
        }
      }
    }
    catch(e)
    {
      PropertiesFile=null;
    }
    if( PropertiesFile==null )
    {
      // ロパティを保存するファイル名を決める
      // 必要に応じて変更する。
      // 複数のスクリプトで共有する場合はここを同じ名前にする。
      var name = DriveApp.getFolderById(ScriptApp.getScriptId()).getName()
                   + "_" + Session.getActiveUser().getEmail();
      PropertiesFile =  DriveApp.createFile(name,"{}",MimeType.PLAIN_TEXT);
      userProperties.setProperty("PropertiesFileID",PropertiesFile.getId());
    }
    return PropertiesFile;                             
  }
}


コードの説明は省略。。。
気が向いたら解説を追記します。

2018年3月18日日曜日

Google Driveにあるファイルサイズの合計を求める

Googleドライブのストレージ消費量はここ(新しいウインドウで開きます)で確認できます。しかし、確認できるのはストレージ容量を消費している容量です。
基本的にはそれでも困らないのですが、高画質設定のGoogleフォトにバックアップした画像などの容量を消費しないファイルサイズの合計を知りたいときもある?。

ということで、Googleドライブに保存されているファイルの総容量を数えてみようと思います。ですが今回はチョット応用して画像(静止画と動画)の総容量を求めてみます。

手順
  1. GoogleドライブにGoogle Apps Scriptをインストールし、新規のスクリプトを作成する。Google Apps Scriptを使ったことがない人はここ(新しいウインドウで開きます)を参照。
  2. 下記のスクリプトを張り付ける。
  3. スクリプト内のMain関数を実行する。(メニュー>実行>関数を実行>Main)
  4. 実行結果はメールで受け取ります。しかし、ファイル数が多い場合には複数回に分けで処理を実行します。そのため5分に1回程度途中経過を通知する集計中メールを飛ばすようにしています。完了メールが来るまで待ってください。

スクリプト
// メニュー>実行>関数を実行>Main を選択して実行してください
function Main(){
  PropertiesService.getUserProperties().deleteAllProperties();
  CountFileSize();
}
  
function CountFileSize(){
  var StartTime   = new Date();

  var triggers = ScriptApp.getProjectTriggers();
  for (var i = 0; i < triggers.length; i++) {
    ScriptApp.deleteTrigger(triggers[i]);
  }

  trigger = ScriptApp.newTrigger("CountFileSize")
  .timeBased()
  .everyMinutes(5)
  .create();
  
  var UserProperties = PropertiesService.getUserProperties();
  var ContinuationToken = UserProperties.getProperty("ContinuationToken");

  if(ContinuationToken) {
    var images = DriveApp.continueFileIterator(ContinuationToken);
    var TotalFileSize = parseFloat(UserProperties.getProperty("TotalFileSize"));
  } else {
    // カウントしたい条件に応じてコメントアウトする行を変更してください。

    // ドライブのすべてのファイルをカウントする場合
    // var images = DriveApp.getFiles();

    // 自分がオーナーのファイルだけをカウントする場合
    // var images = DriveApp.searchFiles('"me" in owners');

    // 画像だけをカウントする場合
    var images = DriveApp.searchFiles('mimeType contains "image/" or mimeType contains "video/"');

    // 自分がオーナーの画像だけをカウントする場合
    // var images = DriveApp.searchFiles('(mimeType contains "image/" or mimeType contains "video/") and "me" in owners');

    var TotalFileSize = 0;
  }
  
  while(images.hasNext()) {
    var CurrentTime = new Date();
    if( (CurrentTime - StartTime) > 4*60*1000 ){
      UserProperties.setProperty("ContinuationToken", images.getContinuationToken());
      UserProperties.setProperty("TotalFileSize", TotalFileSize);
      // 集計中メールがうるさい場合は以下の一行をコメントアウトしてください。
      SendResult("集計中\n" + TotalFileSize + " Byte\n" + TotalFileSize/1024 + " KByte\n" + TotalFileSize/1024/1024 + " MByte\n" + TotalFileSize/1024/1024/1024 + " GByte\n" );
      return;
    }

    var file = images.next();
    TotalFileSize += file.getSize();
  }

  SendResult("完了\n" + TotalFileSize + " Byte\n" + TotalFileSize/1024 + " KByte\n" + TotalFileSize/1024/1024 + " MByte\n" + TotalFileSize/1024/1024/1024 + " GByte\n" );
  ScriptApp.deleteTrigger(trigger);
  UserProperties.deleteAllProperties();
}

function SendResult(MailBody){
  var address = Session.getActiveUser().getEmail();
  GmailApp.sendEmail(Session.getActiveUser().getEmail(), "Googleドライブのファイルサイズの合計を求める", MailBody);
}

スクリプトの説明

要点だけ書きます、なんとなく処理の順に記載しますが、厳密には処理順ではありません。難しいコードではないのでコードを見てください。
  1. 起動はMain関数、実処理はCountFileSize関数
  2. Main関数では後述するトリガを削除する。
  3. 6分制限対策としてスクリプト起動に5分のタイマー起動のトリガーを設定、トリガではCountFileSize関数を呼び出す。(処理が完了したときにトリガを削除する。)
  4. カウントするファイルリストを取得する。
    ここでカウントしたいファイルを検索する。
    • ドライブ内のすべてのファイルをカウントしたい場合
      var images = DriveApp.getFiles();
    • 自分がオーナーのファイルだけをカウントする場合
      var images = DriveApp.searchFiles('"me" in owners');
    • 画像だけをカウントする場合
      var images = DriveApp.searchFiles('mimeType contains "image/" or mimeType contains "video/"');
    • 自分がオーナーの画像だけをカウントする場合var images = DriveApp.searchFiles('(mimeType contains "image/" or mimeType contains "video/") and "me" in owners');
    • 特定のオーナーのファイルをカウントする場合
      var images = DriveApp.searchFiles('"XXXX@google.com" in owners');
  5. スクリプト実行開始から4分経ったらユーザプロパティに必要な情報を保存して処理を中断する。すでにトリガ設定が終わっているのでここではトリガ設定の処理は行わなくてもよい。
  6. ユーザープロパティには「どこまで処理が完了したか」と「集計中のトータルファイルサイズ」を保存する。
  7. ファイルサイズをTotalFileSizeにどんどん加算していきます。
  8. 正常に終了したらトリガを削除し、本文に集計結果を含めて完了メールを送信する。
スクリプトの応用
高画質設定のGoogleフォトにアップロードした画像の総容量を調べたい場合
  1. Googleフォトを使用していない別ユーザにパートナー共有する。
  2. 別ユーザーはすべてのファイルをライブラリに保存する設定にする。
  3. 別ユーザーのGoogleドライブ設定で「Google フォト」フォルダを作成するをオンにする
  4. 別ユーザーで本スクリプトを実行する。もしくはGoogleフォトフォルダをオリジナルユーザーに共有し、オリジナルユーザー側でOwner指定で本スクリプトを実行する。

    2018年3月11日日曜日

    iPhoneのGoogleフォトアプリ利用時にカメラロールから安全に写真を削除する方法

    Googleフォトにアップロードされた画像をiPhoneのカメラロールから削除したい時があります。その方法について説明します。

    ×絶対やってはいけないこと
    Googleフォトアプリで画像を選択し「ゴミ箱」アイコンをタップして画像を削除。
    これを行うとGoogleフォトからもカメラロール(もしくはすべての写真)からも画像が削除されてしまいます。

    △お勧めしない方法
    写真アプリカメラロール(もしくはすべての写真)で画像を選択し「ゴミ箱」アイコンをタップして画像を削除。
    この操作を行う前には削除する画像がGoogleフォトにアップロードされていることを確認する必要があります。確認は https://photos.google.com にアクセスして行ってください。

    ◎おすすめの方法
    Googleフォトアプリで画像を選択し「・・・」をタップし「元のファイルX個を端末から削除」を選択します。雲斜線アイコンがついている画像はアップロードできていません。「元のファイルX個を端末から削除」が表示されないときは選択した画像はすべてアップロードできていません。
    おすすめの理由は、アップロードできていない画像を選択してこの操作を行って実施すると”一部のファイルはGoogleフォトライブラリにバックアップされていないため、この操作を行うと完全に削除されます”と警告が出ることです。この警告が出た場合には一度キャンセルし選択された画像をアップロードしてから再度実施してください。


    [番外編]カメラロールの写真をすべて削除する方法
    Googleフォトアプリ「≡」(合同記号みたいなアイコン)をタップし「空き容量を増やす」を選択します。アップロード済みの画像すべてをカメラロールから削除します。この操作を行った後に残った画像はアップロードできていない画像の可能性が高いです。
    たまにアップロードされている画像も残ることがある気がしますが、未検証です。

    Google Apps Scriptの始め方

    手順
    1. ブラウザでGoogle Driveにログインする。
    2. ”新規” > "その他” > ”+アプリを追加” を選択する。
    3. 検索入力ボックスに”Google Apps Script”と入力しEnterキーを押して検索を行い、Google Apps Scriptを”+接続”する。
    4. ”新規” > "その他” > ”Google Apps Script” を選択する。
    5. コードを入力し、実行したい関数を指定して、実行を押す。(いきなり説明がいい加減、、、)

      今回は以下のスクリプトにした。
      function myFunction() {
        Logger.log("こんにちは");
      }
      
    6. すぐに実行できるかと思いきや、いろいろと許可しないといけない。ただし、こう言ったものはちょくちょく変化していくので細かい説明は省略。現時点での画面を貼っておくので、参考にしてほしい。
    参考になるかもしれない情報



    Googleドライブでフォルダに属していないファイルを集める

    Googleドライブを使用していると、どのフォルダにも属していないファイルが見つかることがある。 ドライブ内を検索したり、ストレージ容量消費が多い順にファイルを並べたりしたとき目にすることがある。
    • フォルダに属しているファイルの情報には”パス”という項目がある。

    • フォルダに属していないファイルの情報には本来あるべき”パス”という項目がない。
    見えないファイルがあるのは気持ち悪いので、行方不明ファイルを一か所のフォルダに集めちゃおうというのが今回の内容。

    手順
    1. GoogleドライブにGoogle Apps Scriptをインストールし、新規のスクリプトを作成する。詳細はここを参照。
    2. 下記のスクリプトを張り付ける。
    3. スクリプト内のMain関数を実行する。(メニュー>実行>関数を実行>Main)
    4. ドライブで管理しているファイル数が少ないときは一回の実行で完了するが、ファイル数が多くなると時間がかかり、Google Apps Scriptの制限の一つである6分以内という制限に引っかかってしまうため、繰り返し実行することですべてのファイルをスキャンする。処理が完了したらメールで通知を行う。
    スクリプト

    • 本スクリプトはフォルダに属していないファイルをマイドライブ直下のLostFilesフォルダにまとめる。
    • Main関数を一回実行すると処理が終了するまで定期的にスクリプトが実行される。
    • 処理が完了するとメールが送られてくる。

    // http://blog.hikozaru.com/2018/03/google.html
    // メニュー>実行>関数を実行>Main を選択して実行してください
    function Main(){
      PropertiesService.getUserProperties().deleteAllProperties();
      FindLostFiles();
    }
    
    function FindLostFiles(){
      var StartTime   = new Date();
    
      var triggers = ScriptApp.getProjectTriggers();
      for (var i = 0; i < triggers.length; i++) {
        ScriptApp.deleteTrigger(triggers[i]);
      }
    
      trigger = ScriptApp.newTrigger("FindLostFiles")
      .timeBased()
      .everyMinutes(5)
      .create();
        
      var UserProperties = PropertiesService.getUserProperties();
      var ContinuationToken = UserProperties.getProperty("ContinuationToken");
    
      if(ContinuationToken) {
        var files = DriveApp.continueFileIterator(ContinuationToken);
      } else {
        var files = DriveApp.getFiles();
      }
      
      var Folder = getSubFolder( DriveApp.getRootFolder(),'LostFiles');
      while(files.hasNext()) {
        var CurrentTime = new Date();
        if( (CurrentTime - StartTime) > 4*60*1000 ){
          UserProperties.setProperty("ContinuationToken", files.getContinuationToken());
          return;
        }
        
        var file = files.next();
        if( !file.getParents().hasNext() )
        {
          Folder.addFile(file);
        }
      }
      // トークンを削除
      ScriptApp.deleteTrigger(trigger);
      UserProperties.deleteAllProperties();
      SendResult("処理終了\n" + Folder.getUrl());
    }
    
    function getSubFolder( baseFolder, subFolderName ) {
      var subFolders = baseFolder.getFoldersByName(subFolderName);
      if( subFolders.hasNext() )
      {
        return subFolders.next();
      }
      return baseFolder.createFolder(subFolderName);
    }
    
    function SendResult(MailBody){
      var address = Session.getActiveUser().getEmail();
      GmailApp.sendEmail(Session.getActiveUser().getEmail(), "フォルダに属していないファイルを探す", MailBody);
    }
    

    スクリプトの説明

    • Main関数はスタート用の関数。
    • FindLostFilesが時間主導トリガとして実行される。
    • スクリプト起動から4分程度でスクリプトを中断する。
    • 再開用のトリガはスクリプト起動時に設定するので中断時には設定不要。
    • 終了時にはファイルを集めたフォルダのURIと主にスクリプトの終了をメールで通知する。

    参考にした情報