カテゴリー
コンピューター

bootstrap-fileinput の Ajax Uploads で Asynchronous Uploads なサンプルを CodeIgniter3 を使って作った記録

使ったもの

  • CodeIgniter3
  • bootstrap-fileinput 4.4.9
  • Bootstrap 3.3.7
  • jQuery 3.2.1

作ったサンプルの内容

  • できること
    • ページ初期表示時にサーバのファイルを表示
    • bootstrap-fileinput 部分のアップロードボタンでファイルをアップロードする
    • 各ファイルの削除ボタンでサーバのファイルを削除する
  • できないこと
    • フォームの Submit で他の input のデータと共にファイルをアップロードする

画面初期表示

次のことをできるようにします。

  • 画面表示時に、bootstrap-fileinput を使えるように準備する。
  • サーバにすでにファイルがある場合は、画面表示時にそれを表示する。これらのファイルは削除できるように設定する。

bootstrap-fileinput のドキュメント

サーバに置いてあるファイル情報を取得して bootstrap-fileinput に渡すデータの内容についてのポイント

  • initialPreview
    • Bootstrap File Input Options – © Kartik
    • ドキュメントでは HTML を設定するような例だが、initialPreviewAsData を true に設定するため、initialPreview には URL を設定するやり方でよくなる(initialPreviewAsData を true に設定するのは、初期表示時にサーバのファイルを表示したいから)。
  • initialPreviewConfig
    • Bootstrap File Input Options – © Kartik
    • これを設定しなくてもページ初期表示時にファイルがサムネイル付きで表示された。なのでこのオプションは不要かと思ったが、必要。これがないと、ファイル削除ができなくなる。

サーバに置いてあるファイル情報を取得して bootstrap-fileinput に渡すのに、CodeIgniter を使うやり方

  • ファイルヘルパー — CodeIgniter 3.2.0-dev ドキュメント
    • get_dir_file_info($source_dir, $top_level_only) を使って、ファイル名、サイズ、を取得する。
    • get_mime_by_extension($filename) を使って、ファイル名から MIME タイプを取得する。
    • get_filenames($source_dir[, $include_path = FALSE]) でもファイル名のみは取得できるが、get_dir_file_info で他の情報も含めて取得できるので旨味がない。

次のコードとなりました。const UPLOAD_PATH = './uploads/ajaxsubmission/'; と、別途宣言しています。

$this->load->helper('file');
$this->load->helper('url');
$initial_preview_json_array = [];
$initial_preview_config_json_array = [];
$dir_file_info = get_dir_file_info(self::UPLOAD_PATH);
foreach ($dir_file_info as $file_name => $v) {
  $initial_preview_json_array[] = sprintf('%1$s%2$s%3$s', base_url(), self::UPLOAD_PATH, $file_name);

  $mime_by_extension = get_mime_by_extension($file_name);
  $initial_preview_config_json_array[] = [
    'filetype' => $mime_by_extension,
    'caption' => $file_name,
    'size' => $v['size'],
    'url' => sprintf('%1$s/ajaxsubmission/delete', site_url()),
    'key' => $file_name, 
    'extra' => [
      'target_file' => $file_name
    ]

  ];
}
// var_dump($initial_preview_json_array);
// var_dump($initial_preview_config_json_array);

bootstrap-fileinput にわたすデータを用意する

  • JSON に変換してやる必要がある。
  • JSON に URL を渡すが、/ をエスケープされないようにしてやる必要がある。
    JSON_UNESCAPED_SLASHES オプションを、json_encode の第 2 引数に指定してやる。

$initial_preview_json = json_encode($initial_preview_json_array, JSON_UNESCAPED_SLASHES);
// var_dump($initial_preview_json);
$initial_preview_config_json = json_encode($initial_preview_config_json_array, JSON_UNESCAPED_SLASHES);
// var_dump($initial_preview_config_json);

また、HTML 側に渡すデータは次のようになります。

  • Ajax Submission モード時の bootstrap-fileinput は、サーバからは JSON レスポンスを期待する。
    しかし、ページの初期表示時は、HTML ページをレンダリングしなければならないため、このタイミングに bootstrap-fileinput のデータも渡してやる必要がある。
    HTML ロード完了直後に、bootstrap-fileinput のファイルをリクエストし、JSON レスポンスを得る方法もあると思うが、これは採用しなかった。
