ファイルのアップロードを伴うアプリケーションを作っていて悩ましいことの1つに「中身の変わらない(全く同じ)ファイルが別のファイル名でアップロードされることがある」ことです。

アプリケーション側の実装としては、アップロードされたファイルをストレージ等に保存する処理を用意しているのですが、その際のファイル名をどうするか? という問題があります。

例えば "a.jpg" という画像ファイルと、"b.jpg" という異なる画像ファイルがあり、これら2つがアップロードされるケースだけを考えるのであれば、ストレージに保存する際にも元のファイル名をそのまま使えることになります:
2017052201
↑異なる2つのファイルを元のファイル名のまま保存する場合(このケースは問題なし)


ところが更に "a.jpg" というファイル名で、既に保存済みの "a.jpg" とは異なるファイルがアップロードされることもあります。この3つ目のアップロードファイルは "a.jpg" というファイル名で(上書き)保存するわけにはいきません(元のファイルが消えてしまう)。ということは元のファイル名をそのまま使うことは正しい処理ではなくなります:
2017052202
↑同じファイル名で中身の違うファイル保存しようとすると上書きすることになってしまう


また別のケースとして、"c.jpg" というファイル名で、"a.jpg" と中身が全く同じ画像ファイルがアップロードされるケースを考えてみます。この場合、ファイル名そのものは元のもの(c.jpg)を使っても被ることがなくいいのですが、全く同じ画像ファイルを2つ保存することになり、無駄にストレージを消費することになります。画像ファイルであればそのサイズもたかが知れているのかもしれませんが、これが仮想イメージとかだったりすると1ファイルで数10Gバイト消費することもあるため、中身の全く同じファイルであれば複数保存せずにすませたいものです:
2017052203
↑異なる名前で中身の同じファイルを元のファイル名のまま保存すると、無駄な保存領域を使うことになる


上記の問題点を実現する方法として、「同じ中身(バイナリ)のファイルは同じファイル名で、異なる中身のファイルは異なるファイル名を用意して保存する」仕組みを用意する方法があります。で、これを比較的簡単に実現する方法がファイルのハッシュ値を使うことが考えられます:
2017052204
↑ファイルのバイナリデータのハッシュ値をファイル名に使えば、中身が異なるファイルは異なるファイル名になる


試しに Node.js で実装したものを Github に公開しました:
https://github.com/dotnsf/node-upload-sample


画像のバイナリデータ(バイト配列)からハッシュ値を生成しているのは app.js 内の以下の箇所です。Node.js 標準の crypto ライブラリを使って、SHA512 アルゴリズムで path のファイルストリームからハッシュ値を生成している箇所です:
  :
var crypto = require( 'crypto' );

  :
  var path = req.file.path;
  var destination = req.file.destination;

  //. Name after Hash value
  var hash = crypto.createHash( 'sha512' );
  var fstream = fs.createReadStream( path );
  hash.setEncoding( 'hex' );
  fstream.on( 'end', function(){
    hash.end();
    var result = hash.read();

      :

ファイル(画像)をストリーム化してハッシュ値を求め、後ろに元の拡張子を付けたものを保存時のファイル名にしています。


このアプリを実際に動かしてみた様子を以下に紹介します。まず動作確認するにあたって、3種類4つのファイルを用意しました。01.png と 02.png はファイル名は異なりますが、全く同じファイルです。03.png は名前も中身も異なります:
2017052301


これらとは別に、中身の異なる 01.png というファイルを用意しました(ファイル名だけ前述のものと被ります)。これら3種類4つのファイルを全て順にアップロードした時の様子が以下です:
2017052302


まずアプリケーションを起動するとこのような画面になります。登録した画像ファイルが一覧表示されますが、まだ何も登録していない状態では何も表示されません。ここで 01.png (03.png と同じ画像の方)を選択して登録してみます:
2017052301


01.png のハッシュ値が生成され、その値をファイル名として保存されました:
2017052302


このリンクをクリックすると、元の 01.png が登録されていることが確認できます:
2017052303


同様にして、今度は 03.png を登録してみます。すると 01.png と 03.png は異なる画像ファイルであるため、ハッシュ値も異なります。そのため別画像として新たにされます:
2017052304


続けて今度は 02.png (01.png と同じ画像ファイル)を登録します。このファイルはファイル名こそ別ですが実体が 01.png と同じものであるためハッシュ値は 01.png と同じものになります。そのため「既存のデータ」と判断され、新たに画像は登録されません(正確には同じ名前のファイル名で、同じ内容を上書きすることになるので、ファイルは増えず、中身が変わることもありません):
2017052305


更にもう1つの 01.png(元の 01.png とはファイル名は同じだが、中身の異なるもの)を登録してみると、今度は中身の違うファイルなのでハッシュ値も別のものになり、新しいファイルとして登録されます:
2017052306


結果的には3種類4つのファイルを登録しましたが、内容の異なる3つのファイルがアップロードされました。中身の同じファイルは二重登録しない、という当初の目的が達成できました:
2017052307


これでストレージの無駄な空間を使わずに済ませられそうです。