UnityからAmazon S3にファイルをアップロードし、公開状態にする


#実現する事
Unityで以下を行う。

0. ファイルをAmazon S3にアップロードし、公開状態にする
0. 公開URLを作る

#確認環境
- MacBook Pro（OS X 10.11.5）
- iPhone 6s
- Unity v5.4.0f3
- Xcode v7.3.1

#関連ドキュメント
この記事は以下のAWS公式ドキュメントの手順に沿って進めているが、同ドキュメントは若干説明不足だったり例が間違っている箇所があるので、そこは適宜補っている。

[Set Up the AWS Mobile SDK for Unity — Unity Developer Guide](http://docs.aws.amazon.com/mobile/sdkforunity/developerguide/setup-unity.html)

[Amazon Simple Storage Serivce (S3) — Unity Developer Guide](http://docs.aws.amazon.com/mobile/sdkforunity/developerguide/s3.html)

#(1) AWS Mobile SDK for UnityをダウンロードしてUnityにimportする
0. [AWS Mobile SDK for Unity](http://sdk-for-net.amazonwebservices.com/latest/aws-sdk-unity.zip)をダウンロードする。
※このダウンロードリンクは後述の上記ドキュメントから直接リンクされているものなので、リンク切れの場合はドキュメントを確認する

0. zipを展開すると、利用できる各サービスの名前が入った.unitypackageファイルが並ぶ
0. Unityを起動し、S3を扱いたいプロジェクトを開く。この記事では新規プロジェクトを作成した
0. 展開した中の **AWSSDK.S3.<バージョン番号>.unitypackage** をimportする。当該ファイルをFinder上でダブルクリック、もしくはUnityのAssetsメニュー -> Import Package -> Cutom Package で当該ファイルを選択する
0. 全てチェックした状態でimport実行
※Examplesは本番では不要

#(2) SDKの設定ファイルを用意する
SDKの設定、及びビルド周りで必要なlink.xmlの設定を行う。

##awsconfig.xml
SDK全体に関わる設定は awsconfig.xml に記載する。この設定はプログラムで行う事も出来るので必須ではない（ファイルが無くて全てプログラム制御でも動作する）。
公式ドキュメントにあるサンプルのXMLは文法エラーがあり、それを直すだけだとやはりエラーになる（少なくとも手元の環境では）。以下は、修正の上で動作確認ができたもの。なおawsconfig.xmlは Resources ディレクトリ以下に置かないと読み込まれない。

```XML:awsconfig.xml
<?xml version="1.0" encoding="utf-8"?>
<aws region="ap-northeast-1" correctForClockSkew="true">
    <logging logTo="UnityLogger"
             logResponses="Always"
             logMetrics="true"
             logMetricsFormat="JSON" />
    <s3 useSignatureVersion4="true" />
</aws>
```

※ **correctForClockSkew="true"** が無いと初期化時にNullReferenceExceptionが起きる。これを追加するのは、 https://github.com/aws/aws-sdk-net/issues/332 を参考にした。

##link.xml
ビルド時のストリッピング回避のため、link.xml を設定する。なおlink.xml自体の説明は [Unity - マニュアル: ビルドした iOS プレイヤーのサイズ最適化](http://docs.unity3d.com/ja/current/Manual/iphone-playerSizeOptimization.html) などを参照。このファイルの置き場は Assets 以下であればどこでも良い（今回はAssets直下に置いた）。

```XML:link.xml
<linker>
<assembly fullname="UnityEngine">
    <type fullname="UnityEngine.Experimental.Networking.UnityWebRequest" preserve="all" />
    <type fullname="UnityEngine.Experimental.Networking.UploadHandlerRaw" preserve="all" />
    <type fullname="UnityEngine.Experimental.Networking.UploadHandler" preserve="all" />
    <type fullname="UnityEngine.Experimental.Networking.DownloadHandler" preserve="all" />
    <type fullname="UnityEngine.Experimental.Networking.DownloadHandlerBuffer" preserve="all" />
</assembly>
<assembly fullname="mscorlib">
    <namespace fullname="System.Security.Cryptography" preserve="all"/>
</assembly>
<assembly fullname="System">
    <namespace fullname="System.Security.Cryptography" preserve="all"/>
</assembly>
<assembly fullname="AWSSDK.Core" preserve="all"/>
<assembly fullname="AWSSDK.CognitoIdentity" preserve="all"/>
<assembly fullname="AWSSDK.SecurityToken" preserve="all"/>
</linker>
```

#(3) Identity Pool IDを取得する
0. [Cognito Console](https://console.aws.amazon.com/cognito/home)を開く
0. **Manage Federated Identities** をクリック
0. **Create new identity pool** をクリック
0. 以下を入力して **Create Pool** をクリック
> Identity pool name : 任意の名前
> Enable access to unauthenticated identities : チェックする

0. **許可** をクリック。なお以下のスクショは詳細を表示した状態
0. Platform 選択で **Unity** を選ぶ。その状態で Get AWS Credentials 欄に表示されているものを、後でUnityのC#スクリプト内で利用する

```c#
CognitoAWSCredentials credentials = new CognitoAWSCredentials (
    "ap-northeast-1:XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX", // Identity Pool ID
    RegionEndpoint.APNortheast1 // Region
);
```

#(4) S3の設定を行う
## バケットの作成
※既に利用するバケットがある場合にはこの手順は不要。

0. [S3 console](https://console.aws.amazon.com/s3/)を開く
0. **バケットを作成** をクリック
> バケット名 : 他と被らない任意の名前（この記事では unity-upload）
> リージョン : 任意（この記事では Tokyo）

0. **作成**をクリック

## IAMの設定
0. [IAM Console](https://console.aws.amazon.com/iam/home) を開く
0. 左のメニューから **ロール** をクリック
0. **Cognito_＜Identity pool nameに入れた名前＞Unauth_Role** をクリック（似た名前の xxxAuth_Role もあるので注意）
0. **ロールポリシーの作成** をクリック
0. Policy Generator の枠内にある **選択** をクリック
> 効果 : 許可
> AWSサービス : Amazon S3
> アクション : すべてのアクション をチェック
> 　　※ポリシー次第。後述の「おまけ」も参照
> Amazon リソースネーム(ARN) : arn:aws:s3:::<バケット名>/*
> 　　※例 arn:aws:s3:::unity-upload/*

0. **ステートメントを追加** をクリック
0. **次のステップ** をクリック
0. **ポリシーの適用** をクリック
※ポリシー次第ではここで書き換える。後述の「おまけ」も参照

#(5) C#スクリプトでの実装
##スクリプト追加
0. S3を扱いたいSceneに、空のGame Objectを作成する
0. このGame ObjectのInspectorから、Add Componentで任意の名前をつけたC#スクリプトを追加する

##実装
###初期化処理
Start()またはAwake()に以下の処理を追加する

```c#
// using Amazon;
UnityInitializer.AttachToGameObject(this.gameObject);

// （2016/09/27追記） DeleteObjectなど幾つかのリクエストは、UnityWebRequestを指定しないとエラーになる。
// 詳しくは本記事末尾に記載した。
AWSConfigs.HttpClient = AWSConfigs.HttpClientOption.UnityWebRequest;

// using Amazon.CognitoIdentity;
CognitoAWSCredentials credentials = new CognitoAWSCredentials (
    "ap-northeast-1:XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX",    RegionEndpoint.APNortheast1
);
```

credentialsの部分は、[Identity Pool IDを取得する](#(3) Identity Pool IDを取得する)で作成したもの。
ここまでは、S3以外のAWSサービスを利用する場合と共通となる。

###S3クライアント作成処理

```c#
AmazonS3Client S3Client = new AmazonS3Client (credentials, RegionEndpoint.GetBySystemName(RegionEndpoint.APNortheast1.SystemName));
```

RegionEndpointはawsconfig.xmlで指定している場合は不要。両方指定した場合はコードで指定したものが優先される。
AmazonS3ClientのAPIドキュメントは [AmazonS3Client Class | AWS SDK for .NET V3](http://docs.aws.amazon.com/sdkfornet/v3/apidocs/items/S3/TS3S3Client.html) 参照。

###S3へのファイルアップロード処理
今回は例として、Assets/StreamingAssets ディレクトリに test_image.jpg を置き、それををアップロードする。
アップロードの際にACL指定で公開（PublicRead）を指定しているので、アップロード完了のタイミングで外部からアクセスできるようになる。

```c#
string S3BucketName = "unity-upload";
string fileName = "test_image.jpg";

var stream = new FileStream(Application.streamingAssetsPath +
	Path.DirectorySeparatorChar + fileName,
	FileMode.Open, FileAccess.Read, FileShare.Read);

var request = new PostObjectRequest()
{
	Bucket = S3BucketName,
	Key = fileName,
	InputStream = stream,
	CannedACL = S3CannedACL.PublicRead // 公開状態
};

S3Client.PostObjectAsync(request, (responseObj) =>
	{
		if (responseObj.Exception == null)
		{
			// success
		}
		else
		{
			// fail
		}
	});
```

#(6) 公開URLを作る
本来であればSDKから取得したいが、AmazonS3Clientにそれらしきメソッドが見つからなかった。レスポンスにも入っていないようだったので、単純に文字列で作る。
リージョンを考えない最も汎用的と思われるS3の公開URLは
> https://<バケット名>.s3.amazonaws.com/<ファイル名>

なので、今回の例は以下となる。

> https://unity-upload.s3.amazonaws.com/test_image.jpg

##おまけ：IAMのロールポリシー設定
SDKの公式ドキュメントで例示されているポリシー設定のARN arn:aws:s3:::examplebucket/* はちょっと罠があって、これだとexamplebucket内のPutObject/GetObjectなどはできますが、examplebucket内のオブジェクト一覧取得（ListBucket）はAccess Deniedとなってしまいます。

今回実現したい事はPutObject/PutObjectAclが出来れば良いのでこれでも問題無いのですが、最初試した時に同ドキュメントのListBucket項でいきなりエラーになってしまいました。

結果としては、[AWS リソースの管理に関するポリシーの例 - AWS Identity and Access Management](http://docs.aws.amazon.com/ja_jp/IAM/latest/UserGuide/access_policies_examples.html)で説明されているポリシー設定を真似て解決しました。

- リスト取得はバケット操作なので、バケット自体（unity-upload）を指定する
- 各オブジェクトの操作は、バケットの中（unity-upload/*）を指定する
- PutObjectAcl は公開状態にするために必要

なお arn:aws:s3:::* と指定すればどちらも含む事ができますが、見ての通りunity-upload以外のバケットも対象になってしまうので、運用方法によってはNGではないかと思います。

```IAMポリシー例
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": [
                "s3:ListBucket"
            ],
            "Resource": [
                "arn:aws:s3:::unity-upload"
            ]
        },
        {
            "Effect": "Allow",
            "Action": [
                "s3:PutObject",
                "s3:PutObjectAcl"
            ],
            "Resource": [
                "arn:aws:s3:::unity-upload/*"
            ]
        }
    ]
}
```

#追記
##2016/09/27 UnityWebRequestの指定について
[初期化処理](#初期化処理)に、HttpクライアントとしてUnityWebRequestを利用する指定を追加しました。
サンプルで挙げているPostObjectやListObjectsなどは特にこれが無くても問題無いのですが、DeleteObjectなど幾つかのリクエストは、このUnityWebRequest指定が無いと以下のようなエラーとなります。

> InvalidOperationException: DeleteObject is only allowed with AWSConfigs.HttpClientOption.UnityWebRequest API option
Amazon.S3.AmazonS3Client.DeleteObjectAsync (Amazon.S3.Model.DeleteObjectRequest request, Amazon.Runtime.AmazonServiceCallback`2 callback, Amazon.Runtime.AsyncOptions options)
> （以下略）

公式の説明は以下にあります。
[Unity V3 support in the AWS SDK for .NET is out of preview - AWS Developer Blog - Mobile](https://mobile.awsblog.com/post/Tx3MK84RMIUSOMP/Unity-V3-support-in-the-AWS-SDK-for-NET-is-out-of-preview)