Node.js
AWS
S3
aws-sdk

初心者でも node.js で S3 にマルチパートアップロードしたい

前書き

プログラミングの基礎知識はあるけれど Web や Node.js の知識が全然ありませんという人が、
参考になる記事がなさすぎるけどS3へのマルチパートアップロードの実装で奮闘した話です。


まず、公式ドキュメントが頼りにならない。古すぎる。

ブラウザからS3に巨大なファイルを低メモリで送りつけるアレ
という唯一参考にできそうな記事が見つかる。
読んでもよく分からないが、これを頼りになんとか実装してみる。

とりあえず AWS SDK for Javascript の uploadPart() というメソッド(プロパティ?)を使えばいいところまでは分かった。
(そして Github で uploadPart を検索してみたが有用なコードに辿り着くのは無理だと悟った)

3行で分かる「ブラウザからS3に巨大なファイルを低メモリで送りつけるアレ」

  1. まずはファイルデータを取得して分割します
  2. 分割したデータを uploadPart にパラメータと一緒に渡してアップロードします
  3. その他いくつか細かい設定が必要です。

因みに最後に紹介されている OSS を採用するのは外部 API の(めんどくさい)実装が必要なので却下。

並列アップロードのロジック、chunkごとの大きさ指定(5MB以上)とかエラー処理は記事のためあえて省いてる

あーっ!お客様困ります!お客様!

HTTP におけるデータ送信の仕様

POST または PUT リクエストで body にデータを乗せて送るらしい。
head をどうすればいいのかは場合によるが場合によってどうするのか不明。
S3 に送る時は PUT リクエストのみ有効の模様。

chunkってなんだ

チャンク送信を使用した通信
HTTP入門 - チャンク

通常は body を一括で送信できるが、
分割で送信する方法がチャンク送信、というイメージ。

blobってなんだ

バイナリ・ラージ・オブジェクト - Wikipedia

データベース管理システム(DBMS)においてバイナリデータを格納する場合のデータ型である。
画像や音声、その他のマルチメディアオブジェクトがBLOBとして格納される。時には実行形式が格納されることもある。

BLOB (またはバイナリー・データ型) - IBM

これまで、人間によって読み取られることを意図したデータを保管するデータ型について、説明してきました。
一部のデータ型は、人間によって直接読み取られることを意図したものでなくても、データベース内に保管できます。
例えば、デジタル・カメラの画像や CD からの歌などは、一連の数値として保管されます。
これらの数値は、人間に対してほとんど意味を成しません。
しかし、デジタル化した画像や音声は BINARY データとして保管できます。
solidDB® は、3 つのバイナリー・データ型をサポートしています。
BINARY、VARBINARY、および LONG VARBINARY (または BLOB) です。

【Azure基礎用語解説】「BLOB」

BLOB(Binary Large OBject)とは、データベースで用いられるデータ型の一つであり、
ビデオや音声、圧縮ファイル、実行ファイルなどの非構造化データ、
プレーンテキスト(平文の文字数字)以外のバイナリデータを格納する際に用いられます。

分かるような分からないような。
DB専用のデータみたいに紹介されているけど、DB以外でも扱う気がする。。。

Clobっていうのもあるらしい。

Etagってなんだ

HTTP ETag - Wikipedia

ETag(エンティティタグ)は、HTTPにおけるレスポンスヘッダの1つである。
これは、HTTPにおけるキャッシュの有効性確認の手段の1つであり、
ETagを利用してクライアントから条件付きのリクエストを行うことができる。
そうすることで、コンテンツが変わらなければレスポンスをすべて返す必要がなくなるので、
キャッシュを効率化し、回線帯域を節約できるようになる。
ETagは複数人が同時にリソースを上書きしてしまうことへの対策となる、
楽観的並行性制御に使うこともできる。
ETagはあるURLから得られる、ある特定のバージョンのリソースに対する、明確でない識別子である。
そのURLにあるリソースに何かしらの変化があれば、ETagも新しい値となる。
このように設定されたETagは、一種のフィンガープリントとなり、
2つのリソースが同じかどうかを容易に判定できるようになる。
あるETagは特定のURLに対してのみ意味を持つものであり、
他のURLから得られたリソースのETagと比較しても何ら有意な結果は得られない。

なるほど。わからん。

HTTPヘッダチューニング Etag・Last-Modified

1:ブラウザ
「前回のリクエスト時ETAGもらってたので送信しておきます。」
2:WEBサーバー
「自分のキャッシュをつかっちゃって。」
3:ブラウザ
「わかった、じゃあ自分のブラウザキャッシュつかうね。」

ちょっと分かった気がする。

とりあえず、実装の途中で ETag を生成するメソッドが必要になるっぽい。

ETagの生成はHTTPにおいて必須ではなく、またETagの生成方法については特に規定がない。
一般には、リソースの内容に対して、衝突耐性のあるハッシュ関数を使う、
最終更新日時のハッシュを取るなどの手法が取られる。

