2018年4月7日土曜日

Googleスプレッドシートの内容をGoogle Apps Scriptを使用してメール送信する

内容
Google DriveのSpreadSheetに作成されたテーブルの内容に従いメールを送信する。
  • テーブルの内容に従いメールを送信する。
  • テーブルの位置もシート内で指定する。
  • チェックのついている行のみを送信する。
  • 送信完了したら送信完了日時を記録する。
  • GASの6分制限を回避するため5分で安全に終了する。
  • 説明が面倒なので変数名を日本語にしている。
シートの例

スクリプト
function myFunction() {
  var スクリプト開始時間 = new Date();
  var アクティブシート = SpreadsheetApp.getActiveSheet();
  var テーブル範囲 = アクティブシート.getRange("シート1!A3").getValues()[0][0];
  var テーブルデータ = アクティブシート.getRange(テーブル範囲).getValues();
  var テーブル定義 = [];
  for( var i=0; i<テーブルデータ[0].length; i++ )
  {
    テーブル定義[テーブルデータ[0][i]]=i;
  }
  var len = テーブルデータ.length;
  for( var i=1; i<テーブルデータ.length; i++ )
  {
    var 今の時間 = new Date();
    // スクリプト実行開始から5分間実行していたらスクリプトを止める。
    if( (今の時間 - スクリプト開始時間) > 5*60*1000 ){
      // ★で書き込んでいるが念のため再度書き込み
      アクティブシート.getRange(テーブル範囲).setValues(テーブルデータ);
      return;
    }

    if( テーブルデータ[i][テーブル定義["送信指定"]] == "✔" && テーブルデータ[i][テーブル定義["送信完了日"]] == "" )
    {
      try{
        var 文頭         = テーブルデータ[i][テーブル定義["文頭"]];
        var 氏名         = テーブルデータ[i][テーブル定義["氏名"]];
        var メールアドレス = テーブルデータ[i][テーブル定義["メールアドレス"]];
        var ご質問       = テーブルデータ[i][テーブル定義["ご質問"]];
        var 回答         = テーブルデータ[i][テーブル定義["回答"]];

        GmailApp.sendEmail(
          メールアドレス,
          "Googleスプレッドシートの内容をメール送信するGASからのメール",
          Utilities.formatString("%s 様\n\n%s\n\n<ご質問>\n%s\n\n<ご回答>\n\n%s",氏名,文頭,ご質問,回答 ) );
        テーブルデータ[i][テーブル定義["送信完了日"]] = Utilities.formatDate(new Date(), "Asia/Tokyo","yyyyMMddHHmmss");

        // (★)スクリプトエラーが発生したときにどこまで送信したか分からなくなると困るのでこまめに書き込む
        アクティブシート.getRange(テーブル範囲).setValues(テーブルデータ);
        
        Utilities.sleep(1000);
      }catch(e)
      {
        // エラーが発生したときはセルを更新しない。
        Logger.log(e);
      }
    }
  }
  // ★で書き込んでいるが念のため再度書き込み
  アクティブシート.getRange(テーブル範囲).setValues(テーブルデータ);
}


実行結果(送信メールの例)
鈴木C助 様

お世話になります。

<ご質問>
質問です。c~

<ご回答>

回答です。c~

2018年4月6日金曜日

iPhoneの機種変更時に写真をGoogleフォトにBackupする方法

概要
iPhoneの機種変更時によくあるトラブルの一つに写真のバックアップミスがある。
Googleフォトにバックアップしたつもりだったのに実はバックアップできていなくて大切な写真を失ってしまう人がいる。特に最近は下取りに出す人が多く本当に戻ってこない。
そういう失敗をする人を少しでも救いと主に、機種変更前に安全にバックアップする方法をまとめてみた。
ただし、一番安全なのは機種変更時に下取りに出さないことです。

よくある失敗
  • Googleフォトアプリに画像が表示されているので、バックアップされていると思い機種変更したが、実はバックアップされておらず写真を消失。 
  • バックアップしているアカウントを忘れて、機種変更後にバックアップ先アカウントにログインできずに写真を紛失。

