jQuery + FileReader APIでファイルを分割し、バイナリでアップロード

HTML 5 + jQueryで複数ファイルのアップロードを試してみました。
CodeIgniter 3 + HTML5 FileAPI + jQueryで複数ファイルのアップロード
HTML5 + jQueryで複数ファイルのアップロード時、プログレスバーを表示

今度は、1つの大きなファイルを分割し、バイナリ形式でアップロード。
サーバー側で復元してみます。



FileReader API



JavaScriptでファイルを読み込み。
指定バイトでスライスする方法は、こちらがとても参考になりました。

Reading files in JavaScript using the File APIs
File API W3C Working Draft 21 April 2015
FileReader.readAsArrayBuffer()
XMLHttpRequest の利用


分割してアップロードする都合上、識別子としてユニークなIDが振りたい。
UUIDの生成は、こちらのJavaScript実装を使用しました。

UUID v4 generator in JavaScript (RFC4122 compliant)


jQueryを使用して、ヘッダー情報をつけたり、バイナリデータをアップロードする方法はこちら。

JSON Post with Customized HTTPHeader Field
Sending binary data in javascript over HTTP


web側の実装はPHP + CodeIgniter 3で行っています。

viewのソースはこんな感じになりました。


・application/views/fileupload.php


  1. <html>
  2. <head>
  3.     <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
  4.     <title>ファイルアップロード</title>
  5.     <script src="//code.jquery.com/jquery-2.2.3.min.js" integrity="sha256-a23g1Nt4dtEYOj7bR+vTu7+T8VP13humZFBJNIYoEJo=" crossorigin="anonymous"></script>
  6.     <script>
  7. <!--
  8. $(function(){
  9.     
  10.     // フォームデータのアップロード処理
  11.     var uploadBlobData = function(fileNmae, fileKey, totalBytes, binaryData, tasks, chunkCount) {
  12.         
  13.         // アップロードの進捗表示
  14.         var xhr_func = function(){
  15.             var XHR = $.ajaxSettings.xhr();
  16.             XHR.upload.addEventListener('progress',function(e){
  17.                 tasks[chunkCount] = e.loaded;
  18.                 
  19.                 var upload = 0;
  20.                 tasks.forEach(function(bytes) {
  21.                     upload += bytes;
  22.                 });
  23.                 
  24.                 var progre = parseInt(upload/totalBytes * 100);
  25.                 
  26.                 $('#prog').val(progre);
  27.                 $('#pv').html(progre);
  28.             });
  29.             return XHR;
  30.         };
  31.         
  32.         
  33.         // Ajaxでアップロード処理をするファイルへ内容渡す
  34.         $.ajax({
  35.             url: 'fileupload/upload',
  36.             type: 'POST',
  37.             data: binaryData,
  38.             processData: false,
  39.             contentType: 'application/octet-stream',
  40.             headers: {
  41.                 'File-Name': fileNmae,
  42.                 'File-Key': fileKey,
  43.                 'Chunk-Index': chunkCount,
  44.                 'Chunk-Total': tasks.length
  45.             },
  46.             xhr : xhr_func
  47.             
  48.         }).done(function(data) {
  49.             console.log(data);
  50.             
  51.         }).fail(function(data) {
  52.             console.log(data.responseText);
  53.         });
  54.         
  55.     };
  56.     
  57.     // https://gist.github.com/jcxplorer/823878
  58.     var createUuid = function() {
  59.         var uuid = "", i, random;
  60.         for (i = 0; i < 32; i++) {
  61.             random = Math.random() * 16 | 0;
  62.             if (i == 8 || i == 12 || i == 16 || i == 20) {
  63.                 uuid += "-"
  64.             }
  65.             uuid += (i == 12 ? 4 : (i == 16 ? (random & 3 | 8) : random)).toString(16);
  66.         }
  67.         return uuid;
  68.     };
  69.     
  70.     // ファイルのアップロード処理
  71.     var uploadFile = function(file) {
  72.         
  73.         $('#prog').val(0);
  74.         $('#pv').html('0');
  75.         
  76.         // 分割するサイズ(byte)
  77.         var chunkSize = 8 * 1024 * 1024;
  78.         // 選択されたファイルの総容量を取得
  79.         var totalBytes = file.size;
  80.         // ファイル名
  81.         var fileNmae = file.name;
  82.         
  83.         // チャンク分割数
  84.         var chunkCount = Math.ceil(totalBytes / chunkSize);
  85.         // 識別キー
  86.         var fileKey = createUuid();
  87.         
  88.         var readBytes = 0;
  89.         var tasks = [];
  90.         for (var i = 0; i < chunkCount; i++) {
  91.             tasks.push(0);
  92.         }
  93.         
  94.         // チャンクサイズごとにスライスしながら読み込み
  95.         $.each(tasks, function(index) {
  96.             
  97.             // stopをオーバーして指定した場合は自動的に切り詰められる
  98.             var blob = file.slice(readBytes, readBytes + chunkSize);
  99.             readBytes += chunkSize
  100.             
  101.             var reader = new FileReader();
  102.             reader.onloadend = function(evt) {
  103.                 // 読み取り完了のイベントだけキャッチ
  104.                 if (evt.target.readyState != FileReader.DONE) {
  105.                     return;
  106.                 }
  107.                 
  108.                 // 読み取ったデータを取り出し
  109.                 var binaryData = evt.target.result;
  110.                 uploadBlobData(fileNmae, fileKey, totalBytes, binaryData, tasks, index);
  111.             };
  112.             reader.readAsArrayBuffer(blob);
  113.             
  114.         });
  115.         
  116.     };
  117.     
  118.     // ファイルドロップ時の処理
  119.     $('#drag-area').on('drop', function(e){
  120.         // デフォルトの挙動を停止
  121.         e.preventDefault();
  122.         // ファイル情報を取得
  123.         var files = e.originalEvent.dataTransfer.files;
  124.         uploadFile(files[0]);
  125.     
  126.     
  127.     // デフォルトの挙動を停止 これがないと、ブラウザーによりファイルが開かれる
  128.     }).on('dragenter', function(){
  129.         return false;
  130.     }).on('dragover', function(){
  131.         return false;
  132.     });
  133.     
  134.     
  135.     // ボタンを押した時の処理
  136.     $('#btn').on('click', function() {
  137.         // ダミーボタンとinput[type="file"]を連動
  138.         $('#file_selecter').click();
  139.     });
  140.     $('#file_selecter').on('change', function(){
  141.         // ファイル情報を取得
  142.         uploadFile(this.files[0]);
  143.     });
  144. });
  145. -->
  146. </script>
  147. </head>
  148. <body>
  149. <div id="drag-area" style="border-style: dashed;background-color: #042943; color: #ffffff;">
  150. <p>アップロードするファイルをドロップ</p>
  151. <p>または</p>
  152. <div class="btn-group">
  153.     <input id="file_selecter" type="file" style="display:none;" name="files"/>
  154.     <button id="btn">ファイルを選択</button>
  155. </div>
  156. </div>
  157. <progress value="0" id="prog" max=100></progress>(<span id="pv" style="color:#00b200">0</span>%)
  158. </body>
  159. </html>





