バランスラボの椅子を購入しました

ラクに姿勢がよくなる椅子|バランス イージー バランスラボ – バランスチェアのbalans lab (バランスラボ) 公式サイト

長女が机に向かうときの椅子としてバランスラボの椅子を購入しました。合う合わないが別れる椅子だと思いますが、長女の場合はのノートと目の距離が近くなりがちという姿勢の問題が改善されたので、購入して良かったと思います。変わった座り方になることもあり、合わない方は安定して座れないことも多いそうです。長女の場合は、試し座りに行ったお店でもきれいに座れていたので、相性がよかったのだと思います。

この椅子の特徴としては、下記の点があります。

  • 座面の高さが変えられない
  • 足が床につかないので、椅子の位置を変更しづらい (一度椅子から降りる必要がある)

座面の高さが変えられないのは、机の高さが一般的な70cm前後ではない机を利用されている場合に問題があると思います。身長が130cmくらいの長女の場合は、70cmの机だとあと2cmくらい高くしてもいいかなくらいのサイズでした。机の高さを返るか、椅子を置く床をかさ上げするかになるので、5cm以上の調整は難しいのではないかと思います。

GoogleDriveの特定フォルダ配下のフォルダ・ファイルの権限をチェックするGoogleAppsScriptを作りました

実行するアカウントが参照出来る範囲で、GoogleDriveの指定フォルダ配下のアクセス権限を調査するGoogleAppsScriptを作成しました。

folderIdで指定したフォルダ配下のフォルダ・ファイルの調査結果を、spreadsheetUrlで指定したスプレッドシートまたはこのスクリプトのコンテナになるスプレッドシートのsheetNameで指定したシート名のシートに書き出します。書き出されたスプレッドシートのイメージは下記表です。

階層種別IDパスアクセス範囲アクセス権限オーナー編集権限所有者参照権現所有者アクセス権限継承サブフォルダチェック
1FolderxxxxxyyyyPRIVATENONExxx@xxx.comxxx@xxx.comyyy@xxx.comFALSETRUE
2Filecccccyyyy/zzzzPRIVATENONExxx@xxx.comxxx@xxx.comyyy@xxx.comTRUE
2Folderdddddyyyy/aaaaPRIVATENONExxx@xxx.comxxx@xxx.comyyy@xxx.com,zzz@xxx.comFALSETRUE

アクセス権限継承という列は親フォルダと、”アクセス範囲”、”アクセス権限”、”オーナー”、”編集権限所有者”、”参照権限所有者”が一致している場合にTRUEで、一致していない場合にFALSEで表示されます。

サブフォルダチェックという列は、該当行のフォルダの一階層したのフォルダ・ファイルのチェックが終わっていない場合はTRUEで、終わっていない場合はFALSEで表示されます。1回のスクリプト実行で指定フォルダ配下のフォルダ・ファイルのチェックが全て終わらないため、どこまでチェックが終わっていて、どこから調査を再開すればいいかを判断するためのフラグとして利用しています。種別がFileの行は空行、種別がFolderの行はTRUEまたはFALSEが表示されます。全てのフォルダのチェックが終わったら、全フォルダの行がTRUEで表示されます。

調査できるフォルダ・ファイルはスクリプトを実行するアカウントの権限に左右されます。実行アカウントが参照出来ないフォルダ・ファイルはリストアップされません。実行アカウントが編集できないフォルダ・ファイルの編集権限所有者と参照権限所有者は表示されません。

1回のスクリプト実行時間はGoogleWorkspaceのBusinessの実行時間の30分を想定して下記スクリプトを作成していますので、実行時間が6分の環境の場合は、想定通りに動かない可能性があります。具体的にはfolderIdで指定したフォルダの直下のフォルダ・ファイルのチェックが6分以内に終わらない場合は、スプレッドシートに調査結果が書き出されません。folderIdで指定したフォルダ直下にあるフォルダ・ファイルの数が300以上あると6分以内に終わらない可能性がでてきます。また実行時間が6分の環境では、処理を中断して再実行用のトリガーを設定する部分を書き換える必要があります。

このスクリプトをご利用される場合は、まずフォルダ数・ファイル数が少ないフォルダに対して実行いただき、動きを確かめていただけますようお願いいたします。もっといい書き方を教えていただけると大変ありがたいです。


2023年8月9日追記
調査完了後に、調査結果をSortしてFilterを作る部分にバグがあったので修正しました。修正箇所は、下記の赤文字部分です。

<<修正前>>

  // スプレッドシートに書き出されたデータがある場合は、調査が途中である可能性があるため、調査を再開するためのトリガーを設定する
  // 書き出されたデータがない場合は、調査終了と判断し、調査結果を階層が見やすいようにソートする
  if (currentRange != sheet.getDataRange()) {
      setTrigger_();
  } else {
      // 絶対パス名で昇順ソートし、各項目でフィルターできるようにフィルターを作成する
      // ソートは2階層目のサブフォルダ以降の調査結果に対して行う(1階層目のフォルダと、2階層目のファイルはソート済なので対象にしない
      let sortStartRowCnt = 1
      for (let value of values) {
        if (value[0] == 2 && value[1] == 'Folder') break;
        sortStartRowCnt++;
      }
      sheet.getRange(sortStartRowCnt, 1, maxRow, maxCol).sort([{column: 4, ascending: true}]);
      sheet.getRange(1, 1, maxRow, maxCol).createFilter();
  }

