ラズベリーパイZERO-WHでカメラ画像をGoogleDriveにアップロードする(python+pydrive)

#初めに
ラズベリーパイ(Raspberry Pi Zero WH)を用いて、監視カメラを作成する前に、撮影した画像などをネットワーク上に保存できる環境を構築します。
指定したファイルをGoogleドライブにUploadする方法はネット上にいくつもありましたが、監視カメラ用の画像のアップロードとなると、たくさんの画像を上げることになります。また参照を容易にするためにも、指定した場所に日付などのフォルダーを作成して格納する必要があります。このほか実際には、通信エラーなどでうまくアップロードできない場合もあるので、指定ディレクトリのファイルをまとめて行うようにしています。

# 利用しているハードウエア
* Raspberry Pi Zero WH
* Raspberry Zero V2 Camera (今回のプログラムには不要)
* microSD カード(32GB)

# 利用しているソフトウエア
* Linux raspberrypi 5.4.35+ #1314 Fri May 1 17:36:08 BST 2020 armv6l GNU/Linux
* Python 3.7.3
* pydrive 1.3.1

# 構築準備
構築にはいろいろなサイトのお世話になっておりますが、試行錯誤のためほとんど整理できていません。
このあたり、記事を書くための準備や整理をするのでしょうが、うまくできていません。初めて書くQiitaでの反省点かもしれません。まとめ作業はなかなか大変ですね。Qiitaだけでなく、ラズベリーパイもpythonも初心者なので、実際の記述方法ややり方をいちいち確認しなければなりません。余計に時間がかかってしまいます。
ともあれ、記憶している中の参考にしたサイトについていくつか挙げておきます。