$data = [
  'fileinput_init' => [
    'initialPreview' => 'initialPreview: ' . $initial_preview_json,
    'initialPreviewConfig' => 'initialPreviewConfig: ' . $initial_preview_config_json,
  ],
];
$this->load->view('ajaxsubmission/index', $data);

サーバ側のメソッド全容

以上のポイントを踏まえ、サーバ側のメソッド全容は次のようになりました。

public function index()
{
  $this->load->helper('file');
  $initial_preview_json_array = [];
  $initial_preview_config_json_array = [];
  $dir_file_info = get_dir_file_info(self::UPLOAD_PATH);
  foreach ($dir_file_info as $file_name => $v) {
    $initial_preview_json_array[] = sprintf('%1$s%2$s%3$s', base_url(), self::UPLOAD_PATH, $file_name);

    $mime_by_extension = get_mime_by_extension($file_name);
    $initial_preview_config_json_array[] = [
      'filetype' => $mime_by_extension,
      'caption' => $file_name,
      'size' => $v['size'],
      'url' => sprintf('%1$s/ajaxsubmission/delete', site_url()),
      'key' => $file_name, 
      'extra' => [
        'target_file_name' => $file_name
      ]
    ];
  }
  $initial_preview_json = json_encode($initial_preview_json_array, JSON_UNESCAPED_SLASHES);
  $initial_preview_config_json = json_encode($initial_preview_config_json_array, JSON_UNESCAPED_SLASHES);

  $data = [
    'fileinput_init' => [
      'initialPreview' => 'initialPreview: ' . $initial_preview_json,
      'initialPreviewConfig' => 'initialPreviewConfig: ' . $initial_preview_config_json,
    ],
  ];
  $this->load->view('ajaxsubmission/index', $data);
}

HTML の書き方と CodeIgniter データの受け取り方

<div class="form-group">
    <div class="file-loading">
        <input id="input-700" name="kartik-input-700[]" type="file" multiple>
    </div>
    <script>
        $("#input-700").fileinput({
            language: 'ja',
            uploadUrl: "<?php echo site_url()?>/ajaxsubmission/upload",
            maxFileCount: 5,
            overwriteInitial: false,
            initialPreviewAsData: true,
            <?php echo $fileinput_init['initialPreview']; ?>,
            <?php echo $fileinput_init['initialPreviewConfig']; ?>,
        });
    </script>
</div>

ファイルアップロード時

画面が無事表示されたので、次はファイルのアップロードです。

HTML ファイルの全容

ここで、ページ初期表示時では一部のみ記載した HTML の全容を載せておきます。

<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>Form Submission</title>
    <!-- Latest compiled and minified CSS -->
    <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css" integrity="sha384-BVYiiSIFeK1dGmJRAkycuHAHRg32OmUcww7on3RYdg4Va+PmSTsz/K68vbdEjh4u" crossorigin="anonymous">
    <link href="https://cdnjs.cloudflare.com/ajax/libs/bootstrap-fileinput/4.4.9/css/fileinput.min.css" media="all" rel="stylesheet" type="text/css" />
    <!-- if using RTL (Right-To-Left) orientation, load the RTL CSS file after fileinput.css by uncommenting below -->
    <!-- link href="https://cdnjs.cloudflare.com/ajax/libs/bootstrap-fileinput/4.4.9/css/fileinput-rtl.min.css" media="all" rel="stylesheet" type="text/css" /-->
    <!-- optionally uncomment line below if using a theme or icon set like font awesome (note that default icons used are glyphicons and `fa` theme can override it) -->
    <!-- link https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.7.0/css/font-awesome.min.css media="all" rel="stylesheet" type="text/css" /-->
    <script src="https://code.jquery.com/jquery-3.2.1.min.js"></script>
    <!-- Latest compiled and minified JavaScript -->
    <script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/js/bootstrap.min.js" integrity="sha384-Tc5IQib027qvyjSMfHjOMaLkfuWVxZxUPnCJA7l2mCWNIpG9mGCD8wGNIcPD7Txa" crossorigin="anonymous"></script>
    <!-- piexif.min.js is needed for auto orienting image files OR when restoring exif data in resized images and when you
        wish to resize images before upload. This must be loaded before fileinput.min.js -->
    <!--
    <script src="https://cdnjs.cloudflare.com/ajax/libs/bootstrap-fileinput/4.4.9/js/plugins/piexif.min.js" type="text/javascript"></script>
    -->
    <!-- sortable.min.js is only needed if you wish to sort / rearrange files in initial preview. 
        This must be loaded before fileinput.min.js -->
    <script src="https://cdnjs.cloudflare.com/ajax/libs/bootstrap-fileinput/4.4.9/js/plugins/sortable.min.js" type="text/javascript"></script>
    <!-- purify.min.js is only needed if you wish to purify HTML content in your preview for 
        HTML files. This must be loaded before fileinput.min.js -->
    <script src="https://cdnjs.cloudflare.com/ajax/libs/bootstrap-fileinput/4.4.9/js/plugins/purify.min.js" type="text/javascript"></script>
    <!-- popper.min.js below is needed if you use bootstrap 4.x. You can also use the bootstrap js 
    3.3.x versions without popper.min.js. -->
    <!--
    <script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.11.0/umd/popper.min.js"></script>
    -->
    <!-- the main fileinput plugin file -->
    <script src="https://cdnjs.cloudflare.com/ajax/libs/bootstrap-fileinput/4.4.9/js/fileinput.min.js"></script>
    <!-- optionally uncomment line below for loading your theme assets for a theme like Font Awesome (`fa`) -->
    <!-- script src="https://cdnjs.cloudflare.com/ajax/libs/bootstrap-fileinput/4.4.9/themes/fa/theme.min.js"></script -->
    <!-- optionally if you need translation for your language then include  locale file as mentioned below -->
    <script src="https://cdnjs.cloudflare.com/ajax/libs/bootstrap-fileinput/4.4.9/js/locales/ja.js"></script>