<<修正後>>

  // スプレッドシートに書き出されたデータがある場合は、調査が途中である可能性があるため、調査を再開するためのトリガーを設定する
  // 書き出されたデータがない場合は、調査終了と判断し、調査結果を階層が見やすいようにソートする
  if (currentRange.getLastRow() != sheet.getDataRange().getLastRow()) {
      setTrigger_();
  } else {
      // 絶対パス名で昇順ソートし、各項目でフィルターできるようにフィルターを作成する
      // ソートは2階層目のサブフォルダ以降の調査結果に対して行う(1階層目のフォルダと、2階層目のファイルはソート済なので対象にしない
      let sortStartRowCnt = 1
      for (let value of values) {
        if (value[0] == 2 && value[1] == 'Folder') break;
        sortStartRowCnt++;
      }
      sheet.getRange(sortStartRowCnt, 1, maxRow - (sortStartRowCnt - 1), maxCol).sort([{column: 4, ascending: true}]);
      sheet.getDataRange().createFilter();
  }

<<全体>>

//----------------
// パラメタ設定 
//----------------
// 書き出し用スプレッドシートURL
// (スプレッドシートをコンテナとしたスクリプトとして利用する際は''のままでOK)
const spreadsheetUrl = '';
//const spreadsheetUrl = 'https://docs.google.com/spreadsheets/d/xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx/';

// 書き出し用スプレッドーシートのシート名
const sheetName = 'Sheet1';

// 調査対象フォルダのフォルダID
const folderId = 'yyyyyyyyyyyyyyyyyyyyyyyyyyyyyy';

// デバッグフラグ
// trueにすると、Excecution logに、アクセス権情報を1件作成する度に表示する
const debugFlg = false;




/**
 * 指定フォルダ(サブフォルダ・ファイル含む)の調査を開始する
 * 調査結果は指定したスプレッドシートに書き出す
 */
function initialCheckPermission() {
  // 初期調査が30分以内に終わらないことは想定していない  

  const sheet = getSheet_();                                // 調査結果を書き出すスプレッドシート
  const folder = getFolder_(folderId);                      // 調査対象フォルダ
  const absolutePath = folder.getName();                    // 調査対象フォルダのフルパス
  const checkLimitDepth = getPathDepth_(absolutePath) + 1;  // 調査する階層の深さ(調査対象フォルダの直下を指定)
  let folderData = new Array();                             // 調査したアクセス権を保存する変数
  let parentPermissionInfo = new Array();                   // 親フォルダのアクセス権情報(最上位フォルダのため空配列)

  // 調査結果を書き出すスプレッドシートの内容をクリアして、指定フォルダのチェックを行う
  refleshSheet_(sheet);
  checkFolder_(folder, absolutePath, checkLimitDepth, parentPermissionInfo, folderData);

  // 調査結果をスプレットシートに書き出す
  let maxRow = sheet.getDataRange().getLastRow();
  sheet.getRange(maxRow + 1, 1, folderData.length, folderData[0].length).setValues(folderData);

  // さらに深い階層を調査するために調査再開トリガーを設定する
  setTrigger_();
}

/**
 * 中断したフォルダ・ファイルのアクセス権調査を再開する
 * アクセス権調査結果が書き出されているスプレッドシートを参照し、
 * "サブフォルダチェック済"欄が"FALSE"のフォルダを対象にチェックを行う
 */
function continueCheckPermission() {
  const startTime = new Date();                 // 調査開始時刻

  const sheet = getSheet_();                     // 調査結果を書き出すスプレッドシート

  const currentRange = sheet.getDataRange();    // 記載されているデータ範囲のオブジェクト
  const minRow = currentRange.getRow();         // 記載されているデータ範囲の最小行数
  const maxCol = currentRange.getLastColumn();  // 記載されているデータ範囲の最大列数
  let maxRow = currentRange.getLastRow();       // 記載されているデータ範囲の最大行数
  const values = currentRange.getValues();      // 記載されているデータを2次元配列化したデータ

  let folderData = new Array();                 // 調査したアクセス権を保存する変数
  let folder;                                   // 調査対象フォルダのフォルダオブジェクト
  let checkLimitDepth;                          // アクセス権チェックを行う最大階層

  // 実行済のトリガーを削除する
  deleteTriggers_();

  let rowCnt = minRow;
  for (let value of values) {
    // サブフォルダチェック未のフォルダに対して、アクセス権チェックを行う
    if (value.length == maxCol && value[1] == 'Folder' && value[maxCol - 1] == false) {
      folderData = new Array();

      folder = getFolder_(value[2]);                // 調査対象フォルダ
      absolutePath = value[3];                      // 調査対象フォルダのフルパス
      checkLimitDepth = value[0] + 1;               // 調査する階層の深さ(調査対象フォルダの直下を指定)

      // 調査対象フォルダ直下のファイル・フォルダを調査して、調査結果をスプレッドシートに出力し、
      // スプレッドシートの対象フォルダのれ行の"サブフォルダチェック"列をTRUEに変更する
      checkSubContents_(folder, absolutePath, checkLimitDepth, value, folderData);
      if (folderData.length > 0) sheet.getRange(maxRow + 1, 1, folderData.length, maxCol).setValues(folderData);
      sheet.getRange(rowCnt, maxCol).setValue('TRUE');
      // スプレッドシートに追記した分、スプレッドシートに記載されているデータ範囲の最大行数を更新する
      maxRow = maxRow + folderData.length;

      // 対象フォルダの調査が1つ終わる度に、経過時間をチェックし、25分経過していたらトリガーを設定して処理を終了する
      if ((Date.now() - startTime) > 1000 * 60 * 25) break;
    }
    rowCnt++;
  }

  // スプレッドシートに書き出されたデータがある場合は、調査が途中である可能性があるため、調査を再開するためのトリガーを設定する
  // 書き出されたデータがない場合は、調査終了と判断し、調査結果を階層が見やすいようにソートする
  if (currentRange.getLastRow() != sheet.getDataRange().getLastRow()) {
      setTrigger_();
  } else {
      // 絶対パス名で昇順ソートし、各項目でフィルターできるようにフィルターを作成する
      // ソートは2階層目のサブフォルダ以降の調査結果に対して行う(1階層目のフォルダと、2階層目のファイルはソート済なので対象にしない
      let sortStartRowCnt = 1
      for (let value of values) {
        if (value[0] == 2 && value[1] == 'Folder') break;
        sortStartRowCnt++;
      }
      sheet.getRange(sortStartRowCnt, 1, maxRow - (sortStartRowCnt - 1), maxCol).sort([{column: 4, ascending: true}]);
      sheet.getDataRange().createFilter();
  }
}

