Google Cloud Functionsを使ってGCSに新規アップロードされたファイルを即座に取得する

# Google Cloud Functionsとは
- HTTPリクエスト(POST，PUT, GET, DELETE, OPTIONS)
- GCS の状態変更
- Cloud Pub / Sub 

をトリガーにして、事前に作成しておいたスクリプトを実行できます。
（ちなみにFirebaseにも対応した[Cloud Functions for Firebase](https://firebase.google.com/docs/functions/)では、上記に加え以下のものもトリガー可能です。データベースへのwrite、所持ドメイン内の任意パスへのアクセス、ユーザの作成・削除、アナリティクスのイベント、モバイルアプリのクラッシュ）

# 環境構築
- Cloud Functionsのインストール
    - [ここ](https://cloud.google.com/functions/docs/quickstart?hl=ja)を参考に。
    - gcloudからbetaのcomponents をinstall<br>```gcloud components update beta```
- Emulatorインストール
    - [ここ](https://github.com/GoogleCloudPlatform/cloud-functions-emulator)を参考に。
    - nodeはnodebrewとかで入れたほうがいいかも。
    - Emulatorのインストールが完了したら、consoleで```which functions```できることを確認。
- 注意
    - 2017/12現在、Google Cloud Functionsはnode v6.11.5しか対応していないので注意。
    - **2018/8現在、Google Cloud Functionsはnode v8に対応しました。v8では関数の引数が異なるのでご注意ください。** https://cloud.google.com/functions/docs/writing/background#functions_background_parameters-node8

# 簡単なサンプルをローカルで動かす
- HTTP Trriger
    - 以下のようなjsを用意し、コマンドを実行

```helloWorld.js
exports.helloWorld = (req, res) => res.send("Hello, World!");
```

```shell
functions start
functions deploy helloWorld --trigger-http
functions call helloWorld
```

- EmulatorでホストされているFunctionsを確認

```shell
functions list
```

- GCS Trriger
    - 以下のようなjsを用意し、コマンドを実行

```index.js
exports.index = function (event, callback) {
    const file = event.data;
    if (file.resourceState === 'not_exists') {
        console.log('File ' + file.name + ' not_exists.');
    } else {
        if (file.metageneration === '1') {
            // この判定を入れることで、ファイルの新規作成なのかそれ以外(変名、削除)なのかの
            // 判別が可能です。
            console.log('File + ' + file.name + ' created.');
            // ファイルが作成されたらやりたい処理 //
        } else {
            console.log('File + ' + file.name + ' metadata updated.');
        }
    }
    callback();
};
```

```shell
functions start
functions deploy index --trigger-bucket=my-bucket-project.appspot.com
functions call index --data='{"name":"upload.zip", "bucket":"my-bucket-project.appspot.com", "metageneration": "1"}'
```

ちなみに、GCSのBucket内にディレクトリ構造を持たせている場合、
以下のようにして指定すれば再現可能です。

```shell
functions call index --data='{"name":"functions_files/upload.zip", "bucket":"my-bucket-project.appspot.com", "metageneration": "1"}'
```

なお、Deployのtargetは`index.js`内でexportsしているmodule名称で指定します。
`index.js`内に複数のexportしても、それぞれDeployすれば問題ありません。
逆に`index.js`以外の名称のjsを読み込むことはできなさそうです。

実行結果のログは ```functions logs read``` で確認できます。

# GoogleCloudStrage, CloudFunctions, サーバーサイドAPIの連携
# まずはStorageの準備
GCSに適当なバケットを作成し、そこに適当なファイルを配置。
以下はサンプル。

- GCP Project
    - sample-project
- バケット
    - artifacts.sample-project.appspot.com
- 階層
    - functions_files /
        - test.zip

### Cloud Functionsでhookされるjs
- このjsでは、バックエンドサーバーへPOSTリクエストを行います。
- パラメータとして、ファイル名とバケット名を渡します。

```index.js
var request = require('request');

exports.index = function (event, callback) {
    const file = event.data;

    if (file.resourceState === 'not_exists') {
        console.log('File ' + file.name + ' not_exists.');
        callback();
        return
    } else {
        if (file.metageneration === '1') {
            var data = {
                'file_name': file.name,
                'bucket_name': file.bucket
            };

            var options = {
                url: 'http://localhost:8000',
                method: 'POST',
                headers: {'Content-Type':'application/json'},
                json: data
             };

            request(options, function(err, response, body) {
                if (response && response.statusCode === 200 && body) {
                    console.log('backend request success.');
                } else {
                    console.log('backend error.');
                }
                callback();
            });
        } else {
            console.log('File + ' + file.name + ' metadata updated.');
            callback();
            return
        }
    }
};
```

### バックエンドAPI
- バックエンドAPIでは、`localhost:8000` にてPythonサーバーを起動して、パラメータで指定されたファイルをGCSから取得してローカルサーバーに保存します。

```sample.py
# -*- coding: utf-8 -*-
import json
from bottle import request, HTTPResponse
from google.cloud import storage

file_types = {
    'zip' : 'application/zip'
}

class Sample():
    @classmethod
    def main(cls):
        try:
            file_name = request.json.get('file_name')
            bucket_name = request.json.get('bucket_name')

            if '.' in file_name:
                file_ext = file_name.split('.')[len(file_name.split('.')) - 1]
            else:
                file_ext = ''

            result, validated_result = get_validated_file(file_name, bucket_name, file_ext)

            if result:
                body = { 'result': 'ok' }
            else:
                body = { 'result': validated_result }

            # レスポンスを返却する
            response = HTTPResponse(status=200, body=json.dumps(body))
            response.set_header('Content-Type', 'application/json')
            return response



def get_file(file_name, bucket_name):
    """GCSからファイルを取得
    """
    storage_client = storage.Client(project='sample-project')
    bucket = storage_client.get_bucket(bucket_name)
    return bucket.get_blob(file_name)


def is_accept_type(file, file_ext):
    """想定ファイルかどうかチェック
    """
    if file_ext not in file_types:
        return False
    else:
        return file.content_type == file_types[file_ext]


def get_validated_file(file_name, bucket_name, file_ext):
    """対象ファイルのバリデーション
    :return: バリデーションの成否, バリデーション結果(成功ならファイルパス)
    """
    file = get_file(file_name, bucket_name)

    if not file or not file.exists():
        return False, 'is not exist'

    if not is_accept_type(file, file_ext):
        return False, 'content_type'

    outfile_name = '/hoge/hoge/text.zip'
    # GCSのBlobオブジェクトをローカルのファイルオブジェクトとして保存
    file.download_to_filename(outfile_name)

    return True, outfile_name
```

### CloudFunctions Emulatorで試す
```command
functions deploy index --trigger-bucket=artifacts.sample-project.appspot.com
functions call index --data='{"name":"functions_files/test.zip", "bucket":"artifacts.sample-project.appspot.com", "metageneration": "1"}'
```


あらかじめ `localhost:8000` でPythonサーバーを起動しておき、上記コマンドでfunctionsをローカルから叩いてPythonへリクエストが通れば成功。

Python側で指定ファイルがGCSに存在するかなどのチェックを行っているため、無い場合はちゃんとエラーが返ります。


これで動作確認が出来れば、GCSにファイル配置をトリガーにしてマスタインポートなどの処理が可能となります。

# GCP上にCloudFunctionsをデプロイする
- index.js があるディレクトリで以下のコマンドを実行すればDeploy可能です。

```shell
gcloud beta functions deploy index --stage-bucket test-stage-bucket --trigger-bucket=artifacts.sample-project.appspot.com
```

※GCFはバケットに展開されるため、stage-bucket はあらかじめ作成しておくこと。

上記にてDeploy完了したら、以後GCSをトリガーにしてFunctionsが実行されます。