LambdaでDropboxにファイルアップロード

## データバックアップをcsv形式で外部ストレージに
例えば**RDS**や**DynamoDB**上のデータを定期的に抽出して外部ストレージに置いておきたい、しかも**S3**ではなく**Dropbox**に保存したいというケースがあったとします。
そこで、Lambdaから[Dropbox API v2](https://www.dropbox.com/developers/documentation/http/overview)を呼ぶ形で実装します。

以前書いた[PHPでDropbox APIを使ってアップロードする記事](https://qiita.com/tksnino/items/bcbde6eddcb553446f63)の応用となります。

### 利用するLambdaランタイム
- Python3.8

## 1. csvファイルを出力する
まずはデータベースから抽出したデータを整形してcsvとして出力する部分です。
`csv`ライブラリでさくっと。
```python
    import csv
    #import os
    
    file = '/tmp/ファイル名.csv'
    # 環境変数で管理するのもよいです
    # file = '/tmp/' + os.environ['FILENAME']
    
    cols = ['項目名1', '項目名2', '項目名3', ...]
    
    with open(file, 'w') as f:
        writer = csv.DictWriter(f, fieldnames=cols)
        writer.writeheader()
    
        for row in rows: # DBから読み込んだデータ配列
            writer.writerow({ \
                '項目名1': row['col_1'], \
                '項目名2': row['col_2'], \
                '項目名3': row['col_3'], \
                ... \
            })
```
ポイントは、出力するファイルは`/tmp/`以下を指定することです。
そうしないと書き込み権限がないというエラーが発生します。
（参考）[aws lambda file書き込む時のエラー](https://qiita.com/namkim/items/3edb9abe3871963bf0f7)

## 2. ファイルをDropboxにアップロード
[こちらの記事](https://qiita.com/tksnino/items/c39b509494cc7f05954a)で追記したように、Dropbox APIは短期的なアクセストークンは都度取得し直す必要があります。
予めリフレッシュトークンを取得しておいてください。
（参考）[Dropbox API: アプリの権限とアクセス トークンの移行](https://navi.dropbox.jp/dropbox-api-migrating-app-permissions-and-access-tokens)

### 2-1. アクセストークン取得
Pythonは[Dropbox公式SDK](https://dropbox-sdk-python.readthedocs.io/en/latest/index.html)があるので、予めローカルでインストールしてパッケージとしてzipファイルをLambdaにアップすればよいのですが、なるべく余計なライブラリを追加せずにやりたかったので、Lambda標準の[urllib3](https://urllib3.readthedocs.io/en/stable/index.html)でAPIエンドポイントを叩いていく方法でいきます。
（参考）[Dropbox API v2 Document](https://www.dropbox.com/developers/documentation/http/documentation)#/oauth2/token

```python
    import os
    import urllib3
    
    # Dropbox API用の APP_KEY, APP_SECRET, REFRESH_TOKEN は環境変数管理がよいでしょう
    dbx_app_key = os.environ['DROPBOX_APP_KEY']
    dbx_app_secret = os.environ['DROPBOX_APP_SECRET']
    dbx_refresh_token = os.environ['DROPBOX_REFRESH_TOKEN']
    
    http = urllib3.PoolManager()
    
    # アクセストークン取得エンドポイント
    url = 'https://api.dropboxapi.com/oauth2/token'
    # APP_KEY,APP_SECRETはBasic認証のID・パスワードとして
    header = urllib3.util.make_headers(basic_auth='{}:{}'.format(dbx_app_key, dbx_app_secret))
    # リクエストパラメータ
    data = { \
        'grant_type': 'refresh_token', \
        'refresh_token': dbx_refresh_token \
    }
    
    resp = http.request('POST', url, headers=header, fields=data)
    
    if resp.status == 200:
        res = json.loads(resp.data)
        dbx_access_token = res['access_token'] # アクセストークン取得
```
エラー処理は省略しています。

### 2-2. ファイルアップロード
そして取得したアクセストークンを使ってアップロードです。

```python
    import json
    import urllib3
    #import os
    
    # ファイルアップロードエンドポイント
    url = 'https://content.dropboxapi.com/2/files/upload'
    
    # Dropbox上のパス "/"始まりで記載
    dbx_up_path = '/バックアップフォルダ/バックアップファイル名.csv'
    # 環境変数で管理するのもよいです
    # dbx_up_path = os.environ['DROPBOX_DIR'] + os.environ['FILENAME']
    
    # ヘッダーの"Dropbox-API-Arg"パラメータにJSON形式でパスやその他オプションを指定
    dbx_param = { \
        'path': dbx_up_path, \
        'mode': { \
            '.tag': 'overwrite' \ # 上書きモード
        }
    }
    
    header = { \
        'Authorization': 'Bearer ' + dbx_access_token, \ # 2-1.で取得したアクセストークン
        'Content-Type': 'application/octet-stream', \
        'Dropbox-API-Arg': json.dumps(dbx_param) \
    }
    
    with open(file, 'rb') as fp: # 1.で出力したファイルパス
        bin = fp.read()
    
        # ファイルバイナリデータをリクエストパラメータとして
        resp = http.request('POST', url, headers=header, body=bin) # コネクションプール(http)は2-1.で取得したもの
    
        if resp.status == 200:
            print('SUCCESS!')
```
これで実際、Dropbox上にファイルがアップされていれば成功です。

### おまけ
応用として、複数のcsvファイルを出力しておいて順次アップロードという形もできます。
（参考）[Python でディレクトリ内のファイルをループする](https://www.delftstack.com/ja/howto/python/loop-through-files-in-directory-python/?utm_source=pocket_mylist)
以下、主に変更部分だけ。
```python
    from pathlib import Path
    
    pathlist = Path('/tmp/').glob('*.csv')
    for path in pathlist:
    ...
        dbx_param = { \
            'path': '/バックアップフォルダ/' + path.name, \ # ファイル名
            'mode': { \
                '.tag': 'overwrite' \
            }
        }
    ...
        with open('/tmp/' + path.name, 'rb') as fp: # ファイルパス
            bin = fp.read()
    ...
```

以上です。
```urllib3```の使い方にやたらと四苦八苦したのですが、終わってみれば割とシンプルでした…