</head>
<body>
    <div class="container">
        <h1>Ajax Submission, Asynchronous Uploads</h1>
        <form action="index" method="post" enctype='multipart/form-data'>
            <div class="form-group">
                <label for="fullname">氏名</label>
                <input id="fullname" name="fullname" type="text">
            </div>
            <div class="form-group">
                <div class="file-loading">
                    <input id="input-700" name="kartik-input-700[]" type="file" multiple>
                </div>
                <script>
                    $("#input-700").fileinput({
                        language: 'ja',
                        uploadUrl: "<?php echo site_url()?>/ajaxsubmission/upload",
                        maxFileCount: 5,
                        overwriteInitial: false,
                        initialPreviewAsData: true,
                        <?php echo $fileinput_init['initialPreview']; ?>,
                        <?php echo $fileinput_init['initialPreviewConfig']; ?>,
                    });
                </script>
            </div>
            <button type="submit" class="btn btn-primary">Submit</button>
        </form>
    </div>
</body>
</html>

サーバ側の処理。ファイルの受け取り方とファイル名の無害化

  • $_FILES['<input> の name 属性で [] を付けない'] で受け取る。例えば、<input id="input-700" name="kartik-input-700[]" type="file" multiple> ならば $upload_files = $_FILES['kartik-input-700'];
  • CodeIgniter3 の sanitize_filename($filename) を使用して、ファイル名に対してディレクトリトラバーサルへの保護をかける。
$upload_files = $_FILES['kartik-input-700'];

$this->load->helper('security');
$sanitized_filename = sanitize_filename($upload_files['name'][0]);

サーバ側の処理。CodeIgniter3 の ファイルアップロードライブラリを使う時の注意

  • ファイルアップロードクラス — CodeIgniter 3.2.0-dev ドキュメント
  • アップロードライブラリの allowed_types 設定は、指定が必須。
  • do_upload([$field = 'userfile']) メソッドでファイルアップロードを行うが、このメソッドは複数ファイルのアップロード (<input> の name 属性値に [] がついていて、multiple 属性がついている) に対応していない。
    そのため、別の $_FILES 変数を定義し、この変数にアップロードしたいファイルの情報を詰め直して do_upload([$field = 'userfile']) の引数として渡してやる必要がある。

先程のコード例と一部内容が重複しますけれども、次のようになります。

$upload_files = $_FILES['kartik-input-700'];

$this->load->helper('security');
$sanitized_filename = sanitize_filename($upload_files['name'][0]);
$_FILES['target_file'] = array(
  'name' => $sanitized_filename,
  'type' => $upload_files['type'][0],
  'tmp_name' => $upload_files['tmp_name'][0],
  'error' => $upload_files['error'][0],
  'size' => $upload_files['size'][0],
);
$target_file = $_FILES['target_file'];

