Lambda(Python)でGoogle Driveへファイルアップロード

# Lambda(Python)でGoogle Driveへファイルアップロード

この記事は[サーバーレスWebアプリ Mosaic](https://mosaic.w2or3w.com "Mosaic")を開発して得た知見を振り返り定着させるための[ハンズオン記事](https://qiita.com/w2or3w/items/87b57dfdbcf218de91e2)の1つです。

以下を見てからこの記事をみるといい感じです。
* [Lambda(Python) + Rekognition で顔検出](https://qiita.com/w2or3w/items/48bc05684c6dafbf5253)

## イントロダクション
S3ってファイルを保存するだけならいいのですが、そのファイルを人が参照するのには向いてないですよね。画像にしてもWebブラウザ上でそのまま見ることができず、一度ダウンロードしてから見る必要がある。ちょっと手間なんですよね。
その点、Google DriveはPCにしろスマホにしろ、ブラウザやアプリでいい感じに画像を参照することができますのでいい感じです。

## コンテンツ

### Google Developers Console プロジェクトの作成

GoogleのDevelopers Consoleのにアクセスします。
https://console.developers.google.com

プロジェクトが無い場合は作成してください。
![Screenshot 2020-01-06 at 22.43.06.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/394775/6e5a7863-4357-a6be-6189-cb7465210f63.png)

### Google Drive APIの有効化
コンソールの「APIとサービス」画面上部にある「＋APIとサービスを有効化」を押下し、Google Driveを有効化します。
![Screenshot 2020-01-06 at 22.45.12.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/394775/868eacdb-cfa7-545f-39ea-41e9aa15ab9a.png)
![Screenshot 2020-01-06 at 22.47.53.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/394775/0790db57-2c1b-19ba-e421-a32b8a91bb00.png)
![Screenshot 2020-01-06 at 22.48.10.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/394775/4573e00b-e2fa-6bad-5851-627213b28af6.png)

### 認証情報の作成
Google Drive APIの有効化が完了したら、コンソールの「APIとサービス」画面左ペインの「認証情報」から、認証情報を作成します。
認証情報のページ上部にある「＋認証情報を作成」を押下すると表示されるドロップダウンメニューより、「サービスアカウント」を選択します。
![Screenshot 2020-01-06 at 22.56.00.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/394775/31df2c4f-6236-abd4-c5cf-92e06ba770f8.png)

サービスアカウント名を入力して作成してください。
なお、サービスアカウント名の下にあるサービスアカウントIDは大切な情報なので漏洩しないよう管理してください。
![ink (1).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/394775/7f99da01-fb03-9a07-766e-55fd8870026f.png)

### サービスアカウントの確認
サービスアカウントの作成が完了したら、コンソールの「IAM と 管理」画面左ペイン「サービスアカウント」から、作成したサービスアカウントの詳細を参照します。
![ink (2).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/394775/0db177ef-6fea-e089-29ac-e5253bf69a31.png)

### サービスアカウント秘密鍵の作成
サービスアカウントの詳細から、「編集」そして「＋キーを作成」ボタンを押下し、秘密鍵をJSON形式で作成、JSONファイルをダウンロードします。
![ink (4).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/394775/2a87eb9a-1b8a-bc15-f77b-f4491ed59860.png)

### Google Driveにフォルダを作成し、サービスアカウントと共有する
Google Driveにファイルアップロード用のフォルダを作成します。ここでは`sample-drive`というフォルダ名としました。そしてそのフォルダを先ほど作成したサービスアカウント(メールアドレス)と共有します。
![Screenshot 2020-01-06 at 23.11.04.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/394775/9f711ac0-cf30-be3c-93da-b6a21f8e310d.png)
![ink (3).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/394775/8712b604-a024-813b-ea0f-91ea223cd44f.png)

このフォルダのURLの後ろの文字列は、後でプログラムから利用します。(下のキャプチャの隠してる部分)
![ink (5).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/394775/892f89e8-9287-54b8-1e7b-1635e162948e.png)

さてこれでGoogle側の設定は完了です。
続いて、プログラムからGoogle APIでファイルをアップロードしましょう。

### Lambda(Python)からGoogle APIをサービスアカウントでリクエストする
まずは、先ほどダウンロードしたサービスアカウントの秘密鍵JSONファイルを、適当な名前にリネームしてlambda_function.pyと同じ場所においておきます。ここでは「service-account-key.json」という名前としました。

続いて、必要なライブラリをインストールします。

```
$ pip install google-api-python-client -t .
$ pip install oauth2client -t .
```

で、必要なものをインポートします。

```lambda_function.py
  :
from googleapiclient.discovery import build 
from googleapiclient.http import MediaFileUpload 
from oauth2client.service_account import ServiceAccountCredentials 
  :
```

そして、以下のような実装でファイルをアップロードします。

```lambda_function.py
  :
def uploadFileToGoogleDrive(fileName, localFilePath):
    try:
        ext = os.path.splitext(localFilePath.lower())[1][1:]
        if ext == "jpg":
            ext = "jpeg"
        mimeType = "image/" + ext

        service = getGoogleService()
        file_metadata = {"name": fileName, "mimeType": mimeType, "parents": ["*********************************"] } 
        media = MediaFileUpload(localFilePath, mimetype=mimeType, resumable=True) 
        file = service.files().create(body=file_metadata, media_body=media, fields='id').execute()

    except Exception as e:
        logger.exception(e)

def getGoogleService():
    scope = ['https://www.googleapis.com/auth/drive.file'] 
    keyFile = 'service-account-key.json'
    credentials = ServiceAccountCredentials.from_json_keyfile_name(keyFile, scopes=scope)
    
    return build("drive", "v3", credentials=credentials, cache_discovery=False) 
```
"parents": ["*********************************"]この部分はGoogle Driveに作成したフォルダのURLの後ろ側の文字列に置き換えてください。

最後に念の為プログラム全体も載せておきますね。

```lambda_function.py
# coding: UTF-8
import boto3
import os
import json
from urllib.parse import unquote_plus
import numpy as np
import cv2
import logging
logger = logging.getLogger()
logger.setLevel(logging.INFO)
s3 = boto3.client("s3")
rekognition = boto3.client('rekognition')

from gql import gql, Client
from gql.transport.requests import RequestsHTTPTransport
ENDPOINT = "https://**************************.appsync-api.ap-northeast-1.amazonaws.com/graphql"
API_KEY = "da2-**************************"
_headers = {
    "Content-Type": "application/graphql",
    "x-api-key": API_KEY,
}
_transport = RequestsHTTPTransport(
    headers = _headers,
    url = ENDPOINT,
    use_json = True,
)
_client = Client(
    transport = _transport,
    fetch_schema_from_transport = True,
)

from googleapiclient.discovery import build 
from googleapiclient.http import MediaFileUpload 
from oauth2client.service_account import ServiceAccountCredentials 

def lambda_handler(event, context):
    bucket = event["Records"][0]["s3"]["bucket"]["name"]
    key = unquote_plus(event["Records"][0]["s3"]["object"]["key"], encoding="utf-8")
    logger.info("Function Start (deploy from S3) : Bucket={0}, Key={1}" .format(bucket, key))

    fileName = os.path.basename(key)
    dirPath = os.path.dirname(key)
    dirName = os.path.basename(dirPath)
    
    orgFilePath = "/tmp/" + fileName
    
    if (not key.startswith("public") or key.startswith("public/processed/")):
        logger.info("don't process.")
        return
    
    apiCreateTable(dirName, key)

    keyOut = key.replace("public", "public/processed", 1)
    dirPathOut = os.path.dirname(keyOut)

    try:
        s3.download_file(Bucket=bucket, Key=key, Filename=orgFilePath)

        orgImage = cv2.imread(orgFilePath)
        grayImage = cv2.cvtColor(orgImage, cv2.COLOR_RGB2GRAY)
        processedFileName = "gray-" + fileName
        processedFilePath = "/tmp/" + processedFileName
        uploadImage(grayImage, processedFilePath, bucket, os.path.join(dirPathOut, processedFileName), dirName, False)

        uploadFileToGoogleDrive(key, orgFilePath)
        detectFaces(bucket, key, fileName, orgImage, dirName, dirPathOut)

    except Exception as e:
        logger.exception(e)
        raise e
        
    finally:
        if os.path.exists(orgFilePath):
            os.remove(orgFilePath)

def uploadImage(image, localFilePath, bucket, s3Key, group, isUploadGoogleDrive):
    logger.info("start uploadImage({0}, {1}, {2}, {3})".format(localFilePath, bucket, s3Key, group))
    try:
        cv2.imwrite(localFilePath, image)
        s3.upload_file(Filename=localFilePath, Bucket=bucket, Key=s3Key)
        apiCreateTable(group, s3Key)
        if isUploadGoogleDrive:
            uploadFileToGoogleDrive(s3Key, localFilePath)
    except Exception as e:
        logger.exception(e)
        raise e
    finally:
        if os.path.exists(localFilePath):
            os.remove(localFilePath)

def apiCreateTable(group, path):
    logger.info("start apiCreateTable({0}, {1})".format(group, path))
    try:
        query = gql("""
            mutation create {{
                createSampleAppsyncTable(input:{{
                group: \"{0}\"
                path: \"{1}\"
              }}){{
                group path
              }}
            }}
            """.format(group, path))
        _client.execute(query)
    except Exception as e:
        logger.exception(e)
        raise e

def detectFaces(bucket, key, fileName, image, group, dirPathOut):
    logger.info("start detectFaces ({0}, {1}, {2}, {3}, {4})".format(bucket, key, fileName, group, dirPathOut))
    try:
        response = rekognition.detect_faces(
            Image={
                "S3Object": {
                    "Bucket": bucket,
                    "Name": key,
                }
            },
            Attributes=[
                "ALL",
            ]
        )

        name, ext = os.path.splitext(fileName)
        
        jsonFileName = name + ".json"
        localPathJSON = "/tmp/" + jsonFileName
        with open(localPathJSON, 'w') as f:
            json.dump(response, f, ensure_ascii=False)
        s3.upload_file(Filename=localPathJSON, Bucket=bucket, Key=os.path.join(dirPathOut, jsonFileName))
        if os.path.exists(localPathJSON):
            os.remove(localPathJSON)
        
        imgHeight = image.shape[0]
        imgWidth = image.shape[1]
        index = 0
        for faceDetail in response["FaceDetails"]:
            index += 1
            faceFileName = "face_{0:03d}".format(index) + ext
            box = faceDetail["BoundingBox"]
            x = max(int(imgWidth * box["Left"]), 0)
            y = max(int(imgHeight * box["Top"]), 0)
            w = int(imgWidth * box["Width"])
            h = int(imgHeight * box["Height"])
            logger.info("BoundingBox({0},{1},{2},{3})".format(x, y, w, h))
            
            faceImage = image[y:min(y+h, imgHeight-1), x:min(x+w, imgWidth)]
            
            localFaceFilePath = os.path.join("/tmp/", faceFileName)
            uploadImage(faceImage, localFaceFilePath, bucket, os.path.join(dirPathOut, faceFileName), group, False)
            cv2.rectangle(image, (x, y), (x+w, y+h), (0, 0, 255), 3)

        processedFileName = "faces-" + fileName
        processedFilePath = "/tmp/" + processedFileName
        uploadImage(image, processedFilePath, bucket, os.path.join(dirPathOut, processedFileName), group, True)
    except Exception as e:
        logger.exception(e)
        raise e


def uploadFileToGoogleDrive(fileName, localFilePath):
    try:
        ext = os.path.splitext(localFilePath.lower())[1][1:]
        if ext == "jpg":
            ext = "jpeg"
        mimeType = "image/" + ext

        service = getGoogleService()
        file_metadata = {"name": fileName, "mimeType": mimeType, "parents": ["*********************************"] } 
        media = MediaFileUpload(localFilePath, mimetype=mimeType, resumable=True) 
        file = service.files().create(body=file_metadata, media_body=media, fields='id').execute()

    except Exception as e:
        logger.exception(e)

def getGoogleService():
    scope = ['https://www.googleapis.com/auth/drive.file'] 
    keyFile = 'service-account-key.json'
    credentials = ServiceAccountCredentials.from_json_keyfile_name(keyFile, scopes=scope)
    
    return build("drive", "v3", credentials=credentials, cache_discovery=False) 
```

### 動作確認
Webアプリから画像をアップロードすると、LambdaのPythonが動いて画像処理をしますが、そのついでにGoogle Driveにもオリジナル画像と顔にROIをマーキングした画像の2つをアップロードしています。
S3と違ってそのまま絵として見れて嬉しいですね。
![Screenshot 2020-01-08 at 22.51.30.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/394775/e8cf7fbf-f84c-50c4-c910-d53a4ee7a2c4.png)

## あとがき
Googleのクラウドサービスもそのうち一通り触ってみたいですね。Google好きですので。2020年の目標の1つです。

ちなみにワタシの身の回りのGoogle製品は、Pixel 3a, Chromebook, Google Home Mini, Chromecast, Google Wifiなど、それなりに多いです。
サービスは、Gmail, Calendar, Drive, Photoなどはもちろん、Google FitをMi Bandとともに日常的に利用してますし、Google Mapはローカルガイド気取りで投稿して楽しんでいます。
Google Oneも2TBにアップグレード(年額¥13,000)しています。

OK Google, だいすきだよ！