Flutter/Dart で S3 へのダイレクトアップロードを実装する
ダイレクトアップロードのクライアント側実装では次の2つのステップが必要です。
- ダイレクトアップロード用のURLを取得する
- S3にダイレクトアップロードする
Flutter /Dart でどう書けば良いのか?私が試した方法を紹介します。
この記事ではサーバ側の実装については述べません。
1. ダイレクトアップロード用のURLを取得する
ダイレクトアップロードしたいファイルのハッシュ値やサイズなどをバックエンドに送ります。 バックエンドはたとえば ActiveStorage を使ったり、自前で作っても良いですね。
ファイルのハッシュ値は crypto で生成して base64
でエンコードして作れます。
Content-Type は mime を使いました。
import 'package:mime/mime.dart' show lookupMimeType; import 'package:path/path.dart' show basename; import 'upload_helper.dart' show calculateMD5CheckSum, getFilzeSize; # 省略 // たとえば ImagePicker などで選択されたファイルパスを受け取る onSubmit: (String filePath) async { final filename = basename(path); final contentMd5 = await calculateMD5CheckSum(path); final contentType = lookupMimeType(filename); final contentLength = await getFilzeSize(path); // サーバにダイレクトアップロード用URL発行に必要な情報を送る // runUploadSignMutation は自前の GraphQL Mutation runUploadSignMutation({ 'contentLength': contentLength, 'contentType': contentType, 'contentMd5': contentMd5, 'filename': filename });
// upload_helper.dart import 'dart:io'; import 'package:crypto/crypto.dart' as crypto; import 'dart:convert'; Future<String> calculateMD5CheckSum(String filePath) async { var ret = ''; var file = File(filePath); if (await file.exists()) { try { var md5 = crypto.md5; var hash = await md5.bind(file.openRead()).first; ret = base64.encode(hash.bytes); } catch (exception) { print('Unable to evaluate the MD5 sum :$exception'); return null; } } else { print('`$filePath` does not exits so unable to evaluate its MD5 sum.'); return null; } return ret; } Future<int> getFilzeSize(String filePath) async { int ret; final file = File(filePath); if (await file.exists()) { try { ret = await file.length(); } catch (exception) { print('Unable to get the length :$exception'); return null; } } else { print('`$filePath` does not exits so unable to get its length.'); return null; } return ret; }
なお、今回はこんなGraphQLを想定しています。 ヘッダー署名に必要な情報を送ると、S3署名付きURLと署名付きのHTTPヘッダーを返すという仕様です。
mutation directUploadSignRequest($contentLength: Int!, $contentType: String!, $contentMd5: String!, $filename: String!) { directUploadSignRequest( input: { contentLength: $contentLength, contentType: $contentType, contentMd5: $contentMd5, filename: $filename } ) { url headersJson signedId } }
2. S3にダイレクトアップロードする
バックエンドでS3アップロードに必要な
- 署名付きURL
- HTTPヘッダー
を得たら、その情報を使ってS3にPUTできます。
ここではこんな応答を受け取ったとします。 アップロードに必要な URL 、ヘッダーのJSON、それにアップロードしたことをサーバに伝えるための識別子ですね。
{ "directUploadSignRequest": { "url": "https://xxxxxxxxxxxxxx.s3.ap-northeast-1.amazonaws.com/a3hwxj8wiyzdb503b4ley2d7smuj?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=TESTKEYID%2F20210123%2Fap-northeast-1%2Fs3%2Faws4_request&X-Amz-Date=20210123T165510Z&X-Amz-Expires=604800&X-Amz-SignedHeaders=content-length%3Bcontent-md5%3Bcontent-type%3Bhost&X-Amz-Signature=c9a99a06e64bf47aa5a48d458f7af39c13f7230b8f1f71ebce65ba15db08216a", "headersJson": "{\"Content-Type\":\"image/jpeg\",\"Content-MD5\":\"716+n5uVJiFVKFsqCh2qHw==\",\"Content-Disposition\":\"inline; filename=\\\"test.jpg\\\"; filename*=UTF-8''test.jpg\"}", "signedId": "eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaHBDZz09IiwiZXhwIjpudWxsLCJwdXIiOiJibG9iX2lkIn19--b673084e015e2e33b3dfca2631afb1b59cb9d47a" }
PUTには dio が便利です。
import 'package:dio/dio.dart'; import 'upload_helper.dart' show getFilzeSize; // 省略 String url = resultData['directUploadSignRequest']['url']; Map<String, dynamic> headers = jsonDecode(resultData['directUploadSignRequest']['headersJson']); // Content-Length が必要 // HTTP Client が自動で付けないので headers['Content-Length'] = await getFilzeSize(filePath); try { Dio dio = new Dio(); await dio.put(url, data: File(filePath).openRead(), ptions: Options(headers: headers)); sendSignedIdToServer(resultData['directUploadSignRequest']['blob']['signedId']); // 終わったのでサーバに伝えて紐付けなど適当に on DioError catch (error) { debugPrint("statusCode: ${error.response.statusCode}"); debugPrint("request headers: ${jsonEncode(error.request.headers)}"); debugPrint("request queryParameters: ${jsonEncode(error.request.queryParameters)}"); debugPrint("request path: ${jsonEncode(error.request.path)}"); debugPrint("response body: ${error.response.data}"); }