* ラズベリーパイのセットアップでは
 * sgi-changさんの [ラズパイ初心者がRaspberry Pi Zero WHでドライブレコーダーを作ってみたらこうなった](https://www.cresco.co.jp/blog/entry/9976/)

* PyDriveについては
 * ganganさんの [【Raspberry Pi】Raspberry Piで撮影した画像をGoogle Driveにアップロードさせてみる ~PyDrive編~](https://gangannikki.hatenadiary.jp/entry/2019/09/14/233000)

* Wifi設定
 * 芽萌丸さんの [ヘッドレスなラズパイゼロをケーブルの類を一切使わずにセットアップ](https://memo.appri.me/iot/rpi0-headless-setup)

　　等です。モニタを接続したほうがいろいろ簡単ですので、セットアップの際にはモニタ+キーボード+USBは用意すべきです（ただし、一部（WIFI）の設定はGUIツールにより手動設定が戻ってしまいますので注意が必要）。実際にpyDriveの設定時には、インストールマシン上のWEBブラウザから実施すべき作業があります。TTYでできる方法を試しましたができませんでした。

# 前提
以下の環境を前提としています。インストール作業や細かな設定は参考サイトにあると思いますので、ここでは省略します。

* ラズベリーパイのセットアップができている
* ネットワークに接続し、Googleにアクセスできる
* python3及びpip3のセットアップができている
* Googleアカウントを作成し、GoogleDriveをブラウザでアクセスできること
* PyDriveのインストールおよび設定ができていること
* Google Developers ConsoleへアクセスしてクライアントIDを取得できていること

 ※ PyDriveのインストールの際に、Googleの認証を得るためにアクセスします。その際には、ラズベリーパイ上のWEBブラウザで操作が必要になります(ttyで利用できるテキストブラウザでは、認証情報を取得できません）。

# googleDriveにアップロードするPythonプログラムの使い方

　作成するpyhonプログラムは、「googleUploadDir.py」で作成します。
　カメラ画像をgoogleドライブの指定位置に日付(yyyymmdd)フォルダにアップロードします。しかしアップロードに問題があったり、処理が集中したりするとうまくいかないこともあるので、おせっかいな機能を付与しています。
　指定する送信ファイルは、ルートからのフルパスで指定します。
　ファイルのあるディレクトリは、アプロード専用とします。
　そのディレクトリにあるデータは原則googleDriveにアップロードするようにしています（必要に応じてmimeTypeの設定を追加してください）。

```
USAGE：$ python3 googleUploadDir.py /tmp/clock.jpg 
```

　「python3」は、プログラムファイルに実行権を与えることで不要になります。
　「/tmp/clock.jpg」はアップロードするファイルです。実際にルート(/)からのフルパスで指定します。

# pythonプログラム
 以下に、プログラムを示します。プログラムを動かす前に1か所、googleDriveの格納先フォルダを指定する必要があるので次の説明を必ず確認してください。

```python:googleUploadDir.py 
     1  #!/usr/bin/env /usr/bin/python3
     2  # -*- coding: utf-8 -*-
     3  # Created on 2020年  5月  22日 土曜日 18:05:07 JST
     4  # @author: ochiai
     5  import os
     6  import sys
     7  import time
     8  import pprint
     9  import datetime as dt
    10  from pydrive.auth import GoogleAuth
    11  from pydrive.drive import GoogleDrive
    12 
    13  FOLDER_ID_TOP = "*********************************"
    14 
    15  class  GoogleuploadClass: # Google upload class
    16      gauth = None
    17      drive = None
    18      
    19      def __init__(self): # GoogleDrive initialize
    20          self.gauth = GoogleAuth()                    # 認証CLASS
    21          self.gauth.LocalWebserverAuth()              # 認証の確認
    22          self.drive = GoogleDrive(self.gauth)         # G-Driveインタフェースの取得
    23 
    24      def search_folder(self, dir): # <<FOLDER_ID_TOP>>にdirフォルダを探し、なければ作成する
    25          f_files = self.drive.ListFile({"q": '"{}" in parents'.format( FOLDER_ID_TOP )}).GetList()
    26          for f in f_files :
    27              if f['mimeType'] == 'application/vnd.google-apps.folder' and f['title'] == dir :
    28                  return f 
    29          f_folder = self.drive.CreateFile({"parents": [{"id": FOLDER_ID_TOP}], 'title': dir
    30              , 'mimeType': 'application/vnd.google-apps.folder'})
    31          f_folder.Upload()  # 指定フォルダ以下にdir名のフォルダを作成
    32          return self.drive.ListFile().GetList()[0]
    33 
    34      def upload_google(self, upload_file): # upload_fileをgoogleDriveに作成する
    35          dir = dt.datetime.now().strftime("%Y%m%d")   # 今日の格納先フォルダ名
    36          f = self.search_folder( dir )                # 格納先フォルダ情報を取得
    37          folder_id = f['id']
    38          m_type = self.get_mimetype( upload_file )
    39          f = self.drive.CreateFile({"parents": [{"id": folder_id, "mimeType":m_type}]})
    40          f.SetContentFile( upload_file )              # 送信するファイルを設定
    41          f['title'] = os.path.basename( upload_file ) # ファイル名を設定
    42          f.Upload()                                   # 送信
    43 
    44      def uploads_google_and_del(self, files, dirname):# filesをgoogleDriveに作成後削除
    45          dir = dt.datetime.now().strftime("%Y%m%d")   # 今日の格納先フォルダ名
    46          f = self.search_folder( dir )                # 格納先フォルダ情報を取得
    47          folder_id = f['id']
    48          for ff in files:
    49              ff_name = os.path.join( dirname, ff)
    50              m_type = self.get_mimetype( ff )
    51              f = self.drive.CreateFile({"parents": [{"id": folder_id, "mimeType":m_type}]})
    52              f.SetContentFile( ff_name )              # 送信するファイルを設定
    53              f['title'] = ff                          # ファイル名を設定
    54              f.Upload()                               # 送信
    55              print( "upload end ("+ff_name + ")" )
    56              os.remove( ff_name ) # remove file
    57 
    58      def get_mimetype( self, filename ) :             # file名よりmime/typeを推定
    59          if filename.endswith( '.jpg' ) :
    60              return( "image/jpeg" )
    61          elif filename.endswith( '.mp4' ) :
    62              return( "video/mpeg" )
    63          elif filename.endswith( '.wvi' ) :
    64              return( "video/x-msvideo" )
    65          elif filename.endswith( '.log' ) :
    66              return( "text/plain" )
    67          elif filename.endswith( '.txt' ) :
    68              return( "text/plain" )
    69          else :
    70              return( "text/plain" )
    71 
    72  # main ====================================================
    73  try:
    74      if __name__ == "__main__":
    75 
    76          upload = GoogleuploadClass()             # Classの取得(初期化)
    77          filename = sys.argv[1]
    78          if os.path.exists(filename):             # Argv[1]のファイルの確認
    79              dirname = os.path.dirname(filename)
    80              if os.path.exists(dirname):          # Argv[1]のファイルの確認
    81                  files = [ff for ff in os.listdir(dirname) if os.path.isfile( os.path.join( dirname, ff ))]
    82                  upload.uploads_google_and_del(files, dirname) # ファイルリストの送信と送信後の削除
    83          else:
    84              print("argv: googleUploadDir filename (ファイルのあるディレクトリをまとめて送信)")
    85          
    86  except TypeError as e:
    87      print('Google-upload TypeError:', e)
```

# 格納先フォルダ（フォルダーID)の設定

* プログラム中の13行目にある、以下の文字列には、GoogleDriveのFolderIDに置き換えてください。

 ```
    13  FOLDER_ID_TOP = "*********************************"
 ```

* GoggleDriveをWEBブラウザで開き、格納したいフォルダ名のところで右クリックするとコンテキストメニューを表示します。その中の「共有可能なリンクを取得」を選びURLをコピーします。
そのURLの中の「id=」の次に始まる文字列が指定したフォルダのIDとなります。
これを13行目のプログラム"****   ****"と置き換えてください。
 　　![GoogleDrive-Folder-id.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/244077/0e993124-cc14-dbd6-52f9-7d7990ae921b.jpeg)

## メインプログラムの解説

* upload用のクラスを作成します。また、引数のファイルがあるかなどを確認し、格納先のファイルをリストにまとめてから送信用のメソッドを呼び出します。ここでは、フォルダ内のすべてのファイルをリストに入れていますが、必要に応じて取捨選択できるようにしています。

```
    72  # main ====================================================
    73  try:
    74      if __name__ == "__main__":
    75 
    76          upload = GoogleuploadClass()             # Classの取得(初期化)
    77          filename = sys.argv[1]
    78          if os.path.exists(filename):             # Argv[1]のファイルの確認
    79              dirname = os.path.dirname(filename)
    80              if os.path.exists(dirname):          # Argv[1]のファイルの確認
    81                  files = [ff for ff in os.listdir(dirname) if os.path.isfile( os.path.join( dirname, ff ))]
    82                  upload.uploads_google_and_del(files, dirname) # ファイルリストの送信と送信後の削除
    83          else:
    84              print("argv: googleUploadDir filename (ファイルのあるディレクトリをまとめて送信)")
    85          
    86  except TypeError as e:
    87      print('Google-upload TypeError:', e)
 ```

* 使用しているメソッドは、upload.uploads_google_and_del()で、名前にあるようにアップロードに成功したものは削除するようにしています。実際にファイルをアップロードする途中でいろいろエラーになるので、成功したものから削除しないと同じものがアップロードされてしまいます。
エラーが発生した場合、エラーのリカバリーはあきらめています。次回実行時にまとめてアップロードするという割り切りのほうが現実的という解釈です。


## GoogleuploadClassの説明

* コンストラクタで、GoogleDriveの認証(pyDriveインストール時に合わせて認証に必要な操作を行います。ログインユーザのホームディレクトリにて実施することで、必要な認証情報を保存します。ここではそれらの情報を読み込んでアクセスに必要な認証操作を行います。）を行います。ラズパイZEROでは数秒～数十秒かかります。

```
    19      def __init__(self): # GoogleDrive initialize
```

## uploads_google_and_delの説明

* 保存する先のディレクトリ名を作成し、そのフォルダの情報を求めます(search_folder()にて)。結果から格納するフォルダIDを求めます。

```
    45          dir = dt.datetime.now().strftime("%Y%m%d")   # 今日の格納先フォルダ名
    46          f = self.search_folder( dir )                # 格納先フォルダ情報を取得
    47          folder_id = f['id']
```

* 次にファイルリストより再度古パス名を作成(49行目)しファイルタイプ（get_mimetype()にて求める。50行目）を推定します。
* 51行目から53行目のdrive.CreateFile()とSetContentFile()などによりアップロードするファイルの情報を設定します。
* Upload()で実際にファイル送信を行います。ファイルサイズやネット環境により時間がかかります。
* ファイル送信後に送信したファイルを削除します。
* ファイルリストすべてに対し実施して終わりになります。

```
    48          for ff in files:
    49              ff_name = os.path.join( dirname, ff)
    50              m_type = self.get_mimetype( ff )
    51              f = self.drive.CreateFile({"parents": [{"id": folder_id, "mimeType":m_type}]})
    52              f.SetContentFile( ff_name )              # 送信するファイルを設定
    53              f['title'] = ff                          # ファイル名を設定
    54              f.Upload()                               # 送信
    55              print( "upload end ("+ff_name + ")" )
    56              os.remove( ff_name ) # remove file
```

# 最後に

ラズベリーパイZERO HW は価格も安く、小さい反面、それなりの処理速度もあります。もちろんラズベリーパイ3Bなどに比べれば半分以下の処理能力です。それでも昔のPC程度の処理能力があります。環境構築でビルドを行うとやはり時間がかかることもありますが、改めてこの大きさで、これだけのことができるのかと感心しています。でも、小さくてもそれなりの消費電力ですね。電池駆動は難しいと思いました。

実は、次の記事では、監視カメラのプログラムを掲載する予定でいます。すでにプログラムを作成し実際に動作させることを含め試行錯誤を行っています。24時間動かせば、いろいろな問題に出くわすので、そのたびに考え方を変えて修正を行ってきています。このプログラムもその過程で作成しています。スレッド対応や、ファイル単位のアップロードなどではうまく解消できませんでした。
監視カメラでは外に設置することを考えると、設置の方法が意外に問題になりますよね。電源の確保や防水、設置場所での画角の調整などやってみると案外難しいです。そのあたりのことも書ければと思います。

また、記事も初めて書いていますし、pythonも初心者の状態です。また開発環境なども整備しなければなりません。ハードウエアが絡むIOTでは、やはりカメラのある実機で操作してしまいます。これゆえ、後戻りも多いのですが。。。
なかなかやりたいことよりも、その準備などに時間がかかっている状況から抜け出せていません。難しいものです。

プログラム掲載していますが、説明用に行番号を振っています。コピーする際には邪魔と思いますので、以下に行番号のないものを載せておきます。

```python:googleUploadDir.py 
#!/usr/bin/env /usr/bin/python3
# -*- coding: utf-8 -*-
# Created on 2020年  5月  22日 土曜日 18:05:07 JST
# @author: ochiai
import os
import sys
import time
import pprint
import datetime as dt
from pydrive.auth import GoogleAuth
from pydrive.drive import GoogleDrive

FOLDER_ID_TOP = "*********************************"

class  GoogleuploadClass: # Google upload class
    gauth = None
    drive = None
    
    def __init__(self): # GoogleDrive initialize
        self.gauth = GoogleAuth()                    # 認証CLASS
        self.gauth.LocalWebserverAuth()              # 認証の確認
        self.drive = GoogleDrive(self.gauth)         # G-Driveインタフェースの取得

    def search_folder(self, dir): # <<FOLDER_ID_TOP>>にdirフォルダを探し、なければ作成する
        f_files = self.drive.ListFile({"q": '"{}" in parents'.format( FOLDER_ID_TOP )}).GetList()
        for f in f_files :
            if f['mimeType'] == 'application/vnd.google-apps.folder' and f['title'] == dir :
                return f 
        f_folder = self.drive.CreateFile({"parents": [{"id": FOLDER_ID_TOP}], 'title': dir
            , 'mimeType': 'application/vnd.google-apps.folder'})
        f_folder.Upload()  # 指定フォルダ以下にdir名のフォルダを作成
        return self.drive.ListFile().GetList()[0]

    def upload_google(self, upload_file): # upload_fileをgoogleDriveに作成する
        dir = dt.datetime.now().strftime("%Y%m%d")   # 今日の格納先フォルダ名
        f = self.search_folder( dir )                # 格納先フォルダ情報を取得
        folder_id = f['id']
        m_type = self.get_mimetype( upload_file )
        f = self.drive.CreateFile({"parents": [{"id": folder_id, "mimeType":m_type}]})
        f.SetContentFile( upload_file )              # 送信するファイルを設定
        f['title'] = os.path.basename( upload_file ) # ファイル名を設定
        f.Upload()                                   # 送信

    def uploads_google_and_del(self, files, dirname):# filesをgoogleDriveに作成後削除
        dir = dt.datetime.now().strftime("%Y%m%d")   # 今日の格納先フォルダ名
        f = self.search_folder( dir )                # 格納先フォルダ情報を取得
        folder_id = f['id']
        for ff in files:
            ff_name = os.path.join( dirname, ff)
            m_type = self.get_mimetype( ff )
            f = self.drive.CreateFile({"parents": [{"id": folder_id, "mimeType":m_type}]})
            f.SetContentFile( ff_name )              # 送信するファイルを設定
            f['title'] = ff                          # ファイル名を設定
            f.Upload()                               # 送信
            print( "upload end ("+ff_name + ")" )
            os.remove( ff_name ) # remove file

    def get_mimetype( self, filename ) :             # file名よりmime/typeを推定
        if filename.endswith( '.jpg' ) :
            return( "image/jpeg" )
        elif filename.endswith( '.mp4' ) :
            return( "video/mpeg" )
        elif filename.endswith( '.wvi' ) :
            return( "video/x-msvideo" )
        elif filename.endswith( '.log' ) :
            return( "text/plain" )
        elif filename.endswith( '.txt' ) :
            return( "text/plain" )
        else :
            return( "text/plain" )

# main ====================================================
try:
    if __name__ == "__main__":

        upload = GoogleuploadClass()             # Classの取得(初期化)
        filename = sys.argv[1]
        if os.path.exists(filename):             # Argv[1]のファイルの確認
            dirname = os.path.dirname(filename)
            if os.path.exists(dirname):          # Argv[1]のファイルの確認
                files = [ff for ff in os.listdir(dirname) if os.path.isfile( os.path.join( dirname, ff ))]
                upload.uploads_google_and_del(files, dirname) # ファイルリストの送信と送信後の削除
        else:
            print("argv: googleUploadDir filename (ファイルのあるディレクトリをまとめて送信)")
        
except TypeError as e:
    print('Google-upload TypeError:', e)
```

こんな感じでよいでしょうか。

追記：(2020/5/30)
　GoogleDriveを使用してディレクトリ(フォルダ)を同期して利用する方法もあります。
　実際に検索するとフリーのドライバがありますが、ラズベリーパイ用のライブラリがないため
　コマンドをコンパイルすることができませんでした。こちらを使えば、双方向のファイルのやり取りができるので
　リモートで設定を変更することも可能になると思っていましたが、現状ではもう少し待つことにしました。