/**
 * 調査結果を書き出すスプレッドシートのシートオブジェクトを取得する 
 * 
 * @returns {Sheet} 指定されたスプレッドーシートの指定されたシートオブジェクト
 */
function getSheet_() {
  let sheet;

  try {
    if (spreadsheetUrl.length > 0) {
      sheet = SpreadsheetApp.openByUrl(spreadsheetUrl).getSheetByName(sheetName);
    } else {
      sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName(sheetName);
    }
  } catch (e) {
    console.error('スプレッドシートURL(spreadsheetUrl)に存在しないURLが設定されているか、このスクリプトがスプレッドシートに紐付けられていません。');
    throw e;
  }

  try {
    sheet.getName();
  } catch (e) {
    console.error('シート名(sheetName)に存在しないシート名が設定されています');
    throw e;
  }

  return sheet;
}

/**
 * チェック対象のフォルダオブジェクトを取得する 
 * 
 * @param {String} folderId   取得したいフォルダのフォルダID
 * @returns {Folder} 指定されたフォルダIDのフォルダオブジェクト
 */
function getFolder_(folderId)
{
  let folder;

  try {
    return DriveApp.getFolderById(folderId);
  } catch (e) {
    console.error('フォルダID(folderId)に存在しないフォルダのIDが設定されています');
    throw e;
  }
}

/**
 * アクセス権情報を書き出すスプレッドシートの内容を全て削除し、
 * アクセス権情報の項目名をヘッダーとして書き出す
 * 
 * @param {Sheet} sheet
 */
function refleshSheet_(sheet) {
  let range;
  let header = [
    '階層',
    '種別',
    'ID',
    'パス',
    'アクセス範囲',
    'アクセス権限',
    'オーナー',
    '編集権限所有者',
    '参照権限所有者',
    'アクセス権限継承',
    'サブフォルダチェック済'
  ]

  // 設定されいているフィルタを解除する
  if ((range = sheet.getDataRange()).getFilter() != null) range.getFilter().remove();

  range.clear();
  sheet.getRange(1, 1, 1, header.length).setValues([header]);
  SpreadsheetApp.flush();
}

/**
 * 過去に実行された調査再開トリガーを削除する 
 */
function deleteTriggers_() {
  let triggers = ScriptApp.getProjectTriggers();

  // 既に処理を起動したトリガーを削除
  for (let trigger of triggers) {
    if (trigger.getHandlerFunction() == 'continueCheckPermission') ScriptApp.deleteTrigger(trigger);
  }
}

/**
 * 1分後に調査を再開するトリガーを設定する
 * (調査対象フォルダの規模により30分のGASの実行時間制限内に終わないため、継続調査を自動実行する)
 */
function setTrigger_() {
  // 1分後に処理を起動するトリガーを設定
  ScriptApp.newTrigger('continueCheckPermission').timeBased().after(1000 * 60).create();
}

/**
 * チェック対象フォルダおよびフォルダ配下のフォルダ・ファイルのアクセス権情報を取得する
 * 
 * @param {Folder} folder                 チェック対象フォルダのフォルダオブジェクト 
 * @param {String} absolutePath           チェック対象フォルダのフルパス
 * @param {Number} checkLimitDepth        アクセス権チェックを行う最大階層
 * @param {Array[]} parentPermissionInfo  親フォルダのアクセス権情報
 * @param {Array[][]} folderData          調査したアクセス権を保存する変数
 * @returns 
 */
function checkFolder_(folder, absolutePath, checkLimitDepth, parentPermissionInfo, folderData) {
  // チェック対象フォルダのアクセス権情報を取得する
  folderData.push(getPermissionInfo_(folder, absolutePath, parentPermissionInfo));

  // チェック対象フォルダの調査結果したアクセス権を保存する変数内の行数を保存する
  // (調査完了後に、"サブフォルダチェック済"カラムをTRUEにするため)
  let checkFolderRow = folderData.length - 1;

  // 調査対象フォルダを1階層目として指定階層と同じ深さになったら配下のフォルダ・ファイルのチェックを止める
  // 指定階層が0の場合は、チェックは止めない
  if (checkLimitDepth != 0 && getPathDepth_(absolutePath) >= checkLimitDepth) return;

  // 調査対象フォルダのファイル・フォルダを調査して、
  // 調査結果格納変数(folderData)内の調査対象フォルダのサブフォルダチェックカラムをTRUEに変更する
  checkSubContents_(folder, absolutePath, checkLimitDepth, folderData[checkFolderRow], folderData);
  folderData[checkFolderRow][(folderData[checkFolderRow]).length - 1] = true;
}

