gologiusの巣

プログラミングなどの技術メモです。誰かの役に立てるとうれしいです。

バニラJavaScriptで頑張ってファイルアップロード処理を書く話

JSのライブラリを利用せずに、モダンなファイルアップロード処理を書こうとすると 割と色々コーディングしないといけないということが分かったのでメモ。

ネットに転がっているのは、

  • D&DでUPする場合と、ファイルダイアログからUPする場合を共存させる考慮
  • ファイルを追加でUPする場合の考慮
  • 特定のファイルを削除する場合の考慮
  • UPしたファイルを一覧で見たい場合の考慮

などが漏れているサンプルが多かったので、参考になれば幸い。

前提

  • JQueryは使わない
  • ライブラリは使わない(いわゆるバニラJS)Vueとか使ったほうが本当はきれいなんだけどね。
  • IEなんて考慮しない
  • 以下では、ある程度JavaScriptを分かっている人向けの適当な解説しかしない(メモなので)

サンプルページ

ファイルUPすると、リストでファイルが表示される。

https://gologius.github.io/test/file_upload/upload.html

ソース

下記参照。

gologius.github.io/test/file_upload at master · gologius/gologius.github.io · GitHub

ポイント1

input タグを二つ用意する。 片方がファイルを仮UPする用で、チェックが通ったものを、もう一方のinputにセットする。

HTML(抜粋

 <form action="./upload.php" method="post" enctype="multipart/form-data">
        <div id="dd_area" class="drop_area_off">
            <div>
                ここにファイルをドラッグ&ドロップ
                or
                <input type="button" value="手動でファイル追加" onclick="clickUpload();" >
            </div>

            <input type="file" id="upload_files" name="upload_files[]">
            <input type="file" id="tmp_files" name="tmp_files" multiple>
        </div>

        <div>アップロードしたファイル↓</div>
        <table class="file_list">
            <tbody id="upload_files_list" >
            <!--実際の要素はJS側から挿入されるで-->
            </tbody>
        </table>    
    </form>

Javascript(抜粋

function upload_files(new_files) {
    
    //変数準備
    let sum_size = 0;
    let file_count = 0;
    let file_name_list = [];
    let work_transfer = new DataTransfer(); //通常の配列ではなく、datatransferでファイル管理する必要あり。

    //現時点でUPされているファイルを走査する → 配列に格納
    let now_files = document.getElementById("upload_files").files;
    for (var i = 0; i < now_files.length; i++) {
        var file = now_files[i];
        work_transfer.items.add(file);
        
        sum_size += file.size;
        file_count += 1;
        file_name_list.push(file.name);
    }

    //アップロード対象のファイル群を走査 → 配列に格納
    for (var i = 0; i < new_files.length; i++) {
        var file = new_files[i];
        work_transfer.items.add(file);
        
        sum_size += file.size;
        file_count += 1;
        file_name_list.push(file.name);
    }

    //対象ファイルの走査が完了して用済みのため、tmp_filesはリセットする
    let elem = document.getElementById("tmp_files");
    elem.files = new DataTransfer().files;

    //UPしてよいか判定
    let errflg = false;
    if (sum_size > MAX_FILE_SIZE_MB * 1048576){
        alert(`ファイルサイズの合計値は ${MAX_FILE_SIZE_MB}MB です`);
        errflg = true;
    }
    if (file_count > MAX_FILE_COUNT){
        alert(`ファイルをUPできる最大個数は ${MAX_FILE_COUNT}個 です`);
        errflg = true;
    }
    //重複を削除したSetを生成。ファイル名が重複していると、Setの要素数がファイル数よりも減るので、それを利用する
    let uniqueSet = new Set(file_name_list); 
    if (file_count != uniqueSet.size) {
        alert("同じファイル名が既に存在します");
        errflg = true;
    }
    
    //UPしても問題ないと判断できれば、実際にファイルアップロードする
    if (errflg == false) {
        document.getElementById("upload_files").files = work_transfer.files; 
    }

    //表示更新
    update_file_list_html();

    return;
}

なんでこんなことをしているのかというと、 inputタグからファイルダイアログを開くと、そこにセットされていたファイルが上書きされてしまう。 上書きされてしまうので、追加でのファイルUPができないのである。

仮UPすることで、ファイル全数のサイズや個数のチェックもできるので、 仮UPしたほうがいろいろ都合もよい。

ポイント2

inputタグが持つfiles要素は、インデックスを指定して参照はできるが、 「直接の加工はできない」

できない例

 let now_files = document.getElementById("upload_files").files;
 for (var i = 0; i < now_files.length; i++) {
        now_files[i]  =  tmp_file;// ←これはできない
 }

よって、datatransferを丸ごとfiles にセットしてあげる必要がある。

できる例

 let now_files = document.getElementById("upload_files").files;
 let work_transfer = new DataTransfer();
 for (var i = 0; i < now_files.length; i++) {
       work_transfer.items.add(tmp_file);
 }

document.getElementById("upload_files").files = work_transfer;

特定のファイルを削除する場合には、「特定のファイルを除いた」datatransferをセットすれば、実現できる。

補足

もっとよい方法があれば教えて。