FlutterでWeb,Android,iOSの3プラットフォームから同じコードでS3に写真をアップロードする

##概要
6月にWeb向けのImagePickerが出ていたので、ネイティブアプリとブラウザアプリを同じソースコードでビルドして、どのデバイスでもファイルアップロード出来るのかというのを試しました。
AndroidとiOSに加えてWebでもPickerが同じ処理で使えるようになったのは結構画期的だなと思ってのメモです。内容はクライアント側に重点を置いています。

### 1. パッケージをインポートする。
pubspec.yamlにimage_pickerとimage_picker_for_web、amazon_cognito_identity_dart_2を追加する。
私が試したバージョンは以下です。

```
image_picker: ^0.6.7
image_picker_for_web: ^0.1.0+1
amazon_cognito_identity_dart_2: ^0.1.14
```

### 2. 画像取得処理を書く。thenを使用してますが、awaitでも問題ありません。

```
import 'package:image_picker/image_picker.dart';
~
~
var picker = ImagePicker();
picker.getImage(source: ImageSource.gallery)
    .then((PickedFile value) {
      // ここから必要に応じて後続の処理を呼び出す
    });
```

## 3. アップロード処理を書く。
こちら（ https://pub.dev/packages/amazon_cognito_identity_dart_2#signing-requests ）のページのFor S3 Uploadsをベースにしますが、PickedFileはlengthが取れないので少し改造します。PolicyやSigV4はそのままで`main()`だけ変更します。
私の場合は`uploadImage`として以下のようにしました。

```
  Future<String> uploadImage(PickedFile file) async {
    final _credentials = CognitoCredentials(ID_POOL_ID, _userPool);
    final _cognitoUser = CognitoUser(_username, _userPool, clientSecret: _clientSecret);
    final authDetails =
    AuthenticationDetails(username: _username, password: _passwd);
    CognitoUserSession _session;
    try {
      _session = await _cognitoUser.authenticateUser(authDetails);
    } catch (e) {
      print(e);
      return null;
    }
    await _credentials.getAwsCredentials(_session.getIdToken().getJwtToken());
    final uri = Uri.parse(_s3Endpoint);
    final req = http.MultipartRequest("POST", uri);
    final multipartFile = http.MultipartFile.fromBytes('file', await file.readAsBytes(),contentType: MediaType('application', 'octet-stream'));
    logger.d('Policy.fromS3PresignedPost');
    String filename = 'test.jpg';
    final policy = Policy.fromS3PresignedPost(
        filename,
        _bucketName,
        150,
        _credentials.accessKeyId,
        10000000,
        _credentials.sessionToken,
        region: REGION);
    final key = SigV4.calculateSigningKey(
        _credentials.secretAccessKey, policy.datetime, REGION, 's3');
    final signature = SigV4.calculateSignature(key, policy.encode());
    req.files.add(multipartFile);
    req.fields['key'] = policy.key;
    req.fields['acl'] = 'public-read';
    req.fields['X-Amz-Credential'] = policy.credential;
    req.fields['X-Amz-Algorithm'] = 'AWS4-HMAC-SHA256';
    req.fields['X-Amz-Date'] = policy.datetime;
    req.fields['Policy'] = policy.encode();
    req.fields['X-Amz-Signature'] = signature;
    req.fields['x-amz-security-token'] = _credentials.sessionToken;
    try {
      final res = await req.send();
      logger.d('res:${res.toString()}');
      await for (var value in res.stream.transform(utf8.decoder)) {
        logger.d(value);
      }
      return filename;
    } catch (e, stacktrace) {
      logger.e(e);
      logger.e(stacktrace);
    }
    return null;
  }
}
```

ID_POOL_IDや_userPoolは各環境に合わせて定義してください。
fromS3PresignedPostへ渡すパラメータmaxFileSizeは一旦10Mとしているので必要に応じて変更してください。
リンク先と主に異なるのは、`final file = File(path.join('/path/to/my/folder', 'square-cinnamon.jpg'));`が`PickedFile`に置き換わっていて、
streamからMultipartFileを生成する処理

```
final stream = http.ByteStream(DelegatingStream.typed(file.openRead()));
final multipartFile = http.MultipartFile('file', stream, length,
      filename: path.basename(file.path));
```

が
bytesからMultipartFileを生成する処理

```
final multipartFile = http.MultipartFile.fromBytes('file', await file.readAsBytes(),contentType: MediaType('application', 'octet-stream'));
```

になっております。目的としてはlengthが取れないからbytesでのリクエストにしました。
クライアント側はこんな感じです。  
他に  

* Cognitoのユーザプール作成
* IDプール作成
* IAM追加
* S3のCORS設定
* iOSのNSPhotoLibraryUsageDescription

が必要です。特にS3のCORS設定はブラウザからのみアップロードできない問題に直面するので、ハマりポイントとして記載します。（その他は調べれば出てくると思うので他の方の記事をご参照ください。)

## 4. サーバ側番外編　S3でCORSの設定
こちら（ https://docs.aws.amazon.com/ja_jp/sdk-for-javascript/v2/developer-guide/cors.html ）のCORS設定例を参考にS3のバケット>アクセス権限>CORSの設定で設定してください。`AllowedOrigin`だけ各環境に合わせて変える必要があります。
ローカル環境からの試しであればアスタリスク`*`で設定すると早いです。

```
<?xml version="1.0" encoding="UTF-8"?>
<CORSConfiguration xmlns="http://s3.amazonaws.com/doc/2006-03-01/">
  <CORSRule>
    <AllowedOrigin>https://example.org</AllowedOrigin>
    <AllowedMethod>HEAD</AllowedMethod>
    <AllowedMethod>GET</AllowedMethod>
    <AllowedMethod>PUT</AllowedMethod>
    <AllowedMethod>POST</AllowedMethod>
    <AllowedMethod>DELETE</AllowedMethod>
    <AllowedHeader>*</AllowedHeader>
    <ExposeHeader>ETag</ExposeHeader>
    <ExposeHeader>x-amz-meta-custom-header</ExposeHeader>
  </CORSRule>
</CORSConfiguration>
```