HTMLからバイナリファイルをアップロードしたり、サーバからバイナリデータをダウンロードしてみます。
クライアント側として、HTMLのSubmitボタンを利用する方法と、Javascriptから送信する方法でやってみます。
サーバ側としては、Node.jsのSwagger-node(中身はExpress)のサーバの場合と、AWSのAPI Gateway+Lambdaの場合を取り上げます。
毎度の通り、ソースコードは以下に上げておきました。
poruruba/fetch_laboratory
https://github.com/poruruba/fetch_laboratory
#バイナリファイルの送信
それでは、送信するクライアント側のWebページから見ていきます。
まずは従来からの、HTMLのSubmitボタンで送信する場合です。
HTMLは以下のような感じになります。
<form v-bind:action="base_url + '/upload'" method="post" enctype="multipart/form-data">
<input type="file" name="upfile" accept="image/*">
<input type="text" name="param" v-model="param_value">
<input type="submit" name="submit" value="送信">
</form>
(v-bindやらv-modelやら、Vueを使っていますが、感触はつかめると思います。)
methodとenctypeは、POST・multipart/form-dataを指定します。それにより、バイナリファイルだけでなく、他のテキストパラメータ(上記では、paramとsubmit)も一緒に送ることができます。
「ファイルを選択」ボタンがありますので、それを押下することでPCにあるファイルを指定することができ、「送信」ボタンでファイルとテキストパラメータがサーバに送信されます。
これで送られるのは以下の通りです。
・upfile:指定したファイル
・param:name=paramで指定したテキスト
・submit:name=submitのvalueである"送信”
これであれば、Javascriptの力を借りずに、送信できます。が、送信後、Webページが送信後のレスポンスの内容に遷移します。
次の、Javascriptから送信する場合は、画面は遷移しません。
とりあえずHTMLです。
<div>
<input type="file" accept="image/*" v-on:change="do_change">
<input type="text" v-model="param_value">
<button v-on:click="do_upload">do_upload</button>
</div>
送信を行うJavascriptは以下の通りです。
do_upload: function(){
var param = {
upfile: this.file,
param: this.param_value,
submit: this.submit_value,
};
do_post_formdata(this.base_url + "/upload", param)
.then(json =>{
this.response = json;
});
},
do_change: function(e){
if (e.target.files.length > 0) {
this.file = e.target.files[0];
}
},
送信のトリガは、v-on:click=”do_upload”と記載してある通り、do_upload()というメソッドです。
ただ、送信するファイルはこのときにはもう触れないので、「ファイルを選択」ボタンからファイルを指定したときにファイルを保持しておく必要があります。そのトリガメソッドがdo_change()のメソッドです。
あとは、POST送信の多面い、以下の関数を呼ぶだけです。
function do_post_formdata(url, params) {
var body = Object.entries(params).reduce((l, [k, v]) => {
l.append(k, v);
return l;
}, new FormData());
return fetch(new URL(url).toString(), {
method: 'POST',
body: body,
})
.then((response) => {
if (!response.ok)
throw 'status is not 200';
return response.json();
});
}
詳細は、以下で説明していますので、割愛します。
fetchの呼び出し @Javascript & Node.js 実験室
ファイルを受け取るサーバの配置
それではさっそく、ファイルを受信するサーバを立ち上げます。
サーバとして、Swagger-nodeサーバを採用しています。
詳細は以下をご参照ください。
Swagger定義ファイルは以下のような定義になります。
/upload:
post:
x-swagger-router-controller: routing
operationId: upload
consumes:
- multipart/form-data
parameters:
- in: formData
name: upfile
type: file
responses:
200:
description: Success
schema:
$ref: "#/definitions/CommonResponse"
consumesとしてmultipart/form-dataを指定しています。
また、parametersとして、ファイルが指定されているupfileの指定を記載しています。paramやsubmitもあるのですが、それらの指定は必須ではありませんが、type=fileの指定は必須のようです。
これで、ファイルおよびパラメータを送信すると、Expressのフレームワークでおなじみのreqとresにおいて、req.bodyにパラメータが、req.filesにファイルが入ってきます。
req.body = {
param:"param_1"
submit:"送信"
}
※paramの内容は、Webページで指定したテキストです。
req.files = {
upfile: [
{
buffer: ファイルの内容(Buffer型),
encoding: “7bit”,
fieldname: “upfile”,
mimetype: “image/png”,
originalname: “test.png”,
size: 424,
}
]
}
※bufferやmimetype、originalname、sizeは、選択したファイルによって変わります。
ソースコードは、api\controllers\upload\index.jsにあるのですが、AWSのLambdaとの互換性を考慮して、api\controllers\routing.jsやfunction.jsを間に挟んでちょこちょこ処理が入っています。具体的には、event.bodyをJSONパースするとパラメータを取得し、event.filesでファイルが取得できるようにしています。
さっそく、Webページから、ファイルを選択して「送信」ボタンまたは「do_upload」ボタンを押してみます。
AWSのAPI Gateway+Lambdaに配置
今度は、このサーバをローカルではなく、AWS上に構築してみます。
ソースコードは、api\controllers\upload\index.jsをほぼそのまま使いますが、手を入れるところがあります。
API Gatewayでは、Swagger-nodeのような、multipart/form-dataを自動的にはパースしてくれませんので自前で用意する必要があります。
以下のnpmモジュールを使わせていただきました。
fransismeynard/lambda-multipart-parse
https://github.com/francismeynard/lambda-multipart-parser
これを使ってパースするユーティリティを作っておきました。
'use strict';
async function parse(parser, event) {
var body = await parser.parse(event);
if (body.files) {
event.files = {};
for (var i = 0; i < body.files.length; i++) {
var file = {};
file.buffer = body.files[i].content;
file.encoding = body.files[i].encoding;
file.fieldname = body.files[i].fieldname;
file.mimetype = body.files[i].contentType;
file.originalname = body.files[i].filename;
file.size = body.files[i].content.length;
event.files[file.fieldname] = [file];
}
delete body.files;
}
return body;
}
module.exports = {
parse,
};
まだ解析されていないmultipart-form-dataは、event.bodyに入っており、解析には先ほどのnpmモジュールであるlambda-multipart-parserを使います。
解析すると、クライアント側から受け取ったファイルとパラメータが取得されます。
そして、ファイルをevent.filesにExpressと同じフォーマットで格納しなおし、それ以外のパラメータはreturnで返すようにしています。
それでは、Lambdaを作っていきます。
例えば、関数名として「upload_labo」としてみました。Node.jsはv10を選択しました。
以上で、とりあえず空のLambdaが出来上がりました。
次に、ソースコードファイルをアップロードします。Web管理コンソールからエディットしてもよいのですが、npmモジュールであるlambda-multipart-parserをアップしないといけないのと、すでにベースとなるindex.jsとその他ユーティリティファイルが手元にあるため、ZIPに固めてアップします。
以下のフォルダに移動し、ZIP対象ファイルを集めます。いくつかユーティリティファイルも含めています。
cd api\controllers\upload
npm init -y
npm install lambda-multipart-parser
mkdir helpers
cp ....\helpers\response.js helpers
cp ....\helpers\multipart.js helpers
cp ....\helpers\binresponse.js helpers
そして、upload配下のファイルをZIPに固めます。npm installで生成されたnode_modulesフォルダも含めます。
固めたZIPを、先ほど作成したLambdaにアップロードします。
コードのエントリタイプのところで.zipファイルをアップロード を選択すると出てくる「アップロード」ボタンを押下してZIPファイルを選択します。
次に、ソースをちょっといじります。以下の部分のコメントを外します。
// Lambda+API Gatewayの場合に必要
const { URLSearchParams } = require('url');
const multipart_parser = require('lambda-multipart-parser');
const Multipart = require(HELPER_BASE + 'multipart');
そして、上記有効化したMultipartを使うように切り替えます。
if( event.path == '/upload' ){
// Lambda+API Gatewayの場合はこちら
var body = await Multipart.parse(multipart_parser, event);
// swagger_nodeの場合はこちら
//var body = JSON.parse(event.body);
console.log('body', body);
console.log('files', event.files);
var response = {
path : event.path,
param: {
param: body.param,
submit: body.submit,
upfile: event.files['upfile'][0].originalname,
}
};
return new Response(response);
}
そして、環境変数を追加します。
HELPER_BASE : ./helpers/
次は、API Gatewayです。
新規作成でもよいのですが、以前、fetch実験室で作ったAPIにアップロード用のエンドポイントを追加します。
リソース名:upload
メソッド:POST
Lambda関数:先ほど作成したupload_labo
CORSやLambdaプロキシ統合の使用は、忘れずに有効化しましょう。
最後に、APIのデプロイをしておきます。
デプロイが完了すると、URLの呼び出しが表示されたと思いますので、それをメモリます。
クライアント向けのstart.jsのbase_urlをAPI Gatewayで表示されたURLの呼び出しのURLに変更することで、アップロード先がAPI Gatewayに代わります。
やってみましょう。
(。。。★ここで発覚!日本語のテキストが文字化けしてしまいました。原因不明です。。。)
#バイナリファイルのダウンロード
最後に、バイナリファイルをダウンロードしてみます。
ダウンロードのトリガは、単純にGET呼び出しとしましょう。
HTMLに以下のリンクを作ります。
<a v-bind:href="base_url + '/download?param=1'">取得</a>
downloadというエンドポイントにGET呼び出しです。
Swagger定義ファイルはこんな感じです。
/download:
get:
x-swagger-router-controller: routing
operationId: upload
parameters:
- in: query
name: param
type: string
responses:
200:
description: Success
schema:
type: file
GETを受け付けてバイナリをダウンロードする実装は、api\controllers\upload\index.jsに追記しました。以下の部分です。
const BinResponse = require(HELPER_BASE + 'binresponse');
・・・
if( event.path == '/download' ){
var binary = [];
for( var i = 0 ; i < 256 ; i++ )
binary[i] = i;
var response = new BinResponse("application/octet-stream", Buffer.from(binary));
response.set_filename("output.bin");
return response;
}
application/octet-streamというMimetypeで、256バイトのデータです。
レスポンスインスタンスの生成用に、binresponse.jsというユーティリティを作ってみました。
class BinResponse{
constructor(content_type, context){
this.statusCode = 200;
this.headers = {'Access-Control-Allow-Origin' : '*', 'Cache-Control' : 'no-cache', 'Content-Type': content_type };
this.isBase64Encoded = true;
if( context )
this.set_body(context);
else
this.body = "";
}
set_filename(fname){
this.headers['Content-Disposition'] = 'attachment; filename="' + fname + '"';
return this;
}
set_error(error){
this.body = JSON.stringify({"err": error});
return this;
}
set_body(content){
this.body = content.toString('base64');
return this;
}
get_body(){
return Buffer.from(this.body, 'base64');
}
}
module.exports = BinResponse;
ここでは詳しく書いていませんが、binresponse.jsでBase64エンコードし、routing.jsでBase64デコードしています。
あとは、WebページからGETを呼び出すとファイル保存のダイアログが表示されば成功です。
#AWSへの配置
/uploadエンドポイントと同様に、API Gatewayから、fetch実験室で作ったAPIに/downloadのリソースを作成し、GETメソッドを追加し、それにupload_laboのLambda関数に割り当てます。
1つ追加で実施しておくことがあります。
LambdaからバイナリデータをAPI Gatewayに渡す際には、Base64エンコードをして渡す必要があるのですが、何もしないとBase64エンコードされたままクライアントに渡してしまいます。
そこで、特定のMimeTypeの場合は、Base64エンコードをデコードしてからクライアントに渡すように設定する必要があります。
API Gateway→ 対象のAPI→設定
そこに、バイナリメディアタイプ というのがあるので、そこにBase64デコードしてほしいメディアタイプを指定します。今回は、application/octet-streamで返そうと思っています。
★が、なぜかうまく一致が働かかなかったので、全メディアタイプをデコード対象としました。「 */* 」と入力し、「変更の保存」ボタンを押下します。
保存後、APIのデプロイをして更新しておきます。
全部を対象としても、レスポンスインスタンスで、isBase64Encodedがtrueになっていなければ、勝手にデコードはされません。
さきほどの、binresponse.jsで見た通り、BinResponseを使うと、自動的にisBase64Encodedをtrueとしていますので、Base64エンコード・デコードしたい時はこれを使うようにします。
これで、start.jsの呼び出し先base_urlをAPI Gatewayに変えても、同じようにバイナリデータがダウンロードできたかと思います。
以上