と言われても困るのだが。
まぁ、ETagの部分はサンプルコードをコピペでいけそうな気はする。

const upload = async (s3 , s3Params , file) => {}

という構文が理解不能

パッと見 async という関数定義を upload という変数に代入しているように見えるが、
アロー演算子 => を使っているので書き直してみると
const upload = async function (s3 , s3Params , file) {}
だし async は単項演算子なのか? 関数定義を単項演算子で評価して変数に格納するの???
そもそもこの関数どうやって使うの? upload(s3, s3Params, file) でいける?

とりあえず「javascript async」と「javascript 関数 代入」で調べる。

async/await 単項演算子

非同期処理のための演算子
async function() {} で非同期にして
await function() {} で同期にする
らしい。

全てのブラウザで対応しているわけではないため、利用するならBabel等でトランスパイルする必要がある。

ブラウザで対応するってどういうこと?レンダリングエンジンに依存している?
必要なライブラリを install するのではダメなのか?
とりあえず Node.js V8 では標準で使える模様。
Promise というものを使って動いているらしい。

参考資料

async/await 入門(JavaScript)
async/awaitを使った非同期処理の書き方 - 30歳からのプログラミング
Async Functions
【node.js】asyncで同期処理【サンプルリポジトリ公開】
Node.jsフロー制御 Part 1 – コールバック地獄 vs. Async vs. Highland
node.jsのいろいろなモジュール17 – asyncで非同期処理のフロー制御
JavaScriptのasync.jsでwaterfallとseries、parallelの違い

Promise

ただしPromise使うので、typescriptやbabel等のプリプロセッサ通さない人は、es6-promiseあたりのポリフィルを念のため入れること

日本語でおk
非同期処理をうまく書くためには Promise というものを利用すると良いらしい。

JavaScript Promiseの本
という電子ドキュメントが公開されている。 🙏

あと Ajax ライブラリの axios も Promise 使ってるらしい。
axiosを乗りこなす機能についての知見集
axios、async/awaitを試してみた

参考資料

Promise - MDN
今更ながらNode.jsでPromiseの使用例
node.js Promiseを使った非同期処理
Promiseについて0から勉強してみた
Node.jsのコールバック地獄をPromiseやGeneratorを使って解消する
Node.jsにPromiseが再びやって来た!
ECMAScript6のアロー関数とPromiseまとめ
非同期をわかりやすく制御しよう!ES6の「Promise」入門

関数式 / 関数リテラル

関数宣言には以下の3種類があるらしい。
* 関数コンストラクタ var callName = new Function(arg, process)
* 関数式 var callName = function(arg) { process }
* 関数宣言 function callName(arg) { process }

【JavaScript】関数定義いろいろ
無名関数 関数式と関数宣言

「低メモリで送りつけるアレ」のサンプルコードを動かすための知見

以下を npm install しておく必要がある。
* aws-sdk
* mime

んで以下のような宣言が最初に必要。<>内は適宜設定。

import fs from 'fs'
import MIME from 'mime'
import AWS from 'aws-sdk'
const s3 = new AWS.S3({
  region: <AWS_REGION>,
  credentials: {
    accessKeyId: <AWS_ACCESS_KEY_ID>,
    secretAccessKey: <AWS_SECRET_ACCESS_KEY>
  }
})
const s3Params = {
  Bucket: <S3_BUCKET>,
  Key: <S3_KEY>
}
const file = fs.readFileSync('filename')

file が何を格納する意図なのか不明だったが readFileSync() でとりあえず(1部修正が必要だが)動く。


3行目 const mime = Mime.lookup(file.name)
は mime のアップデートで lookup()getSize() に変更になっているので、
const mime = Mime.getSize(file.name) に直す。


5行目 const allSize = file.size
readFileSync() を使った場合に .size でサイズが取れないので、

const stat = fs.statSync(fileAbsPath)
const allSize = stat.size

に置き換える。


24〜35行目

const sendData = await new Promise((resolve)=>{
    let fileReader =  new FileReader();

    fileReader.onload = (event)=>{
        const data = event.target.result;
        let byte = new Uint8Array(data);
        resolve(byte);
        fileReader.abort();
    };
    const blob2 = this.file.slice(rangeStart , end);
    fileReader.readAsArrayBuffer(blob2);
})

は何をしようとしているコードなのかは良くわからないが
(何故 file ではなく this.file を参照しているのか不明だが)
const sendData = await file.slice(rangeStart , end); で問題なく動く。
というかそのままだと slicereadAsArrayBuffer でエラー吐いて動かない。
await が必要なのかは未検証。


37行目 const progress = end / file.sizefile.sizestat.size に置き換える。


40行目 const partParams = {
55行目 const doneParams = {
otherParams の部分に

Bucket: <S3_BUCKET>,
Key: <S3_KEY>,

の指定が必要。


以上を直せば動く……はず。
自分の環境では動きました。

おわりに

ツッコミどころ満載だと思うのでマサカリお待ちしております。