/**
 * チェック対象ファイルのアクセス権情報を取得する
 * 
 * @param {File} file                     調査対象のファイルオブジェクト
 * @param {String} absolutePath           調査対象のファイルのフルパス
 * @param {Array[]} parentPermissionInfo  親フォルダのアクセス権情報
 * @param {Array[][]} folderData          調査したアクセス権を保存する変数
 */
function checkFile_(file, absolutePath, parentPermissionInfo, folderData) {
  // チェック対象ファイルのアクセス権情報を取得する
  folderData.push(getPermissionInfo_(file, absolutePath, parentPermissionInfo));
}

/**
 * チェック対象フォルダの配下のフォルダ(サブフォルダ)・ファイルのアクセス権情報を取得する 
 * 
 * @param {Folder} folder                 チェック対象フォルダのフォルダオブジェクト
 * @param {String} absolutePath           チェック対象フォルダのフルパス 
 * @param {Number} checkLimitDepth        アクセス権チェックを行う最大階層数
 * @param {Array[]} parentPermissionInfo  親フォルダのアクセス権情報
 * @param {Array[][]} folderData          調査したアクセス権を保存する変数
 */
function checkSubContents_(folder, absolutePath, checkLimitDepth, parentPermissionInfo, folderData) {
  let subFolders, subFolder;
  let subFiles, subFile;

  // チェック対象フォルダの直下にあるファイルのアクセス権情報を調査する
  if ((subFiles = folder.getFiles()) != null) {
    while (subFiles.hasNext()) {
      subFile = subFiles.next();
      checkFile_(subFile, getAbsolutePath_(subFile,absolutePath), parentPermissionInfo, folderData);
    }
  }

  // チェック対象フォルダのサブフォルダおよびサブフォルダ内のファイルのアクセス権情報を調査する
  // checkFolderの中からcheckFolderが呼び出され、複数階層をチェックする
  if ((subFolders = folder.getFolders()) != null) {
    while (subFolders.hasNext()) {
      subFolder = subFolders.next();
      checkFolder_(subFolder, getAbsolutePath_(subFolder, absolutePath), checkLimitDepth, parentPermissionInfo, folderData);
    }
  }
}

/**
 * 指定されたフルパス階層数を返却する
 * (xxx/xxx/xxx というフルパスが渡されることを想定している) 
 * 
 * @param {String} absolutePath フルパス
 * @returns {Nulber}  フルパスの階層数
 */
function getPathDepth_(absolutePath) {
  let tmp = absolutePath.split('/');
  return tmp.length;
}

/**
 * チェック対象のフォルダまたはファイルのフルパスを返却する
 * (フォルダの区切り文字は"/"を利用する)
 * 
 * @param {Folder or File} obj 
 * @param {String} parentAbsolutePath 
 * @returns {String} 指定フォルダまたはファイルのフルパス
 */
function getAbsolutePath_(obj, parentAbsolutePath) {
  if (parentAbsolutePath.length > 0) {
    parentAbsolutePath = parentAbsolutePath + '/';
  }

  return parentAbsolutePath + obj.getName();
}

/**
 * チェック対象のフォルダまたはオブジェクトのアクセス権情報を返却する
 * 
 * @param {Folder or File} obj            チェック対象のファイルまたはフォルダのオブジェクト
 * @param {String} absolutePath           チェック対象のフルパス
 * @param {Array[]} parentPermissionInfo  チェック対象の親フォルダのアクセス権情報
 * @returns 
 */
function getPermissionInfo_(obj, absolutePath, parentPermissionInfo) {
  let objType = 'File';

  // Fileクラスにしかない関数を呼び出し、エラーが発生したらobjはFolderオブジェクトであると判断する
  try {
    obj.getDownloadUrl();
  }catch(e) {
    objType = 'Folder';
  }

  let permissionInfo = [
    getPathDepth_(absolutePath),
    objType,
    obj.getId(),                            // フォルダID or フォルダID
    absolutePath,                           // フォルダ名 or ファイル名 (フルパス)
    obj.getSharingAccess().toString(),      // 共有アクセス範囲(Domainなど、PrivateはRestrictを示す)
    obj.getSharingPermission().toString(),  // 共有アクセス権
    obj.getOwner().getEmail(),              // オーナーのメールアドレス
    getUsersEmail_(obj.getEditors()),       // 編集権限を持つユーザーのメールアドレス 
    getUsersEmail_(obj.getViewers()),       // 参照権限を持つユーザーのメールアドレス
  ];

  permissionInfo.push(isPermissionInherited_(permissionInfo, parentPermissionInfo));  // 親フォルダ権限継承フラグ
  permissionInfo.push(((objType == 'Folder')? false : ''));                           // サブフォルダチェック済フラグ

  if (debugFlg) console.log(permissionInfo.toString());

  return permissionInfo;
}

/**
 * 指定されたユーザーリストからメールアドレスを抽出し、
 * 1つの文字列に結合して返却する
 * 
 * @param {User[]} users  ユーザリスト
 * @returns {String} ","で区切った複数のメールアドレス文字列
 */
function getUsersEmail_(users){
  let emails = new Array();

  if (users.length > 1) {
    users.forEach((user) => emails.push(user.getEmail()));
  }

  return emails.sort().join(',');
}

/**
 * 親フォルダとアクセス権が同じ(継承している)かを判定する  
 *  
 * @param {Array[]} permissionInfo        チェック対象のアクセス権情報
 * @param {Array[]} parentPermissionInfo  親フォルダのアクセス権情報
 * @returns {BOOLEAN} 親フォルダと権限が同じならtrue, 権限が異なるならfalse
 */
