BETA

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

投稿日:2020-01-08
最終更新:2020-01-09

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

この記事はサーバーレスWebアプリ Mosaicを開発して得た知見を振り返り定着させるためのハンズオン記事の1つです。

以下を見てからこの記事をみるといい感じです。

イントロダクション

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

コンテンツ

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

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

プロジェクトが無い場合は作成してください。

Google Drive APIの有効化

コンソールの「APIとサービス」画面上部にある「+APIとサービスを有効化」を押下し、Google Driveを有効化します。


認証情報の作成

Google Drive APIの有効化が完了したら、コンソールの「APIとサービス」画面左ペインの「認証情報」から、認証情報を作成します。
認証情報のページ上部にある「+認証情報を作成」を押下すると表示されるドロップダウンメニューより、「サービスアカウント」を選択します。

サービスアカウント名を入力して作成してください。
なお、サービスアカウント名の下にあるサービスアカウントIDは大切な情報なので漏洩しないよう管理してください。

サービスアカウントの確認

サービスアカウントの作成が完了したら、コンソールの「IAM と 管理」画面左ペイン「サービスアカウント」から、作成したサービスアカウントの詳細を参照します。

サービスアカウント秘密鍵の作成

サービスアカウントの詳細から、「編集」そして「+キーを作成」ボタンを押下し、秘密鍵をJSON形式で作成、JSONファイルをダウンロードします。

Google Driveにフォルダを作成し、サービスアカウントと共有する

Google Driveにファイルアップロード用のフォルダを作成します。ここではsample-driveというフォルダ名としました。そしてそのフォルダを先ほど作成したサービスアカウント(メールアドレス)と共有します。

このフォルダのURLの後ろの文字列は、後でプログラムから利用します。(下のキャプチャの隠してる部分)

さてこれで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 .  

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

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

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

  :  
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の後ろ側の文字列に置き換えてください。

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

# 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と違ってそのまま絵として見れて嬉しいですね。

あとがき

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, だいすきだよ!

技術ブログをはじめよう Qrunch(クランチ)は、プログラマの技術アプトプットに特化したブログサービスです
駆け出しエンジニアからエキスパートまで全ての方々のアウトプットを歓迎しております!
or 外部アカウントで 登録 / ログイン する
クランチについてもっと詳しく

この記事が掲載されているブログ

@w2or3w の技術ブログ

よく一緒に読まれる記事

0件のコメント

ブログ開設 or ログイン してコメントを送ってみよう