機種変更の際に確実にバックアップする方法
  1. 機種変更前のiPhoneにGoogleフォトアプリをインストールする
  2. Googleフォトアプリを起動してログインする。
  3. Googleフォトアプリ左上の「≡」>「歯車」(設定)>「バックアップと同期」画面に遷移する。
  4. 「バックアップと同期」をオンにする。その際に設定したバックアップアカウントを記録しておく。
  5. バックアップのタイミングを設定する。(WiFi環境で作業を行うのであれば、両方オフでもよい。)
  6. バックアップが完了するまでしばらく待つ。
  7. バックアップされているかどうかは https://photos.google.com にログインして確認する。(ここではアカウント確認の意味もあるので省略せずに必ず実施しましょう。)
  8. Googleフォトアプリ左上の「≡」>「空き容量を増やす」を実行する。
  9. 「空き容量を増やすため、端末からXXX個のファイルを削除しますか」というメッセージが表示されるので、「削除(XXX個)」を押下する。
  10. 今度は「"GooglePhotos"にXXX個の項目の削除を許可しますか?」と表示されるので「削除」をタップする。
    • 写真が大量の場合はこのメッセージが表示されるまで5分とか10分かかることもあるので気長に待つ
  11. iPhone標準の写真アプリでカメラロールの中をチェックする。残っている写真はバックアップできていない可能性が高い。
    • もし大量に残っている場合はまだバックアップ途中の可能性が高いのでもう少し時間をおいてから、8番目の手順から繰り返す。
    • 何度行っても画像が残る場合がある。その場合はGoogleフォトにバックアップできていない可能性もあるので手動でアップロードして、8番目の手順から繰り返す。手動のアップロードは https://photos.google.com にアクセスして、雲上矢印を押して行う。
    • いろいろ試してみるとポートレートモードで撮影した写真がカメラロールに残っているようだ。今のところポートレードモード撮影した写真は「iPhoneのGoogleフォトアプリ利用時にカメラロールから安全に写真を削除する方法」で削除するしかなさそうだ。

以上でカメラロールの中から写真がなくなればすべての写真がアップロードされていることになる。 安心してiPhoneを下取りに出せる。

上記の方法に限らず、Googleフォトは写真アプリで作成していたアルバム構成などはバックアップできないので注意が必要。今後も機種変更などを行うことを考えると、iPhoneの写真アプリでアルバムを作るのではなく、Googleフォトでアルバムを作ったほうが良い。
また、Googleフォトはバックアップは自動でできても、Googleフォトから端末に自動で戻すことはできないので、必要であれば頑張ってダウンロードするしかない。

補足
同じ写真を撮影することはほぼ不可能なので、紛失しないようにバックアップは重要である。Googleフォトにバックアップしていれば安全かもしれないが、やはり操作ミスやアカウント情報忘れなどでバックアップを削除してしまうことがある。特に機種変更後は端末には写真がなくGoogleフォトにしか写真がないので、バックアップが無い状態と同じだ。
パソコンを持っている場合は「Googleフォトを用いた家族の写真データ管理方法」なども参考にしてほしい、別に一人の人でも参考になると思う。

2018年4月1日日曜日

Google Apps Script で指定フォルダ以下のファイルを処理する方法(2)

概要
Google Apps Script で指定フォルダ以下のファイルを処理する方法(1)とは別の方法も記録として残しておく。どちらが良いかは検索範囲や階層の深さなどによるので一概にどちらが良いとは言えないが。フォルダ数が以上に多くファイル数がそれほど多くない場合は今回の方法が有利だが、あまりそういった環境が想像できないので基本的には前回の方法が良いと思う。
今回の方針はドライブ内のすべてのファイルを対象に処理を行い、対象のファイルの親フォルダをたどっていき、指定フォルダにたどり着くかをチェックする。もし指定フォルダにたどり着いたらそのフォルダの配下と判断し処理を行う。


今回はいきなり使い方も含めたサンプル
function sample()
{
  var files = DriveApp.getFiles();
  var TargetFolderID = "205Ps_mAhEFs.......Ugm1m";
  while( files.hasNext() )
  {
    var file = files.next();
    if( checkSubordinate(file,TargetFolderID) )
    {
      ////////////////////////////////////
      // ファイルごとの処理をここから記載する。
      var logString = file.getName()+":"+file.getId();
      Logger.log(logString);
      // ファイルごとの処理をここまでに記載する。
      ////////////////////////////////////
    }
  }
}