function isPermissionInherited_(permissionInfo, parentPermissionInfo) {
  return (
    parentPermissionInfo.length > 8
    && permissionInfo[4] == parentPermissionInfo[4]     // 共有アクセス範囲
    && permissionInfo[5] == parentPermissionInfo[5]     // 共有アクセス権
    && permissionInfo[6] == parentPermissionInfo[6]     // オーナーのメールアドレス
    && permissionInfo[7] == parentPermissionInfo[7]     // 編集権限を持つユーザーのメールアドレス
    && permissionInfo[8] == parentPermissionInfo[8]     // 参照権限を持つユーザーのメールアドレス
  );
}

Azure AD Application Proxyを経由してAzure ADをIdpとしたSAML認証を行う

SAML認証をサポートしているアプリケーションをAWS等のIaaS上に構築して、IP制限を掛けてアクセス制限しており、SAML認証とAzure AD Application Proxyを同時に利用したいケースがあったので検証しました。

MicrosoftのTech Communityの記事で、SAMLベースのアプケーションをAzure AD Application Proxyがサポートしたことは記載があり、設定方法はMicrosoftのApplication Proxyのドキュメントに記載されていましたが、これの通りに設定しても、今回はうまくいきませんでした。

上記の記事には、元々のReply URLが https://contosotravel.com/acs で、対応するApplication ProxyのExternal URLが https://contosotravel-f128.msappproxy.net の場合、Reply URLを、https://contosotravel-f128.msappproxy.net/acs に設定する必要があると記載されています。

ですが、今回検証した環境では、下記の3つのReply URLを設定する必要がありました。

https://contosotravel-f128.msappproxy.net/acs [default]
https://contosotravel-f128.msappproxy.net/
https://contosotravel.com/acs

上記3つを指定する必要があると記載されたドキュメントは見つけることができず、前述したMicrosoftのApplication Proxyのドキュメントには、Application Proxyを利用する前にReply URLに設定していた internal URLは削除することができると書いてあります。しかし、Application Proxy経由でSAML認証を行った際に表示されるエラーメッセージに沿って設定を修正していった結果、上記3つのReply URLの設定が必要という結果なりました。

エラーメッセージに沿って設定を修正したら解決できたので、1日で解決できた問題でしたが、自分ではこのことが記載されているドキュメントが見つけられなかったので、メモ代わりにここに記載します。

今回はオンプレミス環境ではないため、IP制限をかけているURL(internal URL)もインターネット上にて名前解決はできる状態です。Application Proxy利用時に想定されている通常のケースは、インターネット上ではURLの名前解決ができないアプリケーションに対してExternal URLを発行してアクセスする形と理解しています。このような想定されているケースとの環境的な差異が、Microsoft のApplication Proxyのドキュメントの通りに設定してもうまく動かなかった原因の可能性があるのかもと思っていますが、根拠はなく、関係しているとしても正直どう関係しているのかは見当もついていません。

もしMicrosoftから出されているドキュメントなどで、上記設定の根拠になりそうなものをご存じの方がいらっしゃいましたら、教えていただけると大変ありがたいです。

PowerAutomateを使ってGmailの添付ファイルをGoogleSharedDriveにファイルをアップロードする

はじめに

特定の条件にあったメールに添付されているファイルをGoogleSharedDriveにアップしたかったので、試してみました。PowerAutomateで用意されているGoogleDriveのコネクタは個人ドライブへのファイル操作は行えますが、SharedDriveに対する操作はできないという制限があるので、SharedDriveを操作するためのカスタムコネクタを作って対応しました。

GoogleDriveガイドのファイルデータをアップロードする を見たら、マルチパートアップロードでRequest bodyを作るのが手間がかかりそうと感じました。特に境界文字列を使って分割したパーツを識別したりなどが手間がかかりそうと感じました。そのため、既存のGoogleDriveコネクタを使って個人ドライブにファイルをアップロードし、カスタムコネクタで個人ドライブからSharedDriveに移動するというやり方にしました。個人的には既存で使えるものは使って楽をしたいと考えているからです。

作業手順

必要な作業は下記です。手順内で利用するGoogleアカウントは、GoogleWorkspaceのユーザーを想定しています。

手順1. Google Cloud PlatformでOAuthクライアントIDを作成する
手順2. 手順1.で作成したOAuthクライアントIDとクライアントシークレットを保存する
手順3. SharedDriveへファイルを移動するカスタムコネクタを作成する
手順4. 手順3.で作成したカスタムコネクタを利用したフローを作成する
手順5. 手順3.で作成したカスタムコネクタで利用するコネクションを作成する

手順1. Google Cloud PlatformでOAuthクライアントIDを作成する

OAuthクライアントIDを作成し、クライアントIDとクライアントシークレットを入手します。

PowerBuilder Japan Portalのこちらのページにも画像つきで方法が紹介されています。

1. Google Cloud Platform に新規プロジェクトを作成し、Google Drive APIを有効化する

Google Cloud Platformにアクセスし、Googleアカウントでログインします。その後、Google Drive APIを利用するための新規プロジェクトを作成します。

作成したプロジェクトの”APIs & Services”メニューから”library”を選択し、GoogleWorkspaceのカテゴリから”Google Drive API”を選択します。”ENABLED”のボタンをクリックし、Google Drive APIを有効化します。

2. OAuthクライアントIDを作成する

作成したプロジェクトの”APIs & Services”メニューから”Credentials”を選択し、”+CREATE CREDENTIALS”から”OAuth client ID”を選択し、OAuthクライアントIDを作成します。OAuth consent screenにてUser Typeの選択を求められた場合は、”Internal”を選択します。(自分自身または組織内のユーザーのみが利用することを想定しています。)

