至高のファイルアップロード

システムエンジニアにとって頻出機能のひとつで、要件がモリッとしがちなファイルアップロードについてです。アップロードファイルもただの画像やPDFではなく、ExcelやCSVファイルをアップロードして、中身を読み取ってデータベースに格納する、あれを対象とします。

## 標準的なファイルアップロードでの設計ポイント

ファイルのアップロード機能を実装しようとなると、以下のような点の設計を考える必要があります。

- 同期? 非同期?
  - タイムアウトのリスクがあれば、非同期にする
     - ブラウザのタイムアウト
     - 通信経路(プロキシやファイヤウォール)でのタイムアウト
     - Webサーバのタイムアウト
     - ユーザがレスポンス返ってこないので、処理を中止するリスク
- 非同期の場合の実行方式
  - Webサーバ内でスレッドを新規に作る or 別プロセスへ処理をディスパッチする
  - 流量制御のためリクエストをキューイングする
  - Progress
     - 長時間かかる場合は、現在何%まで進んでいるのか?
     - 完了をどうやって伝えるのか (Eメール?)
     - アップロードしたけど間違いに気づいたときにキャンセルする方法は?
- エラーレコードの伝え方
  - エラーレコードをマーキングしたExcelファイルをダウンロードさせる
  - エラーメッセージ通知用のページを用意する
- エラー時の再処理
  - エラー箇所を修正して全量アップさせる? エラー分のみ、差分アップロードさせる?
  - エラーレコードの修正方法は?
- 変なファイルをアップされることへの対策
  - ファイルサイズ上限
     - 2段階の上限を考える。①その業務で使うであろう最大サイズ、と②サーバリソースを守るための最大サイズ。②は通常Webサーバで設定し、上限を超えたら通信を打ち切る。
     - IE10以上であれば、JavascriptでFileオブジェクトが使えるので、①はJavascriptだけでチェック可能
  - ウィルスチェック
     - アップロードファイルは一時格納場所に置き、アンチウイルスソフトでチェックする。
  - 拡張子チェック
     - IE10以上であれば、```<input type="file" accept=".xls,.xlsx"/>```のようにしてファイル選択ダイアログで制限できる
     - WebサーバのMIMEタイプを絞ることでも、制限は可能
- ファイルからDBへの処理方式
  - 処理件数の単位(できるだけバッチインサートになるように)
  - コミットのタイミング

ふぅ、大変ですね…
そこで、2015年も終わろうとしている今、もっとアップロード処理を簡単にできないのでしょうか。考えてみます。

## A. クライアントサイドでExcel/CSVを読み込む (IE9以上)

Excelをクライアントサイドで処理しちゃえば、前述のファイルをアップロードするために考えなきゃいけないことや非同期のための煩わしさは無くなります。

https://github.com/SheetJS/js-xlsx

を使うと、JavascriptだけでExcelファイルを(XLS/XLSXともに)読むことが可能です。

1. ファイルを指定して読み込む
  ![image](https://qiita-image-store.s3.amazonaws.com/0/8475/a8c1be96-370e-a6d7-4c0c-74012de830af.png)
1. 読み取ったレコードをサーバにPOSTし、バリデーションエラーになったものは修正フォームを表示する。
  ![image](https://qiita-image-store.s3.amazonaws.com/0/8475/595b7b57-21f2-dc55-916a-f2795d07f3e2.png)
1. エラーを修正して、「登録」を押して完了

これで、スムーズなアップロードが可能です。エラーの訂正
js-xlsxは、一度にブラウザのメモリにロードするので、大きなファイルはパースの段階で時間がかかり、ブラウザがフリーズします。実用的には、Workerスレッドで処理させる方がよいでしょう(IE10以上が必要)。しかし、何れにせよ巨大なファイルは、ブラウザのメモリを喰うので、POIのEvent APIのように、SAXで処理させるようにした方がよいのですが…js-xlsxでは非対応のようです。

数MB程度のExcelファイルであれば、これでいけます。が、POIと同じように(POI以上に)読めないExcelファイルが出てくるリスクはあることは考えておかなければなりません。

## B. そもそもアップロードしない (IE9以上)

ファイルをアップロードするから、面倒なことが増えるわけで、ここではファイルをアップロードするのではなく、ブラウザ上に表示したグリッドにデータをコピペする方式を考えます。

2015年現在、もっともExcelのようなUIを提供するJavascriptライブラリは、[Handsontable](https://github.com/handsontable/handsontable)で間違いないかと思います。

- 列幅の拡大縮小
- 列のソート
- セルの結合
- セルの固定

など、Excel使いを満足させうる機能をサポートしています。React的に仮想DOMを使い、表示されているエリアのみレンダリングしているので動作も高速です。

HandsontableのExampleを触ってみると、過去似たようなUIに挑戦したことのあるシステムエンジニアにとっては、ここまできたか…と感涙ものでしょう。
http://handsontable.com/examples.html

Bパターンの手順としては、以下のとおりです。

1.貼付け用のグリッドを表示します。 
  ![image](https://qiita-image-store.s3.amazonaws.com/0/8475/0e50f023-f7af-cbe8-6d28-7a4489a7ee68.png)
2. Excelからデータをコピーします。
  ![image](https://qiita-image-store.s3.amazonaws.com/0/8475/b4315342-4653-4fee-148b-74def81b2220.png)
3. グリッドに貼り付け、エラーを修正し保存します。
  ![image](https://qiita-image-store.s3.amazonaws.com/0/8475/a11029bd-4c0d-5c74-161b-5e7e79110e7d.png)

この方式の場合、複数行入力のところ、Excelからのコピペもできますよという位置づけで捉えた方がよいかと思います。が、ふつうの業務システムでは、Excelで数万行もアップロードする機能の方が少なく、数百から数千行であればこの貼り付けで十分カバーできるでしょう。

Handsontableを触ってみた感じ、気になるところがいくつかあります。

1. コピペ件数が一定量(数千行)越えたあたりで、極端にレスポンスが低下する。
   - Handsontableの実装に問題がありそうですが、追えてません。コピペしてしまえば、実用的にはクリップボードのサイズに上限をもたせる必要がありそうです。
2. 全部データはメモリ上にもつ
   - アーキテクチャ上、仕方のないことかもしれませんが、前述の方式同様、一度にアップロードできるサイズには上限が必要です。
3. 日本語を入力するときに、1文字目が変換されない
   - コピペの場合は問題ありませんが、手入力するときこの問題に遭遇します。これはIssueがあがっています。https://github.com/handsontable/handsontable/issues/839
これはJavaFXのTableViewでも、同じ問題が起きます。グリッドUIあるあるなのかもしれませんね…

1.は結構クリティカルなので、調査してみようと思います。

## まとめ

10MBを超えるようなファイルを処理する場合には苦しいですが、大抵の業務で使うようなファイルは、「至高のアップロード」でいけそうです。

HTML5になると、ただ単にモダンな書き方ができるだけではなく、至高を目指せば作りこむ量を大きく減らせる公算が高くなります。

「もう古いIE切り捨てませんか?」

ではなく、

「古いIE切り捨てれば、アップロードが至高になり、工数がこんだけ減りますが……(電卓弾く音)……、どうしますか?」

ですよ。システムエンジニアのみなさん!!

上記のA/B案をClojure+duct+omで、実装したものは、以下に置いています。
https://github.com/kawasima/supreme-uploading