// targetが指定フォルダ(folder ID)の配下のフォルダかチェックする。
function checkSubordinate(target,folderID)
{
  var log = target.getName();
  var parents = target.getParents();
  while( parents.hasNext() )
  {
    var parent = parents.next();
    var id = parent.getId();
    if( id == folderID ){
      return true;
    }
    if( checkSubordinate(parent,folderID) )
    {
      return true;
    }
    Logger.log( log );
  }
  return false;
}

Google Apps Script で指定フォルダ以下のファイルを処理する方法(1)

概要
Google Apps ScriptでGoogleドライブの特定のフォルダ以下に含まれるファイルに対して処理を行いたい場合がある。そういった場合、サブフォルダも含め再帰関数を用いたり、配列などを用いて頑張ってループする方法もあるが、6分制限対策で処理を中断した際の再開方法が面倒です。
ということで指定したフォルダ以下のファイルを一つのFileIteratorオブジェクトとして取得する関数を作成した。その関数と使い方を簡単に説明する。

まずはいきなりソースコード
// 指定したフォルダ以下に含まれるすべてのファイルを列挙
function getFiles(folderID)
{
  // 「指定したフォルダ内に配置されている」という検索式を作る。
  var searchFileParams = Utilities.formatString("'%s' in parents", folderID);
  var searchFolderParams = searchFileParams;
  var folders;
  // フォルダ検索結果が空になるまで検索を続ける。

  while( (folders = DriveApp.searchFolders(searchFolderParams)).hasNext() )
  {
    // 最後に先頭の" or "を削除したいので先頭を示す"@"を入れておく
    searchFolderParams = "@";
    while( folders.hasNext() )
    {
      // このループでは検索式に" or 'XXX' in parents"を追加していく 
      var folder = folders.next();
      searchFolderParams += Utilities.formatString(" or '%s' in parents", folder.getId() );
    }
    // でき上がった検索式の先頭から不要な"@ or "を削除する。
    // searchFolderParamsは次の階層のフォルダ検索式
    searchFolderParams = searchFolderParams.replace( "@ or ","" );
    // searchFileParamsはファイルを一気に検索する検索式
    searchFileParams += " or " + searchFolderParams;
  }
  // 指定フォルダ以下全階層のファイルを一気に検索する。
  return DriveApp.searchFiles(searchFileParams);
}

簡単に説明すると、指定されたフォルダを親フォルダに含むファイルを検索するサーチ文を作成し、searchFilesで一気に検索を行う。
デバッグでsearchFileParamsを観察すると動きがわかる。

基本的な使い方
var files = getFiles("205Ps_mAhEFs.......Ugm1m");
while(files.hasNext())
{
  var file = files.next();
  ////////////////////////////////////
  // ファイルごとの処理をここから記載する。
  var logString = file.getName()+":"+file.getId();
  Logger.log(logString);
  // ファイルごとの処理をここまでに記載する。
  ////////////////////////////////////
}


本格的に使用する場合の使い方
フォルダ以下のファイルすべてに処理したいという要望の時は大体ファイル数が膨大なので、実際にはタイムアウトを考慮する必要がありますので、その場合の実装例。
// メニュー>実行>関数を実行>startupFunction を選択して実行してください
function startupFunction(){
  // まずはトリガをすべて削除する。
  var triggers = ScriptApp.getProjectTriggers();
  for (var i = 0; i < triggers.length; i++) {
    ScriptApp.deleteTrigger(triggers[i]);
  }

  // プロパティすべて消す。
  deleteProperties();
  
  // ここにフォルダIDを書く
  var TargetFolderID = ,"205Ps_mAhEFs.......Ugm1m";
  setProperty("TargetFolderID",TargetFolderID);

  // トリガを設定する。(10分間隔)
  trigger = ScriptApp.newTrigger("triggerFunction")
  .timeBased()
  .everyMinutes(10)
  .create();

  triggerFunction();
}