OAuthクライアントIDの入力項目を、下記の表に沿って入力します。

パラメタ名パラメタ値
Application typeWeb application
Name[お好きな名前]
Authorized JavaScript origins[未設定]
Authorized redirect URIshttps://global.consent.azure-apim.net/redirect
(手順3の2.Security内の画像に記載されているRedirect URLです)

手順2. 手順1.で作成したOAuthクライアントIDとクライアントシークレットを保存する

手順1で作成した新規プロジェクトとの”APIs & Service”メニューから”Credentials”を選択し、手順1で作成したOAuthクライアントの”Action”のダウンロードアイコンをクリックし、OAuthクライアント情報をダウンロードします。

手順3. SharedDriveへファイルを移動するカスタムコネクタを作成する

PowerAutomateの”Data > Custom connectors”メニューから”+ New custom connector > Create from Blank”を選び、カスタムコネクタを作成します。1.Gneral から 4. Code (Preview) まで下記のように入力します。

1. General

下記の表に沿って入力します。下記表にない項目は自由に入力します。

パラメタ名パラメタ値
Schemahttps
Hostwww.googleapi.com
Base URL/

2. Security

下記の表に沿って入力します。

パラメタ名パラメタ値
Choose what authentication is implemented by your APIOAuth 2.0
Identity ProviderGoogle
Client ID[手順2で保存OAuthクライアントID]
Client secret[手順2で保存したクライアントシークレット]
Scopehttps://www.googleapis.com/auth/drive

3. Definition

”+ New action”メニューから新しいアクションを追加し、”+ Import from sample”を選択し、Requestを下記の表に沿って入力します。”{}”で囲んだ部分は変数としてカスタムコネクタに認識されます。

下記表に入力しているパラメタは、GoogleDriveのfile updateのAPIリファレンスを参考にしています。

パラメタ名パラメタ値
VerbPATCH
URLhttps://www.googleapis.com/drive/v3/files/{fileID}?addParents={addParents}&removeParents={removeParents}&supportsAllDrives={supportsAllDrives}
Body{}

Queryパラメタの設定を下記の表に沿って入力します。下記表にない項目は自由に入力ください。

パラメタ名 / パラメタ値AddParentsremoveParentssupportsAllDrives
is required?yesremoveParentsno
Typestringstringboolean

4. Code (Preview)

Code Detailsの部分のトグルボタンをクリックし、”Code Disabled”にします。”Code Enable”になっているとFlowを実行したときに、ここに記載しているテストが実行されます。

手順4. 手順3.で作成したカスタムコネクタを利用したFlowを作成する

“My flows”メニューから”+New flow”を選択し、”Automated clod flow”を選択します。新しいFlowのトリガーは、Gmailの”When a new email arrives”を選択して、Flowを作成します。

下記画像を参考にSharedDriveに保存したいファイルが添付されたメールを抽出できるよう”When a new email arrives”のフィルター条件を設定してください。

次に添付ファイルを個人ドライブに保存するために、既存のGoogleDriveコネクタのCreate fileアクションを追加して、各パラメタを下記表に沿って入力します。

パラメタ名パラメタ値
Folder path[個人Drive内のフォルダパス]
(フォルダアイコンをクリックしてGUIで個人Driveの中のフォルダを選択できます。
認証を求めらたらGoogleアカウントで認証してください。認証するとGoogleDriveのコネクションが作成されます。)
File name[GmailのAttchments Name]
 File content[GmailのAttachments Content]

次に、個人Driveに保存したファイルをSharedDriveに移動するために、手順3で作成したカスタムコネクタのmovefileアクションを追加して、各パラメタを下記表に沿って入力します。

パラメタ名パラメタ値
fileID[GoogleDriveのid]
addParents[移動先SharedDriveのフォルダID]
(フォルダIDの調べ方はメシラボさんの記事がわかりやすかったです)
removeParents[移動元個人DriveのフォルダID]
supportsAllDrivesYes
body[無記入]

ここまで入力したら作成したFlowを保存してください。

手順5. 手順3.で作成したカスタムコネクタで利用するコネクションを作成する

“Data”メニューから”Custom connectors”を選択し、手順3.で作成したカスタムコネクタの横にある”+”を選択します。Googleアカウントでの認証を求めらるので、Googleアカウントで認証してください。


これで準備は完了です。Gmailにファイルが添付されたメールを送り、SharedDriveにファイルが保存されるかをご確認ください。

説明がわかりづらい部分もあるかと思いますが、お役にたちましたらうれしいです。

音声入力に助けられた

先週末、家族で川遊びをした時に転んでしまって、左手の中指を少し折ってしまった。幸いけがは大したことなかったものの左手の親指を残す四本は包帯でぐるぐるに巻かれているため、タイピングができない。左手でタイピングに使えるのは親指と中指のみ、いつもより作業効率が落ちるのは目に見えている。

すこしでも効率を落とさないために何かできることはないかと考えて、思いついたのが音声入力だった。Windowsにはたしか標準でついたことを思い出し、早速試してみた。

これが思ったよりよかった。 ある程度長く話すと変換の精度もなかなかである。アルファベットを打ち込むことには向かないので、スクリプトや設定ファイルを書くことはできないけれど、チャットに返信したり、メールを書いたり、ドキュメントを書くのには充分使えた。ドキュメント内に出てくるアルファベットはカタカナで表記されてしまうため、後で自分で書き直す必要があるが、全部を今の自分が書くよりは全然早い。

また、考えていることを外に出すときは、独特の気持ちよさがあると感じた。タイピングする時に、対して何か妨げられて無い感覚がある。一回できれいな文章を書こうとすると、もちろんタイピングの方が良いのだけれど、考えていることをただつらつらとメモるだけであれば音声入力の方が、一気に書きやすい気がした。

