linuxBean14.04(189)Googleフォトにアップロードするシェルスクリプト

2018-12-02

旧ブログ

t f B! P L
結局シェルスクリプトだけでGoogleフォトにアップロードすることにしました。指定フォルダからfindコマンドで抽出したファイルをGoogleフォトにアップロードします。アルバム作成の機能は未実装です。アップロードするファイルのあるフォルダの指定とそこからアップロードするファイルを選択する条件のfindコマンドのオプションは、簡単に変更できます。

前の関連記事:linuxBean14.04(188)cronの実行ログ、cronで実行させたコマンド出力、の取得方法


コマンドの確認


curlとjqを使うので、インストールされていなければSynapticマネージャーからインストールしておきます。

設定ファイルconfig.jsonの作成

{
 "APIAppCredentials": {
  "CLIENT_ID": "*****",
  "CLIENT_SECRET": "*****"
 },
 "JOB": {"SOURCE_FOLDER":  "~/public/videos",
     "FIND_OPTIONS": "-mmin -10 -type f -name '*.jpg'"
 }
}
設定ファイルはJSON形式でconfig.jsonというファイル名でシェルスクリプトと同じフォルダに入れておきます。

CLIENT_IDとCLIENT_SECRETはGoogle APIライブラリを使用するで取得した値を入れます。

SOURCE_FOLDERはアップロードする画像ファイルがあるフォルダのパスを指定します。

相対パスで指定した場合はシェルスクリプトがあるフォルダがルートになりますが、cronで実行する場合はcrontabで登録したユーザーのホームフォルダがルートになります。

チルダ(~)はホームフォルダに展開されます。

FIND_OPTIONSの値にはfindコマンド(Man page of FIND)のオプションを指定します。

上記の例だと10分以内に更新された拡張子がjpgのファイルをサブフォルダにあるものも含めてアップロードします。

FIND_OPTIONSの値にはfindコマンドのオプションに続いて処理させるパイプも渡せます。

ただし、JSON文字列のエスケープが必要です。

アルバムに入れずにGoogleフォトにアップロードした画像ファイルは、撮影日時に関係なくあとでアップロードした画像が先に表示される仕様のようなので、ファイル更新日でソートしてからアップロードするようにしました。
{
 "APIAppCredentials": {
  "CLIENT_ID": "*****",
  "CLIENT_SECRET": "*****"
 },
 "JOB": {"SOURCE_FOLDER":  "~/public/videos",
     "FIND_OPTIONS": "-mmin -10 -type f -name '*.jpg' -printf '%T@ %p\\0' | sort -nz | grep -zo '.*' | sed 's\/[^ ]* \/\/'"
 }
}
JSON文字列のエスケープにはFree Online JSON Escape / Unescape Tool - FreeFormatter.com使いました。

CLIENT_IDとCLIENT_SECRETからリフレッシュトークンを取得するgetTokens.sh

#!/bin/bash
echo config.jsonを元にトークンを取得します。
eval "$(jq -r '.APIAppCredentials | to_entries | .[] | .key + "=\"" + .value + "\""' config.json)"
REDIRECT_URI=urn:ietf:wg:oauth:2.0:oob
SCOPE=https://www.googleapis.com/auth/photoslibrary.appendonly
echo "https://accounts.google.com/o/oauth2/v2/auth?response_type=code&client_id=${CLIENT_ID}&redirect_uri=${REDIRECT_URI}&scope=${SCOPE}&access_type=offline"
echo 上記URLをブラウザでアクセスし、認証用コードを取得してください。
echo -n 認証用コード?:
read AUTHORIZATION_CODE
curl -s --data "code=${AUTHORIZATION_CODE}" --data "client_id=${CLIENT_ID}" --data "client_secret=${CLIENT_SECRET}" --data "redirect_uri=${REDIRECT_URI}" --data "grant_type=authorization_code" --data "access_type=offline" https://www.googleapis.com/oauth2/v4/token>tokens.json
echo tokens.jsonにトークンを保存しました。

