「GAE/Py」によるGoogleCloudStorage(GCS)の利用


記載しているのは、「GAE/Python」にて動作しているアプリにおける、クライアントからのファイルアップロードを想定しています。
サーバサイドで作成したデータ・ファイルの読み/書き(データバックアップ等)をするものではないです。
クライアントがファイルを添付したり、ダウンロードする機能を追加することを目的としています。

自分が後で理解できるレベルでの記載ですので、細かな説明は端折っています。
また、間違えた解釈もあるかもしれないので、その際はご指摘頂けると助かります。

[参考：以下の記事参考]
- [sinmetalさんの記事](http://qiita.com/sinmetal/items/7d2617001494cd204255)
※ sinmetalさんの記事では「GAE/j」での記載だったのでPython版を書きたかった。

## 動作の概要
### アップロード

1. アップロードを行う際に使用するURLの取得APIが叩かれる(URLを作成するAPI）
2. 「1.」のレスポンスにてクライアントにURLを通知
3. クライアント側より、「2.」で通知されたURLに対しファイルを含めたリクエストがPOSTされる
4. アップロードが完了した際に、GCS(GoogleCloudStorage：以下GCS)よりアップロードされたファイルの情報をふくめたリクエスト(POST)がGAE(※)に通知される。<br>
※ 「1.」にてURLを作成する際に完了通知の受取先を指定している
5. 必要であれば、アップロードされたファイル情報を保存するための付加情報モデルを作成し、DataStoreに登録しておく。

### ダウンロード

1. クライアントよりダウンロードリクエストを受け取る
2. レスポンスでファイルを転送する

## 実装するもの

1. アップロードURLの取得API <br>
ファイルをアップロードする際のPOST先URLの取得
- アップロード後のリクエストハンドラ <br>
アップロードされたファイルの情報取得及び登録
- ダウンロードハンドラ
- ハンドラへの紐付け

### 1.アップロードURLの取得API

GoogleCloudStrageへのアップロード用のURLの発行には、blobstoreの扱いと同様に「create_upload_url」を使用するが、オプションとして「gs_bucket_name = 」を付与することで、GCS上にて作成したバケットを指定することが可能となる。

下記実装はendpoint形式での実装（筆者はiOS向けのAPIを出力する想定）。
GCSを利用する箇所のみ抽出。endpointについての記載は省略する。

```Python:ep_upload.py
# エンドポイント用の独自クラス
import GetUploadUrlResponse

from google.appengine.ext import blobstore
・
・
class GetUploadUrl():
	def __init__(self, request):
        self.response = None

	def done(self):
		# create_upload_url にてGCSのバケット名を「gs_bucket_name」に指定する
		url = blobstore.create_upload_url('/uploaded',gs_bucket_name = 'bucket_name/path/')
		return self.response = GetUploadUrlResponse(ret = 0,
												   	url = url)
・
・
```

### 2.アップロード後のリクエストハンドラ
発行されたURLへのファイルのポストが完了し、GCSへのアップロードが完了した場合、「create_upload_url」にて指定したアドレスに対し、ファイル情報を含めリクエストが通知される。
通知を受け取り、アップロードされたファイルを管理するModel等にてDataStoreに登録しておく。

アップロードされたファイルのリクエストからの取得は、blobstore_handlers.BlobstoreUploadHandlerより継承した、下記の2種類の取得方法がある。

- get_uploads
- get_file_infos

どちらもアップロードしたファイルの情報という意味では同じだが、扱っている形式が全く違うので注意。

get_uploadを使用した場合 : BlobInfo
get_file_infosを使用した場合 : FileInfo

筆者の実装（環境?）では「get_file_infos」を使用した場合、ダウンロードが不可能(エラーが発生)となってしまうため、
「get_upload」を使用した。
※ 他にも削除処理をした場合に、GCSからは削除されるが、BlobStoreから削除されなかった。

```Python:upload.py
import webapp
from google.appengine.ext import blobstore
from google.appengine.ext.webapp import blobstore_handlers
from models import GsFile
・
・
class UploadHandler(blobstore_handlers.BlobstoreUploadHandler,webapp.RequestHandler):
	def post(self):
		blob_infos = self.get_uploads('file')
       
		if not isinstance(blob_infos, list):
			blob_infos = [blob_infos]

		for blob_info in blob_infos:	
			GsFile.create(	filename		= blob_info.filename
							content_type	= blob_info.content_type
							size			= blob_info.size
							key				= str(blob_info.key()))
							
		self.response.write('0')
・
・
```

### 3.ダウンロードハンドラ

クライアントはアップロード完了時に登録された管理用Model(今回の例だと GsFile)を参照し、
key情報にてダウンロード要求を行う。
※ 本記事ではリクエストURL内の文字列として渡されている想定(筆者側ではPOSTのデータとして受け取る予定)

```Python:upload.py
・
・
class ContentDownload(blobstore_handlers.BlobstoreDownloadHandler):
	def get(self, key):
		key = str(urllib.unquote(key))
		blob_info = blobstore.BlobInfo.get(key)
		self.send_blob(blob_info)
・
・
```

ブラウザからのリンクによりダウンロードをさせる場合、このまま呼び出しを行ってしまうとファイル名が取得できないため、URL部分の名称での保存となってしまう。
もし、ブラウザからの直接ダウンロードを実装する場合は、「send_blob」のオプション(save_as)を設定する。

	self.send_blob(blob_info, save_as=True)

※ 文字列の場合はその文字列をファイル名とし、Trueの場合はblob_infoにファイル名があればそれを使用する。

### 4.ハンドラへの紐付け

各リクエストに対応するclassを設定(ダウンロードはURL上にkeyを入力できるようにしている)

```Python:upload.py
・
・
APPLICATION = webapp.WSGIApplication([('/upload', UploadHandler),
                                      ('/download/([^/]+)?', DownloadHandler)])
```

```Python:app.yaml
- url: /(upload|download/.*)
  script: upload.APPLICATION
```