当初は怪我している間の代替手段として始めた音声入力だったけれど、自分の考えをまとめたり、かき出したりという用途だったら、今後も使えそうな気がする。資料を眺めながら気に入ったところメモしたり、資料の感想をただダラダラと喋って行くのにもむいていそうと思っている。

あとボソボソとしゃべると認識率が下がるので、聞き取りやすくはっきり喋ると言う必要はある。大きな声で話す必要はないけれど、きちんと口を開けて動かしてしゃべるというイメージ。これはオンラインミーティングで相手が聞き取りやすく話すということの訓練にも使えるかもしれない。

在宅勤務で働いている方ならヘッドセットやマイクも揃っているので、すぐにでも始められると思う。考えをまとめるとき、なんとなく手が疲れたっていう時、音声入力を試してみると、いいかもしれない。

2021年に買ってよかったもの

キーボード [Mistel BAROCCO MD770]

2020年も同じキーボードを購入しましたが、今回は軸違いです。前回は静音赤軸、今回はスピードシルバー軸です。静音赤軸も気に入っていたのですが、もっと打鍵感を軽くしたくてシルバー軸を買ってみました。結果は大成功。シルバー軸の打鍵感の方が断然好みでした。キーが重いなーと感じることもなく。指に力を込める感覚もほとんどなく快適に使えています。分割キーボード自体は気に入っていたので、とてもよい買い物でした。

マウス [Logicool MX ERGO]

ずっとマウスは小さくて軽いのが好みだったのですが、分割キーボードを使い始めてから、キーボードをマウスが時々当たるのが気になって購入しました。使い始めてすぐは、失敗した。これは慣れられないと感じたのですが、1週間もするとなれて快適になってきました。今では、手を動かさずに操作できるこのマウスが手放せなくなりました。自宅はもちろん、オフィスに出社する際にも持って行くほど気に入っています。ちょくちょくトラックボールの動きが悪くなるのですが、ティッシュで拭くと、またスムーズに動くようになります。数日に1回は拭いているので、もうちょっと手間が省けるとうれしい部分です。

Bluetoothスピーカー [Bose SoundLink Revolve+ II]

奥さんが外で遊んだり、お風呂に入るときに音楽聴くのに欲しいと言っていたので購入しました。Bluetoohでも、Auxでも、MicroUSBでも接続できて便利です。小さい割にはいい音が出ますし、中高音は特にきれいだと感じます。キャンプで使えるように防水設計なので、お風呂の中の衣類干し棒にフックで引っかけて音楽を楽しむこともできます。スマホさえあればどこでも音楽が流せるので、子供達と川沿いに遊びに行くときに持って行ったりもしています。デスクの上に置いてPCとつないでPodcastを聞くのにも便利です。

メガネ [Masaki Matsushima MF-1215/MF-1240]

かけ心地が今の頭・顔の形に合ったメガネです。幅が広めで、こめかみにメガネのつるがあまり当たらず、耳の裏の部分で挟んで固定しています。太ったことに伴い、顔の幅が広くなったので、メガネのつるがこめかみとすれて痛くなるのが嫌だったんですが、このメガネが解消してくれました。もともと顔のサイズが大きかったので、もうこのこすれる痛みは回避できないのかなーと諦めていましたが、眼鏡市場でこのメガネに出会いました。2022年のはじめにもう一本購入して、外を出歩くときに使う遠距離用とデスクワーク用に度を調整して使っています。


以上、普段の生活で触れる時間が長いものにいい出会いがあったので、とてもよかったです。

Microsoftのプライバシー関係のドキュメント

Microsoftサービス(AzureとかMicrosoft365)とかのドキュメントいっぱいあるなーと思いながら、いつも読むのが途中になって、次に読み始めた時に、何読んでたのか覚えていないとなっていた。

今回は、あとで読みかえすため、どんな文書読んでたかを残しておきたので、ブログに書きます。単なるリンク集ですが、自分にとっては、きっと後で見返す、大事なものになると思うので。

Privacy Statement

terms of use

Microsoftサービス規約

プライバシー

Microsoftはお客様のデータをクラウドの中でどあつかうか(ホワイトペーパー)

お客様のデータに誰がどのような条件でアクセスできるか (サブプロセッサーのリストもあり)

Microsoftによるオンラインサービスのデータ分類方法

AzureADのデータ保管場所

Microsoftでのデータ管理

お客様のデータの保管場所

Microsoft 365のデータ保管場所

Microsoft Azureの法的情報

サービスレベルアグリーメント

Azureのセキュリティのドキュメント

Microsoftコンプライアンスのサービス

2020年に買ってよかったもの

キーボード [Mistel BAROCCO MD770]

肩こりの解消を期待して、初めての分割キーボードを買いました。期待した通り、肩こり解消にはつながりました。特に肩から胸にかけての凝りは、かなり解消されました。肩から肩甲骨側の凝りにはあまり効果がなかったので、この部分は、姿勢など、他の要因がありそうです。

メカニカルキーボードも初めてで、軸の知識もあまりなかったのですが、静かで軽いのが欲しいので、静穏赤軸を選択しました。どこで入力されるかが、これまで使っていたHHKBと違ってわかりづらかったので、誤タイプが増えた気がします。タッチ感はHHKBの方が好みですが、指はHHBKより疲れなくなったと思います。

モニターアーム [ErGear 2画面]