function triggerFunction() {
  var StartTime   = new Date();

  var ContinuationToken = getProperty("ContinuationToken");
  if( ContinuationToken != null )
  {
    var files = DriveApp.continueFileIterator(ContinuationToken);
  }
  else
  {
    var TargetFolderID = getProperty("TargetFolderID");
    var files = getFiles(TargetFolderID);
  }

  while(files.hasNext()) {
    var CurrentTime = new Date();
    // トリガを設定する。(5分間実行していたら止める。)
    if( (CurrentTime - StartTime) > 5*60*1000 ){
      setProperty("ContinuationToken",files.getContinuationToken());
      return;
    }
    var file = files.next();
    ////////////////////////////////////
    // ファイルごとの処理をここから記載する。
    var logString = file.getName()+":"+file.getId();
    Logger.log(logString);
    // ファイルごとの処理をここまでに記載する。
    ////////////////////////////////////
  }
  
  deleteProperties();
  var triggers = ScriptApp.getProjectTriggers();
  for (var i = 0; i < triggers.length; i++) {
    ScriptApp.deleteTrigger(triggers[i]);
  }
}

なおここで使用しているProperty関連の関数については「Google Apps Scriptで設定ファイルの保存・読込」を参照してほしい。ContinuationTokenが異常に長くなることがあり、PropertiesServiceクラスを用いて保存しようとすると失敗することがある。PropertiesServiceクラスは9217文字以上を指定すると失敗する模様。

関連情報

Googleフォトを用いた家族の写真データ管理方法

(2019/7に実施されたGoogleフォト/Googleドライブの仕様変更により、この記事の内容は使用できません。)

家族全員が各自のiPhoneで写真を撮影する。最初のうちはみんなのiPhoneをパソコンにつないで、個人毎、年月毎のフォルダに振り分けて、ファイル名も撮影日時に変更してからGoogleフォトにバックアップすることを行っていた。
そんなにたくさん撮るわけではないが、やはり旅行に行ったりすると一日で相当な枚数になるので面倒になった。いろいろと試行錯誤した結果、以下の形に落ち着いた。
  1. 各人のiPhoneで撮影した画像は、それぞれのアカウントGoogleフォトにバックアップされる。
  2. 各人のGoogleドライブGoogleフォトフォルダがありそちらに表示される。
  3. そのGoogleフォトフォルダ家族用のアカウントに共有する
  4. パソコンは家族用のアカウントに共有された全員のGoogleフォトフォルダをパソコンにダウンロードする。(これは専用の自作ツールを使用している。)
  5. そのダウンロードされた画像を「バックアップと同期(Backup and Sync from Google)」を用いてGoogleフォトにアップロードしなおす。
2018/3/31に作成
写真は①各自のiPhone②各自のアカウントのGoogleフォト③パソコン④家族用アカウントのGoogleフォトに存在することになる。①はiPhoneの容量を確保するためにちょくちょく削除するので、②③④の3か所にデータが存在することになる。
②は各自が操作ミスをすると消える可能性があるが、③はほとんど操作しないのでパソコンが壊れない限りなくなる可能性は低い、④も操作することはないので安心だ。

ちなみにこの図を作成していたら、以前にも同じような図を作成していたことを思い出した。アルバムアーカイブを探していたやはり以前も作成してた。今回の図と比較すると、デジカメの写真の管理方法が変わっている。当時はデジカメの写真をどのようにどのように管理するか悩んでいたようだ。
2016/8/8に作成
なにが変わったかというと、Lightning SDカードカメラリーダーを購入したことでデジカメの写真の運用がすっきりしていることです。以前はその運用方法を悩んでいる様子がうかがえる。
デジカメはあまり使用しないが、家族のだれが使用するか分からない。デジカメを使い終わったら、自分のiPhoneにSDカードカメラリーダーを接続して写真を取り込む、取り込むと自動で各自のGoogleフォトにアップロードされる。各自の責任で作業を行うので、私の管理ミスで画像をバックアップし忘れて気まずい雰囲気になることがない。
名前の通り、Lightningコネクタに接続してSDカードに保存されているデジカメのデータをカメラロールに読み込むためのものだが、アップル純正という安心感と写真の管理が一気に楽になることを考えると¥3,200 (税別)は非常にお買い得だった。
カメラデータ以外読み込めないし、リーダーというだけあって書き込むこともできない、サードパーティ製のSDカードアダプタはもう少し高機能かもしれないので、そういったものにも興味がある人は調べてから購入するとよい。

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と主にスクリプトの終了をメールで通知する。

    参考にした情報