このスクリプトは一番最初に一回実行するだけです。

(2023.3.12追記。認証方法が変更になってこのスクリプトではうまくいかなくなりました。

 

手動コピー / 貼り付けオプション(帯域外(OOB)リダイレクト方法によるリクエストはブロックされるので代替法に移行しないといけません。帯域外(OOB)フロー移行ガイド  |  Authorization  |  Google Developersに移行方法が書いてありますがGoogle APIクライアントライブラリを使用するように移行しないといけないようです。)

config.jsonを元にトークンを取得します。
https://accounts.google.com/o/oauth2/v2/auth?response_type=code&client_id=*****&redirect_uri=urn:ietf:wg:oauth:2.0:oob&scope=https://www.googleapis.com/auth/photoslibrary.appendonly&access_type=offline
上記URLをブラウザでアクセスし、認証用コードを取得してください。
認証用コード?:
このgetTokens.shをTerminalから実行するとCLIENT_IDから認証コードを取得するURLを提示します。

このURLをコピーしてブラウザでアクセスすると、Googleアカウントへのログイン画面が表示されるので、アップロードするGoogleフォトのGoogleアカウントでログインします。

ログインすると、「Googleフォトライブラリの表示と管理」への許可が求められるので許可します。

許可すると、認証用コードがブラウザに表示されるのでそれをコピーして、Terminalに戻って、「認証用コード?:」の後にマウスでペーストして、Enterします。

認証用コードから、リフレッシュトークンとアクセストークンがJSON形式で取得できるので、それをスクリプトがあるフォルダのtokens.jsonファイルに保存されてスクリプトが終了します。
{
  "access_token": "*****",
  "expires_in": 3600,
  "refresh_token": "*****",
  "scope": "https://www.googleapis.com/auth/photoslibrary.appendonly",
  "token_type": "Bearer"
}
tokens.jsonの内容はこのようになっていると思います。
今のところはmediaItems.batchCreateしか使っていないのでscopeはhttps://www.googleapis.com/auth/photoslibrary.appendonlyにしています。

expires_inは3600秒になっていて、これはアクセストークンの有効期限です。

期限が切れてもここで取得したリフレッシュトークンでアクセストークンを更新できるので、このgetTokens.shを実行するのはCLIENT_IDやCLIENT_SECRET、Googleフォトのログインアカウントを変更しない限り最初の1回だけです。

アクサストークンを更新するrefreshTokens.sh

#! /bin/bash
echo access_tokenをリフレッシュします。
eval "$(jq -r '.APIAppCredentials | to_entries | .[] | .key + "=\"" + .value + "\""' config.json)"
TOKENS_JSON=$(cat tokens.json)
REFRESH_TOKEN=$(echo $TOKENS_JSON | jq -r ".refresh_token")
ACCESS_TOKEN=$(curl -s --data "refresh_token=${REFRESH_TOKEN}" --data "client_id=${CLIENT_ID}" --data "client_secret=${CLIENT_SECRET}" --data "grant_type=refresh_token" https://www.googleapis.com/oauth2/v4/token |  jq ".access_token")
echo $TOKENS_JSON | jq ".access_token |=${ACCESS_TOKEN}" >tokens.json
echo access_tokenをリフレッシュしました。
このスクリプトはconfig.jsonにあるCLIENT_IDとCLIENT_SECRET、tokens.jsonにあるリフレッシュトークン、から新たなアクセストークンを取得して、tokens.jsonのaccess_tokenの値を更新します。

アクセストークンの有効期限は3600秒、つまり1時間あるので、このスクリプトは1時間に1回だけ実行すればよいです。

(2019.2.10追記。回線が繋がっていないなどでアクセストークンの取得に失敗するとtokens.jsの書き込みに失敗してリフレッシュトークンも失ってしまうので、このスクリプトは修正が必要です。)
#! /bin/bash
echo access_tokenをリフレッシュします。
eval "$(jq -r '.APIAppCredentials | to_entries | .[] | .key + "=\"" + .value + "\""' config.json)"
TOKENS_JSON=$(cat tokens.json)
REFRESH_TOKEN=$(echo $TOKENS_JSON | jq -r ".refresh_token")
ACCESS_TOKEN=$(curl -s --data "refresh_token=${REFRESH_TOKEN}" --data "client_id=${CLIENT_ID}" --data "client_secret=${CLIENT_SECRET}" --data "grant_type=refresh_token" https://www.googleapis.com/oauth2/v4/token |  jq ".access_token")
if [ -n $ACCESS_TOKEN ]; then
  echo $TOKENS_JSON | jq ".access_token |=${ACCESS_TOKEN}" >tokens.json
  echo access_tokenをリフレッシュしました。
else
  echo access_tokenのリフレッシュに失敗しました。
fi
これはACCESS_TOKENにnullという文字列が入ってくるのでうまくいきませんでした。)

(2019.3.8追記
#! /bin/bash
echo access_tokenをリフレッシュします。
eval "$(jq -r '.APIAppCredentials | to_entries | .[] | .key + "=\"" + .value + "\""' config.json)"
TOKENS_JSON=$(cat tokens.json)
REFRESH_TOKEN=$(echo $TOKENS_JSON | jq -r ".refresh_token")
ACCESS_TOKEN=$(curl -s --data "refresh_token=${REFRESH_TOKEN}" --data "client_id=${CLIENT_ID}" --data "client_secret=${CLIENT_SECRET}" --data "grant_type=refresh_token" https://www.googleapis.com/oauth2/v4/token |  jq ".access_token")
NEW_TOKEN=$(echo $TOKENS_JSON | jq ".access_token |=${ACCESS_TOKEN}")
if [ -n "$NEW_TOKEN" ]; then
  echo $NEW_TOKEN >tokens.json
  echo access_tokenをリフレッシュしました。
else
  echo access_tokenのリフレッシュに失敗しました。
  LOG_FILE="refreshTokens.log"
  echo $(date +%Y%m%d_%H%M%S) failed >> "${LOG_FILE}" 2>&1
fi
失敗する原因がはっきりしないので失敗したときにrefreshTokens.logに時刻を書き込むことにしました。)

 ファイルをアップロードするupload.sh

#!/bin/bash
eval "$(jq -r '.JOB | to_entries | .[] | .key + "=\"" + (.value | tostring) + "\""' config.json)"
ACCESS_TOKEN=$(jq -r ".access_token" tokens.json)
for FILE in $(eval find $SOURCE_FOLDER $FIND_OPTIONS); do
 echo $FILEをアップロードします。
 UPLOAD_TOKEN=$(curl -s -X POST -H "Authorization: Bearer ${ACCESS_TOKEN}" -H "Content-type: application/octet-stream" -H "X-Goog-Upload-File-Name: $(basename $FILE)" -H "X-Goog-Upload-Protocol: raw" --upload-file "${FILE}" https://photoslibrary.googleapis.com/v1/uploads)
 curl -s -X POST -H "Authorization: Bearer ${ACCESS_TOKEN}" -H "Content-type: application/json" -d '{ "newMediaItems":[ { "simpleMediaItem": { "uploadToken": "'${UPLOAD_TOKEN}'"}}]}' https://photoslibrary.googleapis.com/v1/mediaItems:batchCreate
done
tokens.jsonからアクセストークンを取得してそれを使ってGoogleフォトにファイルをアップロードしています。

2行目のjqでtostring関数を使っているのは、今後config.jsonの値にブーリアンを使うことがあるかもしれない時のためです。

値がtrueやfalseだとtostringがないと文字列とブーリアンは結合できない、と言われます。

このupload.shを実行するたびにファイルがアップロードされます。
pq@pq-VirtualBox:/media/sf__VirtualBox/画像サーバー用/shellscript$ ./upload.sh
/home/pq/uploads/IMG_2888.JPGをアップロードします。
{
  "newMediaItemResults": [
    {
      "uploadToken": "*****",
      "status": {
        "message": "OK"
      },
      "mediaItem": {
        "id": "*****",
        "productUrl": "https://photos.google.com/*****",
        "mimeType": "image/jpeg",
        "mediaMetadata": {
          "creationTime": "2017-10-15T22:30:37Z",
          "width": "1536",
          "height": "1024"
        },
        "filename": "IMG_2888.JPG"
      }
    }
  ]
}
アップロードが成功したファイルはこのような出力がでます。

エラー処理はしていないので、うまくいかないときはtokens.jsonを開いてアクセストークンとリフレッシュトークンが正しく取得できているか確認してください。

jqのパースエラーがでてくるときはconfig.jsonの値のJSON文字列がエスケープができていないなどのJSON構文が間違っていると思われます。

cronで定期的にファイルをアップロードする


*/10 * * * * ~/upload.sh
57 * * * * ~/refreshTokens.sh
シェルスクリプトをすべてホームフォルダに置いているとします。

これで10分ごとにupload.shでファイルをアップロードします。

毎時57分にrefreshTokens.shでアクセストークンを更新しています。


「容量を解放」してアップロードしたファイルの消費容量を減らす



Googleフォトのウェブ上でアップロードしたファイルはGoogleフォトの「設定」で「高画質」を選択していると自動的に圧縮されて容量を消費しないようになっています。

しかし、Google Photos Library APIでアップロードしたファイルはこの設定にかかわらず「元のサイズ」でのアップロードになり容量を消費します(API limits and quotas  |  Google Photos APIs  |  Google Developers)。

「容量を解放」するとすでにアップロードしたファイルを「高画質」に変換できます。


「圧縮」をクリックすると非常に時間がかかると確認されます。


「圧縮」をクリックすると「圧縮」が始まり、圧縮中と表示されますが、圧縮が終了しても表示は変わらないので、いつ圧縮が終了したのかはわかりません。

参考にしたサイト


Man page of FIND
findコマンドのマニュアル。findコマンドはファイルの抽出にはかなり便利です。

Photos Library API  |  Google Photos APIs  |  Google Developers
Google Photos Library APIのリファレンス。各メソッドのscopeがわかります。

sh - shell scriptでコマンドの引数として渡す文字列を変数でセットする方法|teratail
JSONの値は文字列なのでコマンドの引数にするときはevalが必要でした。

Raspberry Pi ネコ観察カメラの運用をGooglePhotoAPIsに移行する | kimagre inrash
アルバムに定期アップロードする例。引用符などが全角文字になっているので注意です。

[追記あり] Google Photos APIsでアルバム作成と写真のアップロード - Qiita
Googleフォトからのデータの取得方法も解説されています。

Google Photos Library APIで写真をアップロードする - GeekFactory
–data-binaryでアップロードすると画像ファイルではないと言われるので、この例のように--upload-fileでアップロードする必要がありました。

Authentication and authorization scopes  |  Google Photos APIs  |  Google Developers
Google Photos APIのscopeの解説。

json - Can I pass a string variable to jq not the file? - Stack Overflow
JSON文字列をヒアドキュメントでjqに渡す方法。

shellでjsonを扱う方法 - Qiita
JSON文字列をパイプラインでjqに渡す方法。

JSON array to bash variables using jq - Unix & Linux Stack Exchange
JSONの値をループを使わずキーと同名の変数に一挙に代入する方法。

json - How do I use jq to convert number to string? - Stack Overflow
jqのtostring関数の使い方の例。

API limits and quotas  |  Google Photos APIs  |  Google Developers
Google Photos APIでアップロードした画像は容量を消費します。

Free Online JSON Escape / Unescape Tool - FreeFormatter.com
JSON文字列のエスケープをしてくれるサイト。

シェルで変数の空文字判定 | ハックノート
シェル変数の空文字判定。

ブログ検索 by Blogger

Translate

最近のコメント

Created by Calendar Gadget

QooQ