モニターの下部のスペース(モニタースタンドの足がある部分)に、ノートや小物を置くスペースにしたくて、購入しました。クーポン使って5,000円くらいで購入できたので、コストパフォーマンスは抜群です。

作業中にモニターを動かしたり、角度をこまめに変えたい方には向きません。一回モニター位置を決めたら、ほぼ動かさない方にはお勧めです。

フットレスト [サンワサプライ MR-FR3]

デスクチェアに座ると、足の収まりが悪かったので、その対策で購入。チェアにやや後傾姿勢で座った際の安定感が増します。2000円くらいで買えるので、これもコストパフォーマンスは抜群です。

タブレット [Kindle FireHD 8 plus]

iPadは、電車の中やベットに横になって電子書籍を読むにはちょっと大きくて重かったので、もうちょっと軽めのやつが欲しくて、プライムセールで購入。1万円を切っていたので、もし失敗してもいいやと思えるからという理由もあったけれど、買ってよかった。軽いし、片手で持てるし、本を読む頻度が上がりました。壊れたら、また買い替えればいいという値段なので、電子書籍リーダーとして、壊れても買い続けると思います。

ヘッドフォン [SteelSeries Arctis 5]

Web会議用のマイク付きヘッドセットとして購入。LEDが光るのが気になる以外は、相手へ聞こえる自分の声も相手が話す声もクリアで、満足度が高いです。若干重いので、1日中つけ続けるのは首が凝りますが、会議のときだけつけるくらいなら全く問題なし。1万円ちょいでWeb会議が快適になるので、これもいい買い物でした。

ビジネスプリンタ [エプソン PC-M730F]

書類をスキャンすることが多いのと、時々プリントすることもあるので購入。写真はデータで見るし、プリントするときはお店でプリントするようになったので、ビジネス用途のものにしました。こっちの方が、家庭向けの写真もプリントできるプリンタの半額くらいで買えます。スキャンのスピードも早くて快適。

 

以上、全部自宅勤務を快適にするためのものばかりです。毎日触れるものなので、印象に残っているのも、これらになります。

これらを買おうかなーと検討している方の役に立つと嬉しいです。

WindowsUpdateが上手くいないときにためすこと

仕事でWindowsUpdateでのWindows10大型アップデートがうまくできない端末があって、対応しているんんですが、下記リンクで、WidnowsUpdateのユーザデータをクリアするというのは、うまくいかない場合に必ずやってみてください。

あと、トラブルシューティングツールで、WindowsUpdateの問題の解決と、ディスクのプロパティから選べるシステムファイルを対象にしたディスククリーンを実行すると、クライアント側のデータは全て消えます。

まずはこれから試してみてください。

 

https://blogs.technet.microsoft.com/jpwsus/2014/12/02/windows-update-3/

CPUの世代での違い

久々にブログを書こうと思い立ったんですが、何も準備していなかったので、最近驚いたことを書いておきます。最近、会社で新しいPCが支給されたり、自分でもPCを買ったりして、複数のPCを触る機会がありました。ここで体験したのが、CPU世代での性能差です。

使ったPCは、下記の3つ
・VAIO S13(CPUはインテル6世代のCOREi7)
・DELL XPS13(CPUはインテル7世代のCOREi7)
・富士通LIFEBOOK UH(CPUはインテル7世代のCOREi5)

VAIOはまだ2年も使っていないモデルで、会社でメインで利用しています。通常の利用に不満はないです。DELL XPSは家用に買ったPCで、VAIOよりも性能がいいのはわかった上で触っているのですが、びっくりしたのは富士通LIFEBOOK。CPUはi5なのに、データ量と数式が多く入ったEXCELファイルを開いたときの速度が、富士通LIFEBOOKの方が、VAIOよりも速かったんです。時間がかかるEXCELの式の再計算に差があったように見えたので、CPUの差だと考えられます。

世代が1世代違ってもi7が優位だと漠然と思っていたんですが、アーキテクチャの進化は想像以上に大きかったです。値段は富士通LIFEBOOKよりDELL XPSの方が安いので、DELL XPSの方がお買い得感があります。DELLの直販サイトでは頻繁に割引もやっているので、週に一回くらいチェックすると、よりお得なキャンペーンで購入することがあります。今(8/12)にやっているキャンペーンは18000円引き+5%オフなので、だいぶお得です。僕が買ったときより安いですね。14万くらいでi7モデルが買えてしまいます。

軽さをもとめる人は、富士通LIFEBOOKです。1kgを切っています。macbookより軽いので持ち歩いでも苦になりません。XPSより高いし、CPUのランクも落ちますが、使い比べなければわからないレベルでしょうし、重さはXPSの2/3程度です。(LIFEBOOKは約800g、XPSは約1200g)

これからは、PCを購入する際に1世代とはいえ、CPUの世代はCPUのランク以上に意識する必要があるデータとして考えていきます。次は、家のメインPCのiMacの買い替えですね。XPS用に4Kの外付けディスプレイも購入したので、次はMacBook Proの15インチにして、机の上をすっきりさせたいと考えています。iMacもスペックも価格も値上がりしているので、迷うところです。iMacはデスクトップ用のCPUを積んでいるのでクアッドコアというのが魅力です。購入資金はまだないし、iMacもMacBook Airも現役で働いてくれているので、購入はまだ先になりそう。今月に第二子も生まれて、また色々買うものも出てきますしね。

WindowsPCを購入する際の参考になればと思います。国産PCだと、今の富士通LIFEBOOKはかなりいいと想います。VAIOが第7世代のCPUを積んできたら、VAIOもいいですね。デザインと使い勝手でいうと、富士通LIFEBOOK、VAIO、DELL XPSのどれかではないかと思います。