$config = array(
  'upload_path' => self::UPLOAD_PATH,
  'allowed_types' => 'gif|jpg|png|pdf|mp4',
  'overwrite' => TRUE,
);
$this->load->library('upload', $config);
if (!$this->upload->do_upload('target_file')) {
  $this->output->set_content_type('application/json', 'utf-8')
  ->set_output(json_encode(array(
    'error' => $this->upload->display_errors(),
  )));
  return;
}

サーバ側の処理。bootstrap-fileinput へ渡すデータの作成

  • bootstrap-fileinput の append オプションへ true を渡す。これで、ファイルアップロード完了後に、さらにファイルを追加してアップロードできるようになる。
    。。。ということだと思っていたのだが、無くても本サンプルでは動いた。

  • 繰り返しにもなるが、bootstrap-fileinput の Ajax Uploads の Asynchronous Uploads の場合、サーバのメソッドには常に単一ファイルが送られてくる。
    したがって、サーバから HTML 側の bootstrap-fileinput へ渡すデータも、単一のファイルの情報を JSON にして送る必要がある。

  • initial_preview には最初サンプルのように を設定してたが、ページの初期表示のために initialPreviewAsData: true, と設定したため、URL を設定するように変更した。
    を設定した頃のコード↓

      $initial_preview = sprintf(
        '<img src="%1$s%2$s%3$s" class="file-preview-image" title="%3$s" alt="%3$s" style="width:auto;height:auto;max-width:100%%;max-height:100%%;">',
        base_url(), self::UPLOAD_PATH, $target_file['name']
      );
      
  • JSON を HTML へレスポンスするには、CodeIgniter3 では、set_content_type($mime_type[, $charset = NULL])set_output($output) メソッドを使う。
$initial_preview = sprintf(
  '%1$s%2$s%3$s',
  base_url(), self::UPLOAD_PATH, $target_file['name']
);
$output = json_encode(array(
  'initialPreview' => array($initial_preview),
  'initialPreviewConfig' => array(array(
    'filetype' => $target_file['type'],
    'caption' => $target_file['name'],
    'size' => $target_file['size'],
    'url' => sprintf('%1$s/ajaxsubmission/delete', site_url()),
    'key' => $target_file['name'], 
    'extra' => array(
      'target_file_name' => $target_file['name'],
    )
  )),
  // 'append' => true,
), JSON_UNESCAPED_SLASHES);
$this->output->set_content_type('application/json', 'utf-8')->set_output($output);

PHP 側のアップロードのコードの全容

上で掲載した内容と重複しますけれども次のようになりました。

public function upload()
{
  $upload_files = $_FILES['kartik-input-700'];

  $this->load->helper('security');
  $sanitized_filename = sanitize_filename($upload_files['name'][0]);
  $_FILES['target_file'] = array(
    'name' => $sanitized_filename,
    'type' => $upload_files['type'][0],
    'tmp_name' => $upload_files['tmp_name'][0],
    'error' => $upload_files['error'][0],
    'size' => $upload_files['size'][0],
  );
  $target_file = $_FILES['target_file'];

  $config = array(
    'upload_path' => self::UPLOAD_PATH,
    'allowed_types' => 'gif|jpg|png|pdf|mp4',
    'overwrite' => TRUE,
  );
  $this->load->library('upload', $config);
  if (!$this->upload->do_upload('target_file')) {
    $this->output->set_content_type('application/json', 'utf-8')
    ->set_output(json_encode(array(
      'error' => $this->upload->display_errors(),
    )));
    return;
  }

  $initial_preview = sprintf(
    '%1$s%2$s%3$s',
    base_url(), self::UPLOAD_PATH, $target_file['name']
  );
  $output = json_encode(array(
    'initialPreview' => array($initial_preview),
    'initialPreviewConfig' => array(array(
      'filetype' => $target_file['type'],
      'caption' => $target_file['name'],
      'size' => $target_file['size'],
      'url' => sprintf('%1$s/ajaxsubmission/delete', site_url()),
      'key' => $target_file['name'], 
      'extra' => array(
        'target_file_name' => $target_file['name'],
      )
    )),
    // 'append' => true,
  ), JSON_UNESCAPED_SLASHES);
  $this->output->set_content_type('application/json', 'utf-8')->set_output($output);
}