コントローラー側は、分割されたチャンクデータを受け取り保存。
すべてのチャンクが揃ったことを確認して復元します。

POSTされたデータの読み取りはこちら。
CodeIgniter3 JSONを返すAPIサーバーとして使用する

分割ファイルの結合はこちら。
PHP 指定バイト数でファイルを分割&結合する


・application/controllers/Fileupload.php


  1. <?php
  2. class Fileupload extends CI_Controller {
  3.     
  4.     // アップロード用の画面を表示
  5.     public function index() {
  6.         $this->load->view('fileupload');
  7.     }
  8.     
  9.     // 画像アップロード
  10.     public function upload() {
  11.         
  12.         $headers = $this->input->request_headers();
  13.         
  14.         // 一時領域に[UUID].[チャンクの番号(0詰め10桁)]というファイル名で保存
  15.         // ※globで取得した時、ファイル名が順番に取れるようにしておく
  16.         $tmp_file = sprintf("%stmp/%s-%010d", FCPATH, $headers['File-Key'], $headers['Chunk-Index']);
  17.         file_put_contents($tmp_file, $this->input->raw_input_stream);
  18.         
  19.         // ファイルが揃っているかチェック
  20.         $chunk_list = glob( FCPATH.'tmp/'.$headers['File-Key'].'*');
  21.         $chunk_count = count($chunk_list);
  22.         
  23.         // 他のチャンクがアップロードされるのを待つ
  24.         if ($chunk_count < $headers['Chunk-Total']) {
  25.             $this->output
  26.                 ->set_content_type('application/json')
  27.                 ->set_output(json_encode(['result' => 'other chank waiting']));
  28.             return;
  29.         }
  30.         
  31.         
  32.         // チャンクが揃ったら結合する
  33.         // 結合用の同名ファイルが存在したら消しておく
  34.         $file_path = FCPATH.'tmp/'.$headers['File-Name'];
  35.         if (file_exists($file_path)) {
  36.             unlink($file_path);
  37.         }
  38.         
  39.         // 順番に結合
  40.         foreach($chunk_list as $chunk_path) {
  41.             $chunk = file_get_contents($chunk_path);
  42.             file_put_contents($file_path, $chunk, FILE_APPEND);
  43.             unlink($chunk_path);
  44.         }
  45.         
  46.         // 保存結果を返信
  47.         $this->output
  48.             ->set_content_type('application/json')
  49.             ->set_output(json_encode(['result' => $file_path]));
  50.         
  51.     }
  52. }





画面表示はこんな感じ。

691_01.png


プログレスバーの動きが進まない場合があるのは今後の課題です。

691_02.png


これで100MBを超えるファイルもアップロードできるようになりました。






【参考URL】

Reading files in JavaScript using the File APIs
File API W3C Working Draft 21 April 2015
FileReader.readAsArrayBuffer()
XMLHttpRequest の利用
UUID v4 generator in JavaScript (RFC4122 compliant)
CodeIgniter3 JSONを返すAPIサーバーとして使用する
PHP 指定バイト数でファイルを分割&結合する

関連記事

コメント

プロフィール

Author:symfo
blog形式だと探しにくいので、まとめサイト作成中です。
https://symfo.web.fc2.com/

PR

検索フォーム

月別アーカイブ