ラベル Googleドライブ の投稿を表示しています。 すべての投稿を表示
ラベル Googleドライブ の投稿を表示しています。 すべての投稿を表示

2022年2月26日土曜日

GoogleDriveで共有されたデータをマイドライブにコピーする

G Suite無料のアカウントを無料で使い続ける方法を知らない時にドライブのデータを頑張って移動するために作ったスクリプトを紹介します。
オーナー権限を変更するスクリプトもあったのですが、G Suite(Google Workspace)の制約で組織外のユーザーに権限移行ができないため、今回はフォルダ階層を維持したままコピーするスクリプトを作成しましたり

アカウントを廃止するなどの目的でGoogleDriveで共有されたフォルダを自分のアカウントにコピーするスクリプトです。ご利用は自己責任で、、、
var me;
  function CopyFolders() {
  var url = "https://drive.google.com/drive/folders/6dpqdZ1ESK<< ここはコピー元のフォルダURL >>yPoQBdhwEOP";

  var idAndResoucekey = url.replace(/.*\//g,"").split("?");
  var id = idAndResoucekey[0];
  var resoucekey = null;
  me = Session.getActiveUser();
  if( idAndResoucekey.length == 2 )
  {
    resoucekey = idAndResoucekey[1].replace(/.*=/,"");
  }
  var srcFolder = DriveApp.getFolderByIdAndResourceKey(id,resoucekey);
  var dstFolder = getFolder( null, '共有フォルダからのコピー', false );
  CopyFolder( srcFolder, dstFolder );
  
}

function CopyFolder(src,dst){
  var folders = src.getFolders();
  var files = src.getFiles();
  while( folders.hasNext() )
  {
    var folder = folders.next();
    CopyFolder( folder, getFolder( dst, folder.getName() ) );
    if( src.getFiles().hasNext() )
    {
      Logger.log("削除できないファイルが残ってる?");
    }else{
      folder.removeEditor(me);
    }
  }
  while( files.hasNext() )
  {
    var file = files.next();
    try
    {
      file.makeCopy(file.getName(),dst);
      file.removeEditor(me);
    }
    catch(e)
    {
      Logger.log(file.getName()+" 削除失敗");
    }
  }
}


/**
 *  指定されたフォルダ取得する、フォルダがないときは新規に作成する。
 *
 *  @param {Folder} parent 基準フォルダ、null指定時はルートフォルダ
 *  @param {string|Array of strings} name 取得・生成するフォルダ名
 *      string 指定時はnameを取得。生成する
 *      Array of strings 指定時は多階層フォルダを一括作成
 *  @param {bool} noCreate 作成は行わず既存のフォルダを取得するときにtrue
 *      省略可能、省略時はfalseとして動作
 *  @return {Folder|undefined} 取得・生成したフォルダ
*/
function getFolder( parent, name, noCreate ) {
  // 親フォルダが指定されなかった場合はルートフォルダを取得する。
  if ( parent == null ){
    var folder = DriveApp.getRootFolder();
  }else{
    var folder = parent;
  }
  // フォルダ名に配列が指定されていない場合は配列にする。
  if( !Array.isArray( name ) ){
    name = [name];
  }
  // noCreateが指定されていない場合はfalseにする。
  if ( noCreate !== true ){
    noCreate = false;
  }

  while( name.length > 0 ){
    var newFolder = name.shift();
    var childs = folder.getFoldersByName( newFolder );
    if( childs.hasNext() ){
      // フォルダが見つかった場合は最初のフォルダを採用
      folder = childs.next();
    } else {
      if( noCreate ){
        // noCreateの場合はnullを返す
        return undefined;
      }else{
        // フォルダが見つからなかった場合はフォルダを作成
        folder = folder.createFolder( newFolder );
      }
    }
  }
  return folder;
}

2022年2月22日火曜日

Googleスプレッドシートに書かれたデータに関してWEBからデータを取得する

サンプルとしてGoogle Apps Scriptでスプレッドシートに書かれた郵便番号から住所を調べるスクリプトを作成した。郵便番号を取得するなら別の方法をお勧めする、あくまでGASの使い方説明の素材にしただけです。
エラー処理などを皆無だし、HTMLから必要なデータを抽出する部分も適当です。
スプレッドシートなどから呼び出した際にすぐに処理を抜けたい場合はstart関数を呼び出す、いきなり処理したい場合やデバッグ時はzipSearch関数を呼び出す。5分経過したらいったん中断し、1分後に処理を再開しまう。

A:B列を取得してB列が空の行のみを処理します。すべて処理が終わるか5分経過するかで結果をスプレッドシートに書き込みます。都度書き込むとAPI呼び出し回数が増え、処理も遅くなります。


function start(){
  // 1分後にスタート
  setTrigger("zipSearch");
}
function zipSearch() {
  // 一旦トリガを削除
  clearTrigger( arguments.callee.name );
  // スクリプト実行開始時間を取得(6分制限用)
  var StartTime   = new Date();

  // シートから入出力部分を取得
  // A列に郵便番号(”222-0001”)が入力されており、対応する住所をB列に入力する
  let ss = SpreadsheetApp.getActiveSpreadsheet().getSheetByName('シート1');
  let range = ss.getRange(1,1,ss.getLastRow(),2);
  let values = range.getValues();

  // 一行ずつ処理を実施
  values.forEach( row =>{
    if( row[0] != "" && row[1] == "" )
    {
      let url = "https://www.post.japanpost.jp/cgi-zip/zipcode.php?zip="+row[0];

      // 今回は<td class="data"><small>.*?</small></td>を抽出して処理
      let table = UrlFetchApp.fetch(url)
                  .getContentText()
                  .match(/<td class="data"><small>.*?<\/small><\/td>/gis)
     if( table!=null )
     {
       // 結合しタグを削除
       row[1] = table.join(" ").replace(/<.*?>/gis,"");
     }
     else
     {
       row[1] = "error";
     }
    }
    // スクリプト開始から5分経過したら一旦終了
    var CurrentTime = new Date();
    if( (CurrentTime - StartTime) > 5*60*1000 ){
      // 結果をシートに書き戻す
      range.setValues(values);
      //トリガをセットして終了
      setTrigger(arguments.callee.name);
      return;
    }
  });
  // 結果をシートに書き戻す
  range.setValues(values);
}

function setTrigger(func)
{
  trigger = ScriptApp.newTrigger(func)
            .timeBased()
            .everyMinutes(1)
            .create();
}

function clearTrigger(func)
{
  var triggers = ScriptApp.getProjectTriggers();
  triggers.forEach( trigger=>{
    if( trigger.getHandlerFunction() == func )
    {
      ScriptApp.deleteTrigger(trigger);
    }
  });
}

2022年2月20日日曜日

Googleドライブのフォルダに保存している画像をOCRしてドキュメントを生成する

画像をOCRして、画像とOCR結果が貼り込まれたドキュメントを生成するスクリプトです。

function main() {
  // マイドライブ\'GoogleAppsScript\dev\test-data\変換元データ
  var srcFolder = getFolder( null, ['GoogleAppsScript','dev','test-data','変換前データ'], false );
  var dstFolder = getFolder( null, ['GoogleAppsScript','dev','test-data','変換後データ'], false );


  let option = {
    "ocr": true,// OCRを行うかの設定です
    "ocrLanguage": "ja",// OCRを行う言語の設定です
  }
  var files = srcFolder.getFiles();
  while( files.hasNext() )
  {
    var file = files.next();
    var resource = {
      title: file.getName()
    };
    let doc = DriveApp.getFileById( Drive.Files.copy(resource, file.getId(), option).id);
    doc.moveTo( dstFolder );
  }

}

/**
 *  指定されたフォルダ取得する、フォルダがないときは新規に作成する。
 *
 *  @param {Folder} parent 基準フォルダ、null指定時はルートフォルダ
 *  @param {string|Array of strings} name 取得・生成するフォルダ名
 *      string 指定時はnameを取得。生成する
 *      Array of strings 指定時は多階層フォルダを一括作成
 *  @param {bool} noCreate 作成は行わず既存のフォルダを取得するときにtrue
 *      省略可能、省略時はfalseとして動作
 *  @return {Folder|undefined} 取得・生成したフォルダ
*/
function getFolder( parent, name, noCreate ) {
  // 親フォルダが指定されなかった場合はルートフォルダを取得する。
  if ( parent == null ){
    var folder = DriveApp.getRootFolder();
  }else{
    var folder = parent;
  }
  // フォルダ名に配列が指定されていない場合は配列にする。
  if( !Array.isArray( name ) ){
    name = [name];
  }
  // noCreateが指定されていない場合はfalseにする。
  if ( noCreate !== true ){
    noCreate = false;
  }

  while( name.length > 0 ){
    var newFolder = name.shift();
    var childs = folder.getFoldersByName( newFolder );
    if( childs.hasNext() ){
      // フォルダが見つかった場合は最初のフォルダを採用
      folder = childs.next();
    } else {
      if( noCreate ){
        // noCreateの場合はnullを返す
        return undefined;
      }else{
        // フォルダが見つからなかった場合はフォルダを作成
        folder = folder.createFolder( newFolder );
      }
    }
  }
  return folder;
}

2021年3月21日日曜日

Google PhotosとGoogle Driveのデータを他のアカウントに移行する方法

大学からもらったアカウントでGoogleフォトやGoogleドライブを運用していて、そのまま卒業して大事な青春時代のデータを失くす人がちらほらいるので、そうならないようにデータを移行する方法をまとめた。


Googleデータのエクスポート機能でパソコンのHDDにデータを保存するのが良い。その方法についてはhttps://support.google.com/accounts/answer/3024190?hl=jaに記載されているので参照するとよい。この際にデータをDriveに保存したりメールで受信する方法もあるが、アカウントが消えるとDriveにアクセスできなくなるし、メールも閲覧できなくなるので確実にパソコンにダウンロードしておくことをお勧めする。


Googleフォトのデータ移行

これについてはGoogleフォトのデータを別のアカウントに移行するを参照してください。


Google Apps ScriptによるGoogleドライブのデータ移行

Googleドライブにはデータ共有機能があるが共有しただけではオーナーアカウントが消えたときにデータがなくなってしまう。共有設定だけでなくオーナー変更も行う必要がある。

ファイル数が少ない場合は手動で行えばよいが、データが多いときに手動で実行するのは大変です。そこで登場するのがその手順を自動で実行してくれるGoogle Apps Script(GAS)です。
Google Apps Scriptを使ったことがない人はGoogle Apps Scriptの始め方はみてほしい。

当然すべてのケースを検証しているわけではないので保障はできないので、この方法でデータが紛失しても責任は負えません。事前にのGoogleデータのエクスポート機能でパソコンにダウンロードしておくことをお勧めします。

手順

  1. 移行元アカウントでGoogleドライブにログインしGoogle Apps Scriptを作成する。
    この時スクリプト名としてわかりやすい名前を付ける。
    後ほど紹介するスクリプトを張り付け、移行先のアカウントの指定部分だけ編集する。
  2. 移行元アカウントでGoogle Apps Scriptを実行する。
  3. 移行先アカウントに上記スクリプトも含めて移行されるが、マイドライブの下に紐づけられていないので、紐づけを行うために移行先アカウントでもう一度スクリプトを実行する。
    手順1で作成したスクリプトも移行されているはずなので、再度同じスクリプトをそのまま実行する。スクリプトが見つからない場合は手順1でつけたわかりやすい名前で検索する。

スクリプトの紹介

  • 下で紹介しているスクリプトの"移行先アカウント@gmail.com""移行先アカウント確認用@gmail.com"を編集してください。
    • 移行先のアカウントを間違えるとほかの人にデータが移動してしまうので注意が必要です。
    • "移行先アカウント@gmail.com"と"移行先アカウント確認用@gmail.com"とで別の値を設定しています。
    • 実際に動かすときは同じ値にしてください。
  • 以降元アカウントで実行するときと、移行先アカウントで実行するときで処理を分けています。(日本語の気持ち悪い関数名にしています。)
    • 移行元アカウントで実行するときは、すべてのフォルダおよびファイルのオーナーを変更します。
    • 移行先アカウントで実行すると、移行されたデータをマイドライブに紐づけます。

function myFunction() {
  ////////////////////////////////////////////////
  // ここだけカスタマイズが必要(同じ値を設定する)
  var 移動先アカウント       = "移行先アカウント@gmail.com";
  var 移動先アカウント確認用 = "移行先アカウント確認用@gmail.com";
  ////////////////////////////////////////////////
  if( 移動先アカウント != 移動先アカウント確認用 )
  {
    return;
  }

  var スクリプト実行アカウント = Session.getActiveUser().getUserLoginId();

  if( 移動先アカウント == スクリプト実行アカウント ) {
    移行先アカウントで実行する処理();
  }
  else
  {
    移行元アカウントで実行する処理(スクリプト実行アカウント,移動先アカウント);
  }
}

function 移行元アカウントで実行する処理(predecessorId,successorId) {
  var folders = DriveApp.getFolders();
  while (folders.hasNext()) {
    var folder = folders.next();
    try {
      folder.addEditor( successorId );
      folder.setOwner( successorId );
//      folder.removeEditor( predecessorId );
    }
    catch(e) {
      Logger.log(e)
    }
  }
  
  var files = DriveApp.getFiles();
//  var scriptId = ScriptApp.getScriptId();
  while (files.hasNext()) {
    var file = files.next(); 
    try {
      file.addViewer( successorId );
      file.setOwner( successorId );
//      if( scriptId != file.getId())
//      {
//        file.removeEditor( predecessorId );
//      }
    }
    catch(e) {
      Logger.log(e)
    }
  }
}

function 移行先アカウントで実行する処理() {
  var root = DriveApp.getRootFolder();

  var folders = DriveApp.getFolders();
  while( folders.hasNext() )
  {
    var folder = folders.next();
    var parents = folder.getParents();
    if( parents.hasNext() == false){
      root.addFolder(folder);
    }
  }

  var files = DriveApp.getFiles();
  while( files.hasNext() )
  {
    var file = files.next();
    var parents = file.getParents();
    if( parents.hasNext() == false){
      root.addFile(file);
    }
  }

}

注意事項

  • ここで記載した方法で何かトラブルが発生しても責任は負えません。解決に向けて多少の協力は行えるかもしれません。
  • 2021/3/21時点で動作確認しています、Google側の仕様変更などで動作が変更される場合がありますのでご注意ください。
  • 本格的に検証していません、スクリプトの意味が分かる人が内容を理解したうえで実行してください。Drive ServiceのAPIリファレンスを参照にしてください。
  • 移行先アカウントの容量が足りないときにどのようになるか検証できていません。
  • Google Apps Scriptは一回に実行できる時間に制限があります。1回実行し途中までしか移行できていない場合、コメントアウトしている3箇所(合計6行)のコメントアウトを外したうえですべてのデータが移行できるまで繰り返し実行してください。
    • 移行完了したファイルやフォルダが移行元アカウントから見えなくなるのでスクリプトを実行するたびに移行未完了データを処理できるようになります。
    • スクリプトファイルが見えなくなると繰り返し実行できなくなるので除外するようにしています。
  • Googleデータのエクスポート機能で別の場所に保存してから実行することをお勧めします。

2020年12月27日日曜日

Googleストレージポリシー変更

2021年6月からGoogleのストレージポリシーが変更になる。「Googleフォトが使えなくなる」とか「課金しないと2021年6月から写真が見れなくなる」など大げさにとらえている人が多い。

まずは公式の情報をしっかり読んでほしい。知りたい情報はどこかに書かれていることが多い。


公式情報

[Googleブログ記事]Google アカウントのストレージポリシー変更について

[ヘルプ記事]Google フォトのストレージに関する変更

[ヘルプ記事]Google ストレージの仕組みに関する今後の変更

[ヘルプ記事]サービスの利用状況と保存容量について(いつのまにかタイトルが変わっていて気がつきませんでしたが⑦と同じリンクでした)

[公式ヘルプコミュニティでの発信]Google アカウントのストレージの仕組みに関する変更

[ヘルプ記事]Pixel のストレージ情報

[ヘルプ記事] Google アカウント アクティビティについて


ざっくり解説

  • 無料で15GBのストレージ容量が与えられる、それ以上使用する場合は月額課金が必要:今回変更なし
  • Googleフォトで[元の画質]設定でバックアップした場合は、ストレージ容量を消費する:今回変更なし
  • Googleフォトで[高画質]設定でバックアップした場合は、ストレージ容量を消費するようになる
    2021年5月31日以前のアップロード:ストレージ容量を消費しない
    2021年6月1日以降のアップロード:ストレージ容量を消費する
    ※高画質設定だとファイルサイズが小さくなるので、消費容量も小さくなる。
    ※ Pixel 5 以前のPixelについては公式情報⑤参照
  • Googleドライブですべてのファイル形式でストレージ容量を消費するようになる
    2021年6月1日以降に、アップロード、作成、更新したデータがストレージ消費の対象
  • 2年間未使用のサービスはデータが削除される、実際に削除されるのは2023年6月1日以降(公式情報⑦)
  • アカウントの容量を超えた状態が2年間継続するとデータが削除される(公式情報②,公式情報③)


よくある質問

Q1.Googleフォトが使えなくなるのでしょうか?

A.継続して使えます

Q2.課金しないとデータが見れなくなるのでしょうか?

A.課金しなくてもデータは見れます

Q3.無料の15GBを使い切るとどうなりますか?

A.メールの送受信もできなくなります。GoogleドライブやGoogleフォトへのデータ追加もできなくなります。データを削除するか、Google One のメンバーシップを購入しストレージ拡張を行ってください。

Q4.現在消費している容量の確認方法は?

A.https://www.google.com/settings/storageにアクセスして確認してください。

Q5.高画質でアップロードしてきたGoogleフォトのデータ容量を調べたい

A.https://blog.hikozaru.com/2020/11/GooglePhotosCapacityEstimation.htmlを参考に調べてください。2021年5月31日以前にアップロードしたデータは2021年6月1日以降も引き続き容量カウントされずに保存されます。

Q6.課金していれば長期間未使用で勝手に削除されることはないでしょうか?

A. GoogleOneのパートナーシップを購入している場合にはログインしなくてもデータ削除されません(公式情報④)。 
ただし、Google One のメンバーシップでストレージ拡張していたとしても、購入した容量オーバーを2年間継続するとデータが削除されます。(公式情報②,公式情報③
 

Q7.GMAILにログインしておけばGoogleフォトの写真は消えませんか?

A.2年間未使用の判断はサービス毎です、Googleフォトに2年間ログインしないとGoogleフォトのデータは消されます。Google One のメンバーシップを購入している場合にはログインしなくてもデータ削除されません。詳細は公式情報④参照

Q8.アカウントを増やせば実質的に無制限になりますか?

A.Googleフォトの「不正使用に関するプログラム ポリシーと違反措置」には以下のように記載されています。アカウントに設定されている容量制限を回避する目的として複数アカウントを作成・使用するのは禁止されているように読めます。
13. 技術的保護手段の回避
Google のポリシーを回避したり、アカウントに設定された制限を回避したりすることを意図した行為に関与しないでください。これには、複数のアカウントの作成や使用、または以前は禁止されていた行為に関与するその他の方法が含まれます。

 

 Q9.2021年6月1日以降Googleフォトの[高画質]設定は意味がないのでしょうか?

A.容量を消費するようになりますが、元のサイズよりも容量消費が少なくなります。

 Q10.6/1にストレージ容量がリセットされないのは何故ですか?

A.今回の変更で容量をリセットされる事はありません。5月末日に消費していた容量は6月以降に引き継がれます。

 Q11.勝手に課金されてしまうのでしょうか?

A.Google Oneにメンバーシップ登録する手続きをしない限り勝手に課金されることは有りません。

 Q12.2年間サービスを未使用だと削除されるととのことですが、何をすると使用したことになりますか

A.公式情報⑦に記載されています。「サービス内のデータを有効な状態で維持する方法」や「2 年に 1 度必ず利用するかどうかわかりません。その場合、どうすればよいですか?」の部分を読んでください。


 

2020年9月19日土曜日

Googleフォトにバックアップできない動画をドライブ経由でアップロードする

目的

Googleフォトへのアップロード条件を満たす10GB以下の動画をバックアップする際に「バックアップできませんでした ファイルが大きすぎます」というエラーが表示されうまくバックアップできないことがあるようです。(Googleフォト ヘルプ コミュニティでの質問例

そんな時にはブラウザでGoogleフォトにアップロードという方法も試す価値はあると思うが、ブラウザ経由で直接Googleフォトにアップロードしても同じエラーが表示されるようです。(私自身がアップロードできない現象を再現できていません。)

そんな時にはGoogleドライブ経由でGoogleフォトにアップロードする方法を試してほしい。


概要

手順は以下の3ステップです。

  1. Googleドライブに動画をアップロードする
  2. ブラウザでGoogleフォトにアクセスしてドライブからアップロードする
  3. Googleドライブから動画を削除する

この情報だけで作業できる方は詳細手順を見る必要はありません。


詳細手順

スマホだけで実施しようと思うと結構面倒ですし、文章だけで説明するのは結構大変なので、図を使用して説明する。


  1. Googleドライブに動画をアップロード
    1. Safariブラウザ/ChromeブラウザでGoogleドライブ(ここをクリック)にログインする
      もしGoogleドライブアプリが起動した場合は「3.動画をアップロードする」に進んでください。

      ※すでにログインしている場合はログイン画面はスキップされる


    2. パソコン版のGoogleドライブに切り替える
      「≡」メニューから「パソコン版」を選択する。

      「”ドライブ”で開きますか?」と聞かれるが、今回はブラウザで操作するので「キャンセル」を選択する。

      以下のような画面に遷移したら「パソコン版」への切り替えが成功。


    3. 動画をアップロードする
      左上の「新規」から「ファイルのアップロード」を選択する。


      アップロード元として「フォトライブラリ」を選択して、フォトライブラリの中からアップロードしたい画像を選択し、右上の「完了」を押す。(Androidの場合も同様に写真を選択してください)


      なお、Googleドライブアプリの場合は以下です。


    4. 動画がアップロードされていることを確認する
      アップロード直後は動画アイコン表示される。しばらく待ち、再読み込みなどを行い、動画の先頭フレームの上に再生ボタンが表示されるまで待つ。

      ※待たなくてもよいのだが、ここで動画と認識されることを確認したい。動画と認識されていない場合はこの先の手順で失敗する可能性が高い。

  2. ブラウザでGoogleフォトにアクセスしてドライブからアップロードする
    1. GoogleドライブからGoogleフォトに移動する
      「Googleフォト」アプリを使用してもよいが、アカウントが異なるなどのトラブルを避けるためにブラウザ上でGoogleフォト(https://photos.google.com/login)に移動する。
      画面上の●が9個(3X3)並んでいるアイコンから「フォト」を選択する。


    2. Googleフォトでドライブからのアップロードを選択する
      ↑アイコンをタップしアップロード元として「Googleドライブ」を選択する。


    3. ドライブにアップロードした動画選択する
      Googleドライブの中から目的の動画を探し「アップロード」を行う。今回は「マイドライブ」にアップロードしたので「マイドライブ」の直下にある動画を選択した。


    4. アップロードサイズの設定を選択する
      「元のサイズ」でアップロードするか、「高画質」でアップロードするかを選択する。この設定の違いについてはヘルプの記事を参考にしてほしい。なお、記事の最後のほうにGoogleドライブからアップロードしたデータは「容量の解放」を行っても「高画質」に変換できないと書いてある(未検証)ので適当に選択しない方が良いかもしれない。


    5. アップロードされていることを確認する
      ここでもしばらくは灰色表示だが、しばらくすると正常になる。

      ※ここで動画が再生できるかも確認する。

  3. Googleドライブから動画を削除する。

    1. 再度SafariブラウザでGoogleドライブ(ここをクリック)にアクセスして動画を削除する。
      動画を選択し、右上のゴミ箱アイコンで削除する。


    2. ゴミ箱からも完全に削除する
      ゴミ箱に移動し、動画を選択し、右上のゴミ箱アイコンで完全に削除する。


注意事項

  • GoogleドライブにアップロードするとGoogleアカウントのストレージ容量を消費するので、十分な容量が開いていないときにはこの方法は使用できない。
  • 検証していないが、ヘルプの記事にGoogleドライブからアップロードしたデータは「容量の解放」を行っても「高画質」に変換できないと書いてある。

2020年7月26日日曜日

Google Driveで共有しているファイルやフォルダの一覧を表示する。

Googleドライブで共有しているものを探す方法を知りたい人がいたので、簡単にコードだけを張っておきます。(見てもらえそうなヒントだけ張っておきましたが、見てもらえるか、、、、)
といっても15,000程度のファイルを処理できるか検証途中です。

①GoogleのSpreadsheetでスクリプトエディタを開く
②以下のコードを張り付けて保存する
③メニューの「実行」>「関数を実行」>「start」を行う
④「ContinuationToken」というシートが追加され途中経過が保存される。
 6分間で処理しきれないので、処理を中断するためのトークンが保存されている
 A1セルおよびA2セルがFinishになったら処理終了
⑤結果は「SharedFolders」シートと「sharedFiles」シートに出力される。

  // メールに対する処理
function start()
{
  // ファイルとフォルダを並行して処理する
  // フォルダの処理は1分後にトリガ指定
  ScriptApp.newTrigger("CreateFolderList")
  .timeBased()
  .everyMinutes(5)
  .create();
  
  CreateFilesList();
  return;
}

function CreateFilesList() {
  
  // スクリプト実行開始時間を取得(6分制限用)
  var StartTime   = new Date();

  // トリガを削除する
  var triggers = ScriptApp.getProjectTriggers();
  for (var i = 0; i < triggers.length; i++) {
    if( triggers[i].getHandlerFunction() == arguments.callee.name )
    {
      ScriptApp.deleteTrigger(triggers[i]);
    }
  }

  // 出力シート
  var resultSheet = getSheet('SharedFiles');

  // ContinuationTokenを保存するセルを取得
  var ContinuationTokenRange = getSheet('ContinuationToken').getRange("A1");

  // ContinuationTokenを取得
  var ContinuationToken = ContinuationTokenRange.getValue();
  if( ContinuationToken == "" ){
    // ContinuationTokenが空の時は全ファイルを取得
    var targets = DriveApp.getFiles();
  }else if( ContinuationToken == "Finish" ){
    // 処理が終了している場合は実行しない。
    return;
  }else{
    // ContinuationTokenを取得できた場合はイテレータにセット
    var targets = DriveApp.continueFileIterator(ContinuationToken);
  }

  while( targets.hasNext() )
  {
    // 5分間実行したら処理を止めて、次のトリガーを設定する。
    var CurrentTime = new Date();
    if( (CurrentTime - StartTime) > 5*60*1000 ){
      var trigger = ScriptApp.newTrigger(arguments.callee.name)
      .timeBased()
      .everyMinutes(5)
      .create();
      ContinuationTokenRange.setValue(targets.getContinuationToken());
      return;
    }

    // 処理対象を取得
    var target = targets.next();
    
    // 共有モードを取得
    var access = target.getSharingAccess();
    
    if( DriveApp.Access.PRIVATE != access )
    {
      // 共有している場合は出力用のデータを取得
      var path = createPathString(target);
      var owner = target.getOwner().getEmail();
      var id = target.getId();
      resultSheet.getRange(resultSheet.getLastRow()+1,1,1,4).setValues([[path,owner,access,id]]);
    }
  }
  ContinuationTokenRange.setValue("Finish");
  return;
}

function CreateFolderList() {
  
  // スクリプト実行開始時間を取得(6分制限用)
  var StartTime   = new Date();

  // トリガを削除する
  var triggers = ScriptApp.getProjectTriggers();
  for (var i = 0; i < triggers.length; i++) {
    if( triggers[i].getHandlerFunction() == arguments.callee.name )
    {
      ScriptApp.deleteTrigger(triggers[i]);
    }
  }

  // 出力シート
  var resultSheet = getSheet('SharedFolders');

  // ContinuationTokenを保存するセルを取得
  var ContinuationTokenRange = getSheet('ContinuationToken').getRange("A2");

  // ContinuationTokenを取得
  var ContinuationToken = ContinuationTokenRange.getValue();
  if( ContinuationToken == "" ){
    // ContinuationTokenが空の時は全フォルダを取得
    var targets = DriveApp.getFolders();
  }else if( ContinuationToken == "Finish" ){
    // 処理が終了している場合は実行しない。
    return;
  }else{
    // ContinuationTokenを取得できた場合はイテレータにセット
    var targets = DriveApp.continueFolderIterator(ContinuationToken);
  }

  while( targets.hasNext() )
  {
    // 5分間実行したら処理を止めて、次のトリガーを設定する。
    var CurrentTime = new Date();
    if( (CurrentTime - StartTime) > 5*60*1000 ){
      var trigger = ScriptApp.newTrigger(arguments.callee.name)
      .timeBased()
      .everyMinutes(5)
      .create();
      ContinuationTokenRange.setValue(targets.getContinuationToken());
      return;
    }

    // 処理対象を取得
    var target = targets.next();
    
    // 共有モードを取得
    var access = target.getSharingAccess();
    
    if( DriveApp.Access.PRIVATE != access )
    {
      // 共有している場合は出力用のデータを取得
      var path = createPathString(target) + "/";
      var owner = target.getOwner().getEmail();
      var id = target.getId();
      resultSheet.getRange(resultSheet.getLastRow()+1,1,1,4).setValues([[path,owner,access,id]]);
    }
  }
  ContinuationTokenRange.setValue("Finish");
  return;
}

// targetファイル/フォルダの親フォルダをループしてパスを生成する
function createPathString(target)
{
  var path = target.getName();
  var cur = target;
  while( cur.getParents().hasNext() ){
    cur = cur.getParents().next();
    path = cur.getName() + "/" + path;
  }
  return path;
}

// 名前を指定してシートを取得、シートがないときは新規に作成する
function getSheet(sheetName)
{
  var ss = SpreadsheetApp.getActiveSpreadsheet();
  var sheet = ss.getSheetByName(sheetName);
  if( sheet === null )
  {
    sheet = ss.insertSheet(sheetName)
  }
  return sheet;
}

2019年9月8日日曜日

Googleのバックアップと同期の設定画面

Googleのバックアップと同期の設定画面は文章だけで説明するのが面倒なので設定画面のキャプチャ画像を張ってみた。

1.タスクトレイの通知領域に表示されるGoogleのバックアップと同期アイコン



1-1.待機状態のGoogleのバックアップと同期アイコン

1-2.同期実施状態のGoogleのバックアップと同期アイコン

2.Googleのバックアップと同期アイコンクリック時のポップアップメニュー


2-1.Googleドライブアイコンのポップアップメニュー

3.Googleのバックアップと同期設定画面

3-1.マイ パソコン


3-1-1.設定画面:マイ パソコン
バックアップしたいフォルダにチェックを入れます
バックアップしたいフォルダを①のリストに追加します
バックアップ対象のファイルを選択します。
「すべてのファイル」か「写真と動画だけ」か選びます。
写真と動画のアップロードサイズを選択します。
「高画質」か「元の画質」か選べます。
「高画質」は画質の変化を伴います。
ファイル削除時の動作を選択します。Googleフォトには影響を与えません。
常に両方のコピーを削除する: パソコンまたは drive.google.com からアイテムを削除すると、他の場所からも削除されます。
両方のコピーを削除しない: パソコンからアイテムを削除しても、drive.google.com からは削除されません。
両方のコピーを削除する前に確認メッセージを表示する: パソコンからアイテムを削除するときに、他の場所からも削除するかどうかを確認するメッセージが表示されます。
写真をGoogleフォトにアップロードするときにチェックを入れます
カメラなどのデータを直接アップロードしたいときに設定します。

3-2.Googleドライブ


3-2.設定画面:Googleドライブ

3-3.設定


3-3.設定画面:設定

























2019年1月2日水曜日

Google Apps Script でプロパティをファイルに保存する

GoogleAppsScriptは便利だがいろいろと困ることがある、その代表格は6分制限。スクリプトの実行時間が6分を超えると途中で処理が終了してしまいます。 その対応として実行経過を保存し時間駆動型のトリガーを設定して、細切れに処理をする必要がある。これについてはGoogleで検索すれば山のように情報が出てくるので割愛する。
今回その途中経過を保存する方法について書いてみる。 一般的にはProperties ServicesetProperty(key, value)などを用いると非常に楽である。しかし、9217文字以上の文字列を扱おうと思うとエラーが発生してしまう。

var scriptProperties = PropertiesService.getScriptProperties();
scriptProperties.setProperty(
  'key_string',  // keyにもサイズ制限があるかもしれないが未検証
  'ここに文字数が9217以上の文字列を指定、XXXXXXXXXX');
}

上の例では以下のようにエラーではじかれる。
引数が大きすぎます:value(行 nn,ファイル「コード」)
そこまで大きい値を扱わない場合には問題ないが、DriveAPIで複雑な検索式でsearchFilesを行った場合のContinuationTokenがこの上限を大幅に超えることがある。9216文字以下になるように分割して複数のプロパティに保存することで対処可能ではあるが、今回はプロパティをファイルに保存する方法を考えてみたので忘れないようにメモ。使い方はそのうち別記事で記載します。

初回起動時にプロパティファイルを生成し、そのファイルIDをProperties Serviceで保存しておく。ファイルIDが変わらない限りはプロパティファイルの名前が変更されても参照が継続する。使い勝手が良いのか、悪いのかはよくわからない。
// 使い方はfunction examplePersistentProperty()を参照

// PersistentProperty()
// 概要:クラスのコンストラクタ
// 引数:filename  プロパティ保存する初期ファイル名
// 戻り値:インスタンス
// 使い方:
//   var pp = new PersistentProperty('PersistentPropertyTest');
// 備考:一度生成したファイルを参照し続ける。ファイル名を変えても参照し続ける。
PersistentProperty = function(filename){
  this.file = null;
  var id =  PropertiesService.getUserProperties().getProperty("PropertiesFileID");
  try{
    if( id!=null )
    {
      // ファイル取得を試みる
      this.file = DriveApp.getFileById(id);
      if( this.file.isTrashed() )
      {
      // ユーザープロパティから取得したファイルがゴミ箱にある場合には廃棄する
        this.file=null;;
      }
    }
  }
  catch(e)
  {
    // ファイル取得に失敗した場合は
    this.file=null;
  }
  // プロパティファイルがない場合には新規に作成する。
  if( this.file==null )
  {
    var name = filename;
    this.file =  DriveApp.createFile(name,"{}",MimeType.PLAIN_TEXT);
    var fileId = this.file.getId();
    PropertiesService.getUserProperties().setProperty("PropertiesFileID",fileId);
  }

  // ファイルからテキスト読み込み
  var text =  this.file.getBlob().getDataAsString();
};


// deleteAllProperties()
// 概要:保存されているプロパティをすべて消去
// 引数:なし
// 戻り値:なし
// 使い方:
//   var pp = new PersistentProperty('PersistentPropertyTest');
//   pp.deleteAllProperties();
// 備考:
PersistentProperty.prototype.deleteAllProperties = function(){
  this.file.setContent(JSON.stringify({}));
};

// deleteProperty(key)
// 概要:指定したプロパティを消去
// 引数:key  削除するプロパティを指定するキー
// 戻り値:なし
// 使い方:
//   var pp = new PersistentProperty('PersistentPropertyTest');
//   pp.deleteProperty('PropaertyKeyString');
// 備考:
PersistentProperty.prototype.deleteProperty = function(key){
  var text =  this.file.getBlob().getDataAsString();
  var Properties =  JSON.parse(text);

  try{
    // 指定されたプロパティ削除
    delete Properties[key];
  }
  finally
  {
    // 最終的にオブジェクトをファイルに保存
    this.file.setContent(JSON.stringify(Properties));
  }  
};

// getKeys()
// 概要:保存しているプロパティのキーの一覧を取得
// 引数:なし
// 戻り値:キーの一覧(string[])
// 使い方:
//   var pp = new PersistentProperty('PersistentPropertyTest');
//   var keys = pp.getKeys()
// 備考:
PersistentProperty.prototype.getKeys = function(){
  var text =  this.file.getBlob().getDataAsString();
  var Properties =  JSON.parse(text);

  var keys = [];
  for (key in Properties) {
    keys.push(key);
  }
  return keys;
};

// getProperties()
// 概要:保存しているプロパティリストを取得
// 引数:なし
// 戻り値:保存しているプロパティリスト(連想配列)
// 使い方:
//   var pp = new PersistentProperty('PersistentPropertyTest');
//   var properties = pp.getProperties();
//   var value = properties['PropaertyKeyString'];
// 備考:
PersistentProperty.prototype.getProperties = function(){
  var text =  this.file.getBlob().getDataAsString();
  var Properties =  JSON.parse(text);
  return Properties;
};

// getProperty(key)
// 概要:指定したプロパティを取得
// 引数:key  取得するプロパティを指定するキー
// 戻り値:プロパティの値
// 使い方:
//   var pp = new PersistentProperty('PersistentPropertyTest');
//   var value = pp.getProperty('PropaertyKeyString');
// 備考:
PersistentProperty.prototype.getProperty = function(key){
  var text =  this.file.getBlob().getDataAsString();
  var Properties =  JSON.parse(text);

  if( key in Properties ){
    return Properties[key];
  }else{
    return null;
  }
};

// setProperties(properties,deleteAllOthers)
// 概要:プロパティリストを設定
// 引数:properties  設定するプロパティリスト(連想配列)
//   :deleteAllOthers  trueを指定すると他のプロパティをクリア、書略時はfalseとして動作する
// 戻り値:なし
// 使い方:
//   var pp = new PersistentProperty('PersistentPropertyTest');
//   pp.setProperties({ "key1":"value1","key2":"value2"},true); // 他のプロパティは削除
//   pp.setProperties({ "key3":"value3","key4":"value4"}); // プロパティを追加
// 備考:deleteAllOthersがfalseの場合はプロパティリストの追加
PersistentProperty.prototype.setProperties = function(properties,deleteAllOthers){
  var text =  this.file.getBlob().getDataAsString();
  var Properties =  JSON.parse(text);

  if( deleteAllOthers ){
    Properties = properties;
  }else{
    for( var key in properties ){
      Properties[key] = properties[key];
    }
  }
  var json = JSON.stringify(Properties);
  this.file.setContent(json);
};

// setProperty(key,value)
// 概要:プロパティを設定
// 引数:key  設定するプロパティのキー
//   :value  設定するプロパティの値
// 戻り値:なし
// 使い方:
//   var pp = new PersistentProperty('PersistentPropertyTest');
//   pp.setProperty( "key1", "value1" );
// 備考:
PersistentProperty.prototype.setProperty = function(key,value){
  var text =  this.file.getBlob().getDataAsString();
  var Properties =  JSON.parse(text);

  Properties[key] = value;
  this.file.setContent(JSON.stringify(Properties));
};

//////////////////////////////////////////////////////////////////////////////
// これ以降は使い方サンプル及びDEBUG用
//////////////////////////////////////////////////////////////////////////////
function examplePersistentProperty()
{
  // ファイル名を決める
  var pp = new PersistentProperty('PersistentPropertyTest');
  
  // プロパティのセット
  pp.setProperty( "test1","test1 value" );

  // プロパティを複数追加
  pp.setProperties(
    { "test2":"test2 value","test3":"test3 value"});

  // プロパティを複数追加
  pp.setProperties(
    { "test4":"test4 value","test5":"test5 value"},false );

  // 一つずつ取得してもよい
  var test1 = pp.getProperty("test1");
  
  // 一気に取得してもよい
  var properties = pp.getProperties();
  var test2 = properties["test2"];
 
  // プロパティを一部削除
  pp.deleteProperty("test4");
  
  // プロパティを一部削除や追加し一気に更新
  delete properties["test5"];
  properties["test6"] = "New Property";
  pp.setProperties(properties,true);
  
  // 全プロパティを列挙
  var keys = pp.getKeys();
  for( var i=0; i<keys.length; i++ ){
    var key = keys[i]
    var value = pp.getProperty(key);
  }

  // 全プロパティを削除
  pp.deleteAllProperties();
}

GMAILに受信したメールをGoogle Apps Scriptで処理する

GMailに受信したメールを処理するスクリプトを作成した。
検索を行いその結果に対して処理を行うが、一度処理したメールは再度処理しないようにしなければならない。処理をしたメールにたいし、ラベルをつけたり、既読化したり、スターをつけたりすることで対処しようとしているページが多い。ラベルや既読化はスレッド単位で検索されてしまうので問題が発生するし、既読化やスターをつける方法は複数のスクリプトで処理したいメールがある場合に対応できない。

今回はメール受信日時で処理済みかどうかを判断する方法を採用した。時間駆動トリガで定期的に実行することを想定しているが、トリガを仕掛ける処理は組み込んでいないので、手動でトリガ指定する必要がある。処理量が多いと一回のスクリプト実行時間(6分)では処理しきれないこともあるため中断することも考慮に入れている。
目的に合わせて青字の部分をカスタマイズして使用する。


/// トリガ設定は行っていないので、手動で指定する必要があります。

function Main() {
  var ScriptStartTime = new Date();
//////////////////////////////////////////////////////////////////////////////////////
// カスタマイズはここから
////////////////////////////////////////
  // メールを検索する条件(beforとafterは指定しない)
  var SearchString = 'in:all -in:spam -in:trash has:attachment';
    
  // 処理対象期間
  var SearchStartDate = new Date('2018/10/00 0:00:00');
  var SearchEndDate = new Date(ScriptStartTime);

  // 一回の検索で検索する期間:5分で処理できる量を指定するのが一番良い。
  var IncrementHours = 24;

  // メールに対する処理(例:メール受信日時フォルダを生成し、その中に添付ファイルを保存する)
  var Action = function(mail)
  {
    var DriveFolderName = "GmailAttachments";  
    var reciveDate = mail.getDate();
    var folderID = getDriveFolder([DriveFolderName,reciveDate.getFullYear(),reciveDate.getMonth()+1,reciveDate.getDate()]);
    var attachments = mail.getAttachments();
    for (var n = 0; n < attachments.length; n++)
    {
      folderID.createFile(attachments[n]);
    }
  }  
////////////////////////////////////////
// カスタマイズはここまで
//////////////////////////////////////////////////////////////////////////////////////

  var properties = PropertiesService.getScriptProperties().getProperties();
  
  // 検索開始日時をプロパティから取得する
  if( 'SearchStartDate' in properties ){
    SearchStartDate = new Date(properties['SearchStartDate']);
  }

  // 実施すべきジョブ(MailIDの配列)をプロパティから取得する。
  var JobList = [];
  if( 'JobList' in properties ){
    JobList = JSON.parse(properties['JobList']);
  }

  for( var start = SearchStartDate; start < SearchEndDate; )
  {
    // メール検索期間の設定
    var end = new Date(start);
    end.setHours(end.getHours() + IncrementHours);
    if( end > SearchEndDate ){
      end = new Date(SearchEndDate);
    }
    
    // JobListの生成(処理すべきメールのIDリスト)
    if( Object.keys(JobList).length == 0 )
    {
      JobList = getMailIdList(SearchString,start,end);
    }
    
    // JobListに従い処理実行
    while( JobList.length > 0 )
    {
      // スクリプト開始から5分経過している場合は実行経過をプロパティに保存して処理を中断する。
      var CurrentTime = new Date();
      if( (CurrentTime - ScriptStartTime) > 5*60*1000 )
      {
        properties['SearchStartDate'] = start;
        properties['JobList'] = JSON.stringify(JobList);
        PropertiesService.getScriptProperties().setProperties( properties );
        return;
      }
      
      Action(GmailApp.getMessageById(JobList.shift()));   // 処理実行関数呼び出し
    }
    
    // 検索期間を更新
    start = end;
  }
  properties['SearchStartDate'] = start;
  properties['JobList'] = JSON.stringify(JobList);
  PropertiesService.getScriptProperties().setProperties( properties );
}

// 指定期間で検索を行い、指定期間に受信したメールのIDを配列で取得する
function getMailIdList( searchString, start, end )
{
  var after = 'after:' + Math.floor( start.getTime()/1000 ).toString();
  var before = 'before:' + Math.floor( end.getTime()/1000 ).toString();
  const search = after + ' ' + before + ' ' + searchString;

  var mailIdList = [];
  var indexStart = 0;
  while( true )
  {
    var myThreads = GmailApp.search(search, indexStart, 500);
    if( myThreads.length==0)
    {
      return mailIdList;
    }
    indexStart+=myThreads.length;
    
    var myMsgs = GmailApp.getMessagesForThreads(myThreads);
    for(var thread = 0;thread < myMsgs.length;thread++){
      for(var msg = 0;msg < myMsgs[thread].length;msg++){
        var reciveDate = myMsgs[thread][msg].getDate();
        if( start < reciveDate && end > reciveDate )
        {
          mailIdList.unshift(myMsgs[thread][msg].getId());
        }
      }
    } 
  }
}

//////////////////////////////////////////////////////////////////////////////////////
// カスタマイズ部分から呼び出している関数はここから
////////////////////////////////////////
// 名前を指定したフォルダ生成する、すでに存在する場合はそのフォルダを取得する
// 階層構造を一気に作成できます。
// 作成したいフォルダ:(RootFolder)/folderA/folderB/folderC
// 呼び出し方:getDriveFolder(["folerA","folderB","folderC"]);
function getDriveFolder( folderHierarchy, folder ) {
  if ( folder === undefined ) folder = DriveApp.getRootFolder();    
  for(var h = 0;h < folderHierarchy.length; h++ ){
    var childs = folder.getFoldersByName(folderHierarchy[h]);
    if( childs.hasNext() ) {
      // 指定された名前のフォルダがあるときはそのフォルダを使用
      folder = childs.next();
    } else {
      // 指定された名前のフォルダが見つからなかったときはフォルダを生成
      folder = folder.createFolder( folderHierarchy[h] );
    }
  }
  return folder;
}
////////////////////////////////////////
// カスタマイズ部分から呼び出している関数はここまで
//////////////////////////////////////////////////////////////////////////////////////

メールから必要な部分を抜き出してスプレッドシートに張り付ける場合のAction関数は以下のような感じで実現した。

  // メールに対する処理
  var Action = function(mail)
  {
    // 処理サンプル:以下のメールから必要な部分をシートに張り付ける。
      //    氏 名 様
      //    
      //    いつも ANAプリペシルバーマイルコース をご利用いただき、
      //    ありがとうございます。
      //    氏 名 様のカードのご利用がありましたのでご連絡します。
      //    
      //     【ご利用明細の承認番号】
      //      1234567
      //    
      //     【ご利用カード番号】
      //      ****-****-****-0000
      //      ※セキュリティ保護のため、下4桁のみ表示しています。
      //    
      //     【ご利用日時(日本時間)】
      //      2018/12/29 17:20
      //    
      //     【ご利用先】
      //      セブン-イレブン
      //    
      //     【ご利用金額】
      //      1,234 円
      //    
      //     【バリュー残高】
      //      123,456 円
      //    
      //    ご利用先は、加盟店からの売上情報到着前の情報となり、実際のご利用先と名称が異なる場合があります。
      //    (売上情報到着後の名称については、マイページにログインのうえ「ご利用明細の確認」メニューよりご確認ください。)
      //    なお、携帯電話料金などの月額料金のお支払いに登録され、
      //    9:00PM以降にバリュー残高からの加算・減算が発生した場合は、翌8:00AM以降のメール送信となります。
      //    
      //    ご不明点などありましたら、JCBプリペイドカードデスクにお問い合わせください。    
    
    
    var sheet = SpreadsheetApp.getActiveSheet();
    var valTitle = [["【ご利用日時(日本時間)】","【ご利用先】","【ご利用金額】","【バリュー残高】","【ご利用明細の承認番号】","【ご利用カード番号】"]];
    var i = sheet.getLastRow();
  
    if(i==0){
      sheet.getRange(1, 1, 1, valTitle[0].length).setValues(valTitle); //シートに貼り付け
      i=1;
    }
    var bodys = mail.getPlainBody().replace(/[\t \r]/g,'').split("\n");
    var valMsgs=[];
    valMsgs[0]=[];
    for(var l = 0;l < valTitle[0].length; l++ ){
      valMsgs[0][l] = bodys[bodys.indexOf(valTitle[0][l])+1];
    }
    sheet.getRange(i+1, 1, 1,valTitle[0].length).setValues(valMsgs); //シートに貼り付け
  }

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月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文字以上を指定すると失敗する模様。

関連情報

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日日曜日

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

    参考にした情報