ファイル削除時

最後に、ページの初期表示時、またはファイルアップロード後に、ファイルのサムネイルのゴミ箱アイコンをクリックした時の削除処理です。

HTML 側の記述

  • 追加のコーディングは不要。初期表示時またはファイルアップロード時にサーバから bootstrap-fileinput へ渡すデータに削除時の情報を指定している。
  • 指定する bootstrap-fileinput のオプションは、url、key、extra

追加の HTML コーディングは不要なのですが、今までのどの部分でファイル削除に関することを記述していたのかを示します。

画面初期表示時は、次の部分がファイル削除に該当します。

  $initial_preview_config_json_array[] = [
    'filetype' => $mime_by_extension,
    'caption' => $file_name,
    'size' => $v['size'],
    'url' => sprintf('%1$s/ajaxsubmission/delete', site_url()),
    'key' => $file_name, 
    'extra' => [
      'target_file' => $file_name
    ]
  ];

ファイルのアップロード時は、次の部分がファイル削除に該当します。

$output = json_encode(array(
  'initialPreview' => array($initial_preview),
  'initialPreviewConfig' => array(array(
    'filetype' => $target_file['type'],
    'caption' => $target_file['name'],
    'size' => $target_file['size'],
    'url' => sprintf('%1$s/ajaxsubmission/delete', site_url()),
    'key' => $target_file['name'], 
    'extra' => array(
      'target_file_name' => $target_file['name'],
    )
  )),
), JSON_UNESCAPED_SLASHES);

ファイル削除時のサーバ処理

  • key として、ファイル名文字列を受け取ることにしたので、このファイル名と一致するファイルを削除する
  • 今回、extra のデータは使用していない。
  • 削除処理に成功した場合でも、bootstrap-fileinput へ JSON を返してやる必要がある。{} で良い。
  • 失敗した場合は、error という key とその内容をセットして JSON にして返してやる。
public function delete()
{
  $posted = $this->input->post();
  $is_deleted = unlink(self::UPLOAD_PATH . $posted['key']);
  if (!$is_deleted) {
    $this->output->set_content_type('application/json', 'utf-8')
    ->set_output(json_encode(array(
      'error' => '削除できませんでした。',
    )));
    return;
  }
  $this->output->set_content_type('application/json', 'utf-8')
  ->set_output('{}');
}

補足。HTML からファイルをアップロードすることについて、学んだこと

HTML は大体次のようになります。

<form method="post" enctype="multipart/form-data">
  <div>
    <label for="profile_pic">アップロードするファイルを選択してください</label>
    <input type="file" id="profile_pic" name="profile_pic" multiple>
  </div>
  <div>
    <input type="submit" class="submit" value="Submit" />
  </div>
</form>

JavaScript が絡むときのやり方です。

ドラッグ&ドロップでファイルを選択する機能を作る時の注意

  • FileList オブジェクトは、取得はできるが変更 (要素の追加、削除) はできない。
    • 例えば、<input type="file" multiple> の “ファイルを選択” と、ドラッグアンドドロップエリアがあるとする。”ファイルを選択” でユーザが選択したあとに、ドラッグアンドドロップエリアにファイルをドロップしたときに、FileList オブジェクトに、File オブジェクトを追加することはできない。
    • 例えば、<input type="file" multiple> の “ファイルを選択” でユーザーがファイルを選択すると、選択したファイルをサムネイル付きでリスト表示するとする。このリストの横に、”削除” ボタンなどをつけ、削除しようとしても、FileList オブジェクトから指定したファイルを取り除くことはできない。
  • したがって、ファイルをページにリスト等で表示し、そのリストに追加、リストから削除といった操作をしたい場合は、<input type="file" multiple> を使用せずに、JavaScript を使ってなんとかしなければならない。

おわりに

bootstrap-fileinput を使用したファイルアップロードのサンプルに取り組んでみました。

とても時間がかかりました><。

bootstrap-fileinput の使い方はもちろんよくわかっていなかったですけれども、それ上以に HTML を使用したファイルアップロード時に注意することが全然わかっておらず、とても回り道をしたように感じます。

今回の内容は、次のリポジトリとなります。

以上です。

コメントを残す