You are here
Home > Posts tagged "ファイルのアップロード"

Laravel 5.3 AWS S3にファイルをアップロード

アップロードしたファイルの保存のメソッドがLaravelで5.3で少し変わりました。ここでそれらの情報更新とともに、AmazonのストレージサービスS3にファイルをアップロードする仕方を紹介します。

まず、準備から、

パッケージの追加と設定

コマンドラインで以下の実行が必要です。


composer require league/flysystem-aws-s3-v3 ~1.0

これにより、

config/filesystems.php

の設定ファイルが作成されます。


return [
    'default' => 'local',
    'cloud' => 's3',

    'disks' => [

        'local' => [
            'driver' => 'local',
            'root' => storage_path('app'),
        ],

        'public' => [
            'driver' => 'local',
            'root' => storage_path('app/public'),
            'visibility' => 'public',
        ],

        's3' => [
            'driver' => 's3',
            'key' => 'AWSのキー',
            'secret' => '秘密のキー',
            'region' => '地域のコード', // 日本なら、ap-northeast-1
            'bucket' => 'バケット名'
        ],
    ],
];
    

localは、使用しているサーバーのストレージのことです。

rootは、Laravelをインストールしたディレクトリのサブディレクトリ、storage/appの場所となります。

publicは、ウェブユーザーにアップロードしたファイルをパブリックに紹介する場所です。

以下の実行で、public/storageが、storage/app/publicにリンクされます


php artisan storage:link

例えば、アップロードされたファイルは、

storage/app/public/mario.jpg

に保存され、

http://localhost/public/storage/mario.jp

で閲覧できるということです。

s3のkey, secret, region, bucketの指定は必須です。これらは、Amazonのウェブサービスのコンソールで取得できます。

これで設定終わりです。

ファイルのアップロードのプログラム

簡単なファイルのアップロードのプログラムを書いてみます。

まず、routeの設定から、


    Route::get('upload', 'UploadController@create');
    Route::post('upload', 'UploadController@store');

これで、

http://localhost/upload

にアクセス可能です。

次にコントローラ、

namespace App\Http\Controllers;

use Illuminate\Http\Request;

use Storage;

class UploadController extends Controller
{
    public function create()
    {
        return view('upload');
    }

    public function store(Request $request)
    {
        $filename = $request->file('image')->getClientOriginalName(); //アップロードしたファイル名を取得

        $path = $request->file('image')->storeAs('public', $filename);

        return back()->with('filename' => $filename);
    }
}

storeAs('public', $filename);

この最初のパラメータは、ファイルを保存するディレクトリ名です。先のconfig/filesystems.phpの設定で、storage/appがルートのディレクトリゆえに、上のコードではstorage/app/publicにファイルが保存されることになります。

ファイル名がmario.jpgであれば、

storage/app/public/mario.jpg

と保存されます。

ファイルをアップロードするフォームのブレードは、


@extends('layouts.app')

@section('content')
<div class="container">
    <div class="row">
        <div class="col-md-8 col-md-offset-2">
            <div class="panel panel-default">
                <div class="panel-heading">Media Upload</div>
                <div class="panel-body">
                    <form class="form-horizontal" role="form" method="POST" action="{{ url('upload') }}" enctype="multipart/form-data">
                        {{ csrf_field() }}

                        <div class="form-group">
                            <label for="image" class="col-md-4 control-label">File</label>

                            <div class="col-md-6">
                                <input id="image" type="file" name="image">
                            </div>
                        </div>

                        <div class="form-group">
                            <div class="col-md-6 col-md-offset-4">
                                <button type="submit" class="btn btn-primary">
                                    Upload
                                </button>
                            </div>
                        </div>
                    </form>
                </div>
            </div>
        </div>
    </div>
    <div class="row">
        <div class="col-md-8 col-md-offset-2">
            <div class="panel panel-default">
                <div class="panel-body">
                @if (session('filename'))
                    <h4>Local</h4>
                    <img src="{!! asset('storage/'.session('filename')) !!}">
                @endif
                </div>
            </div>
        </div>
    </div>
</div>
@endsection

先の例で、ファイル名が、mario.jpgならば、

asset('storage/'.session('filename'))

は、

http://localhost/public/storage/mario.jpg

のようになるわけです。

ファイルをアップロードした後の画面はこんな感じです。
my-application

S3に画像をアップロード

さて、サーバーにアップした画像を、今度はS3にアップするのですが、これはconfig/filesystems.phpの設定が済んでいれば、本当に簡単です。

コントローラのstoreメソッドにたったの2行追加するだけです。


   public function store(Request $request)
    {
        $filename = $request->file('image')->getClientOriginalName();

        $path = $request->file('image')->storeAs('public', $filename);

        $contents = Storage::get('public/'.$filename); //ファイルを読み取る
        Storage::disk('s3')->put($filename, $contents, 'public'); // S3にアップ
 
        return back()->with(['filename' => $filename]);
    }

>put($filename, $contents, 'public')

ここのpublicに注意してください。これがないと一般には公開されません。

以下のAWSのコンソールの赤箱の部分がそれにより追加されます。

s3-management-console

先の画面にS3から直接画像を表示したいなら、ブレードに以下の変更を。

    <div class="panel-body">
    @if (session('filename'))
       <h4>Local</h4>
       <img src="{!! asset('storage/'.session('filename')) !!}">
       <h4>S3</h4>
       <img src="{!! Storage::disk('s3')->url(session('filename')) !!}">
    @endif
    </div>

S3の以下のURLが生成されます。

https://s3-us-west-2.amazonaws.com/demo53/mario.jpg

us-west-2は、設定に使用した地域コードです。日本ならap-northeast-1となります。

ファイルのアップロード(8)プライベートに画像を表示

前回は、画像をパブリックに表示する方法を説明しましたが、今回は画像をプライベートに表示する方法です。

いくつか方法があります。

まず、前回のようにアップロードをパブリックの場所に保存して、特定のユーザーだけに表示のためのURLを教える。

しかし、DBから自動発行されるproduct_image_idを画像ファイル名に使用するなら、URLを操作することで他のファイルも見れてしまいます。

そうなら、画像のURLをわかりにくいように変えて、他の画像のURLを予想しにくくすることも可能です。

例えば、235.jpgとは見せずに、1f3870be274f6c49b3e31a0c6728957f.jpgにするとか。

それは、md5()を利用することで簡単に可能です。


    public function filename()
    {
        $ext = 'jpg';
 
        switch($this->mime)
        {
            case 'image/jpeg':
            case 'image/jpg':
                $ext = "jpg";
                break;
 
            case 'image/png':
                $ext = "png";
                break;
 
            case 'image/gif':
                $ext = "gif";
                break;
        }
 
        return sprintf("%d.%s", md5($this->product_image_id), $ext);
    }

よりセキュアにするには、DBに保存するときに、uniqid()あるいは、openssl-random-pseudo-bytes()を使用してランダムな値を生成して、その値をファイル名として保存するとか。要するに、IDのように連続な番号とはならないので、容易にファイル名を予測できないようにすることです。

しかし、究極は、ファイルをパブリックから見れない場所に保存して、それを表示する方法です。

例えば、storage/images/product/1.jpgのように、パブリックから見れないstorageのディレクトリに画像をアップロードするようにして、見せるときには、ログインしたユーザーと関連ある画像だけを、そのユーザーに表示する。

この場合は、パブリックに保存されている画像と違い、固定のURLを通してウェブサーバーに画像の表示を任せることはできません。逆に、あたかもウェブサーバーが画像ファイルを読んでデータをストリームするという作業と同じことをプログラムで行います。header()を使用すれば、そう難しいことではありません。

namespace App\Http\Controllers\User;

use Illuminate\Http\Request;

use Log;

use App\Http\Requests;
use App\Http\Controllers\Controller;
use App\Product;
use App\ProductImage;

class ProductController extends Controller
{
	public function getImage(Product $product)
	{
		return view('user/product_image', compact('product'));
	}

	public function downloadImage(ProductImage $product_image)
	{
		//@TODO ここで、認証したユーザーに画像を表示していいかどうかをチェック。
		//そうでないなら、空の画像を表示

		$filename = $product_image->filename();

		header("Content-type: $product_image->mime name=$filename");
		header("Content-Disposition: attachment; filename=$filename");
		header("Content-Length: ".@filesize($product_image->path));
		header("Expires: 0");
		@readfile($product_image->path);
		exit;
	}
}

上で使用されるテンプレートは、

@extends('user.layouts.app')

@section('content')
<div class="container">
    <div class="row">
        <div class="col-md-8 col-md-offset-2">
            <div class="panel panel-default">
                <div class="panel-heading">アップロードしたファイルを表示</div>
                <div class="panel-body">
                    <div>
                        @foreach ($product->product_images as $image)
                            <img src="{{ url('/user/product_image', $image->product_image_id) }}">
                        @endforeach
                    </div>
                </div>
            </div>
        </div>
    </div>
</div>
@endsection

のようになります。

routes.phpは、以下のように認証保護された中でコールされます。


Route::group(['prefix' => 'user', 'middleware' => 'web'], function () {

	Route::get('login', 'User\Auth\AuthController@showLoginForm');
	Route::post('login', 'User\Auth\AuthController@login');
	Route::get('logout', 'User\Auth\AuthController@logout');
..
	Route::group(['middleware' => 'auth:user' ], function () {
		Route::get('home', 'User\HomeController@index');
..
		Route::get('product/{product}/image', 'User\ProductController@getImage');
		Route::get('product_image/{product_image}', 'User\ProductController@downloadImage');
	});
});

ファイルのアップロード(7)パブリックに画像を表示

アップロードしてサーバーに保存した画像ファイルを表示するには、いくつか方法があります。

前回のように、誰もが見れるパブリックな場所(public/images/product)にファイルを保存したなら、

namespace App;

use Illuminate\Database\Eloquent\Model;

class ProductImage extends Model
{
	protected $table = 'product_image';
	protected $primaryKey = 'product_image_id';
	public $incrementing = true;
	public $timestamps = true;

	protected $fillable = [
		'product_id', 'mime'
	];

	protected $baseUri = 'images/product';

	public function getUrlAttribute()
	{
		return url($this->baseUri, $this->filename());
	}

	public function storeImage($file)
	{
		$file->move(public_path($this->baseUri), $this->filename());
	}

	public function filename()
	{
		$ext = 'jpg';

		switch($this->mime)
		{
			case 'image/jpeg':
			case 'image/jpg':
				$ext = "jpg";
				break;

			case 'image/png':
				$ext = "png";
				break;

			case 'image/gif':
				$ext = "gif";
				break;
		}

		return sprintf("%d.%s", $this->product_image_id, $ext);
	}

と、getUrlAttribute()のアクセサーを作成して、


@extends('user.layouts.app')

@section('content')
<div class="container">
    <div class="row">
        <div class="col-md-8 col-md-offset-2">
            <div class="panel panel-default">
                <div class="panel-heading">File Upload</div>
                <div class="panel-body">
                    <div class="form-group{{ $errors->has('file') ? ' has-error' : '' }}">
                        <div>
                            <form
                                method="POST"
                                action="/demo/public/admin/product/{{ $product->product_id }}/image"
                                class="dropzone"
                                id="imageUpload"
                                enctype="multipart/form-data">
                                {{ csrf_field() }}
                            </form>
	                    </div>
                    </div>

                    <div>
                        @foreach ($product->product_images as $image) // 1商品に対して複数の画像を表示
                            <img src="{!! $image->url !!}">
                        @endforeach
                    </div>
                </div>
            </div>
        </div>
    </div>
</div>
@endsection

@section('script')

Dropzone.options.imageUpload = {
    dictDefaultMessage: 'アップロードするファイルをここへドロップしてください',
    maxFiles: 10,
    acceptedFiles: '.jpg,.jpeg,.gif,.png',
    maxFilesize: 5, // 5 MB
    init: function () {
        this.on('queuecomplete', function () { // アップロードがすべて完了したら、画面を更新してアップロードした画像を表示
            location.reload(); 
        });
    }
}
@endsection

のように、$image->urlとしてテンプレートでコールして、https://www.larajapan.com/demo/public/images/product/4.pngのような値を生成し画像をブラウザに表示します。

また、以下のEloquentのRelationshipを使用して、1商品に対して複数アップロードした画像を表示させています。


namespace App;

use Illuminate\Database\Eloquent\Model;

class Product extends Model
{
    protected $table = 'product';
    protected $primaryKey = 'product_id';
    public $incrementing = true;
    public $timestamps = true;

    protected $fillable = [
        'name'
    ];

    public function product_images()
    {
        return $this->hasMany('App\ProductImage');
    }

}

さて、以上は、管理者が管理画面からアップロードした画像を、同画面で表示する例ですが、ユーザー画面でも$image->urlを使用すれば、誰にでも同じ画像を見せることが可能です。

しかし、アップロードしたファイルを誰からも見えない場所に保存して、例えば、認証したユーザーしか見れないという制限が必要なら、どうしましょう?

ファイルのアップロード(6)異なる画像フォーマットの対応

画像のファイルのアップロードの基本を学んだところで、少し実践的なことを考えてみましょう。

例えば、ECサイトなら、販売する商品画像を管理画面でアップロードします。しかし、重複となるかもしれないので、アップロードした画像のファイル名で保存することはできません。

そこで、DBが自動発行する商品のIDあるいは商品番号を利用してファイル名を変えてサーバーに保存します。

例えば、Nikeの靴.jpgというファイル名のファイルをアップするなら、その商品のIDが234なら、234.jpgとしてサーバーに保存します。

もう1つ考慮必要なことは、アップする画像はいつも、JPEGの形式とは限らないことです。GIFかもしれませんし、PNGのフォーマットかもしれません。もちろん、手元でJPEGに変換してからアップもできますが、サーバーで違う画像フォーマットに対応できるならそれに越したことありません。

これらのフォーマットの違いの情報を対応するには、アップしたMIMEの情報あるいはファイルの拡張子をDBに保存する必要あります。

まず、DBの設計から始めましょう。ここで必要なのは以下の2つのテーブル、

+------------+------------------+------+-----+---------+----------------+
| Field      | Type             | Null | Key | Default | Extra          |
+------------+------------------+------+-----+---------+----------------+
| product_id | int(10) unsigned | NO   | PRI | NULL    | auto_increment |
| name       | varchar(255)     | NO   |     | NULL    |                |
| created_at | timestamp        | YES  |     | NULL    |                |
| updated_at | timestamp        | YES  |     | NULL    |                |
+------------+------------------+------+-----+---------+----------------+
+------------------+------------------+------+-----+---------+----------------+
| Field            | Type             | Null | Key | Default | Extra          |
+------------------+------------------+------+-----+---------+----------------+
| product_image_id | int(10) unsigned | NO   | PRI | NULL    | auto_increment |
| product_id       | int(11)          | NO   | MUL | NULL    |                |
| mime             | varchar(255)     | NO   |     | NULL    |                |
| created_at       | timestamp        | YES  |     | NULL    |                |
| updated_at       | timestamp        | YES  |     | NULL    |                |
+------------------+------------------+------+-----+---------+----------------+

一応、productproduct_imageのテーブルの関係は、1対多の関係となります。つまり、商品1に対して複数の画像を持つことが可能。

migrationを作成して、以下のように作成してテーブルを作成します。

use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;

class CreateProductTable extends Migration
{
    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
		Schema::create('product', function (Blueprint $table) {
			$table->increments('product_id');
			$table->string('name');
			$table->timestamps();
		});
    }

    /**
     * Reverse the migrations.
     *
     * @return void
     */
    public function down()
    {
        Schema::drop('product');
    }
}

class CreateProductImageTable extends Migration
{
    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
		Schema::create('product_image', function (Blueprint $table) {
			$table->increments('product_image_id');
			$table->integer('product_id');
			$table->string('mime');
			$table->timestamps();
			$table->index('product_id');
		});
    }

    /**
     * Reverse the migrations.
     *
     * @return void
     */
    public function down()
    {
        Schema::drop('product_image');
    }
}

次は管理画面でのルートの作成。

...
Route::group(['prefix' => 'admin', 'middleware' => 'web'], function () {
    ...
    Route::get('product/{product}/image', 'Admin\ProductController@getImage');
    Route::post('product/{product}/image', 'Admin\ProductController@postImage');
    Route::resource('product', 'Admin\ProductController');
    ...
});
...

CRUD(Create, Read, Update, Delete)のオペレーションは、Route::resource('product', ..に任せて、画像に関しては、product/{product}/imageのURIを使用して、それらのメソッドはすべてProductControllerに収めます。今回は、前回と違って管理画面での作業のことに注意してください。

それらの定義は、

namespace App\Http\Controllers\Admin;

use Illuminate\Http\Request;

use App\Http\Requests;
use App\Http\Controllers\Controller;
use App\Product;
use App\ProductImage;

class ProductController extends Controller
{
	public function getImage(Product $product)
	{
		return view('admin/product_image', compact('product'));
	}

	public function postImage(Request $request, Product $product)
	{
		$file = $request->file('file');

		$image = new ProductImage;

		$image->product_id = $product->product_id;
		$image->mime = $file->getClientMimeType();

		$image->save();  // product_imageにレコードを作成

		$image->storeImage($file); // アップロードしたファイルを移動
	}

}

となり前回とほぼ同じようなコードです。しかし、前回と違って、product_imageのテーブルに、アップロードしたファイルのMIME情報を入れてレコードを作成しています。


namespace App;

use Illuminate\Database\Eloquent\Model;

class ProductImage extends Model
{
    protected $table = 'product_image';
    protected $primaryKey = 'product_image_id';
    public $incrementing = true;
    public $timestamps = true;

    protected $fillable = [
        'product_id', 'mime'
    ];

    public function storeImage($file)
    {
		$file->move(public_path('images/product'), $this->filename());
    }

    public function filename()
    {
    	$ext = 'jpg';

    	switch($this->mime)
    	{
    		case 'image/jpeg':
    		case 'image/jpg':
    			$ext = "jpg";
    			break;

  			case 'image/png':
  				$ext = "png";
  				break;

  			case 'image/gif':
  				$ext = "gif";
  				break;
    	}

    	return sprintf("%d.%s", $this->product_image_id, $ext);
    }
}

filenameでは、レコードで自動発行されたproduct_image_idとMIMEをもとにした拡張子を合わせて移動先のファイル名を作成します。

コントローラにより使用されるテンプレートも少し違います。

@extends('user.layouts.app')

@section('content')
<div class="container">
    <div class="row">
        <div class="col-md-8 col-md-offset-2">
            <div class="panel panel-default">
                <div class="panel-heading">File Upload</div>
                <div class="panel-body">
                    <div class="form-group{{ $errors->has('file') ? ' has-error' : '' }}">
                        <div>
                            <form
                                method="POST"
                                action="/demo/public/admin/product/{{ $product->product_id }}/image"
                                class="dropzone"
                                id="imageUpload"
                                enctype="multipart/form-data">
                                {{ csrf_field() }}
                            </form>
	                    </div>
                    </div>
                </div>
            </div>
        </div>
    </div>
</div>
@endsection

@section('script')

Dropzone.options.imageUpload = {
    dictDefaultMessage: 'アップロードするファイルをここへドロップしてください',
    maxFiles: 10,
    acceptedFiles: '.jpg,.jpeg,.gif,.png',
    maxFilesize: 5, // 5 MB
}
@endsection

ファイルのアップロードの結果は以下のようにレコードの生成となります。

+------------------+------------+------------+---------------------+---------------------+
| product_image_id | product_id | mime       | created_at          | updated_at          |
+------------------+------------+------------+---------------------+---------------------+
|                1 |          1 | image/jpeg | 2016-05-15 19:46:24 | 2016-05-15 19:46:24 |
|                2 |          1 | image/gif  | 2016-05-15 19:46:24 | 2016-05-15 19:46:24 |
|                3 |          1 | image/png  | 2016-05-15 19:46:24 | 2016-05-15 19:46:24 |
+------------------+------------+------------+---------------------+---------------------+

ファイルのアップロード(5)複数の画像ファイルをアップロード

Dropzone.jsを使用する利点は、ファイルアップロードの途中経過を表示するようになりUIが良くなるだけでありません。1画面で複数の画像ファイルを一度にアップすることができます。

もちろん、以下のように複数のファイルのアップロードは、Dropzone.jsを使用しなくても可能です。

<form
    method="POST"
    action="/demo/public/user/upload"
    class="form-horizontal"
    role="form"
    enctype="multipart/form-data">
    {{ csrf_field() }}
...
    <input type="file" name="file1">
    <input type="file" name="file2">
...
</form>

しかし、以下のポストで説明したように、

ファイルのサイズの制限・制限なしでも制限ある

ウェブサーバーによる制限により、1フォームでアップロードできるファイルの数は、すべてのファイルを足したファイルのサイズになります。

例えば、

post_max_size = 2M
upload_max_filesize = 1M

の設定なら、1フォームでは、最大1Mのファイルが2つまでしかアップロード可能でありません。

しかし、Dropzone.jsを使用すれば、ajaxを使用するので、それぞれのファイルアップロードは、1フォームで1つのファイルのアップロードとなり、この例では、1Mファイルをいくつでもアップロード可能となります。

そして、この複数のファイルのアップロードに対応するプログラムの変更といえば、

まず、フロントエンド、

Dropzone.jsでアップロード途中経過を表示

とまったく同じです。変更はありません。

アップロード時、複数のファイルを選択してドラッグするか、ファイルダイアログで複数のファイルを選択して実行すれば以下のようにアップされます。
multiple

バックエンドは、

namespace App\Http\Controllers\User;
 
use Illuminate\Http\Request;
 
use App\Http\Requests;
use App\Http\Controllers\Controller;
 
use Validator;
 
class UserController extends Controller
{
...
    public function getUpload()
    {
        return view('user/upload');
    }
 
    public function postUpload(Request $request)
    {
        $file = $request->file('file');
        $filename = $file->getClientOriginalName();
        $file->move(public_path('images'), $filename)
    }
}

と、アップしたファイル名で、/public/imagesのディレクトリに保存するように変更します。

同時にアップするファイル数を制限したいなら、

<script type="text/javascript">
       
Dropzone.options.imageUpload = {
    dictDefaultMessage: 'アップロードするファイルをここへドロップしてください',
    acceptedFiles: '.jpg, .jpeg',
    maxFilesize: 5 // 5MBまで
  maxFiles: 2 // ファイルは2つまでアップロード可能
}
</script>

と、Dropzone.jsの設定で、maxFilesを指定します。

only2

このように3番目のファイルは、アップロード不可となります。

ファイルのアップロード(4)バックエンド

前回のDropzone.jsを使用したファイルアップロードのフロントエンドに対して、サーバーサイドのバックエンドをLaravelでプログラムします。つまり、アップロードされたファイルを受け取るプログラムです。

まず、routes.phpにルートの追加となりますが、前回の設定を見てみますと、

<div class="form-group{{ $errors->has('file') ? ' has-error' : '' }}">
    <div>
        <form
             method="POST"
             action="/demo/public/user/upload"
             class="dropzone"
             id="imageUpload"
             enctype="multipart/form-data">
             {{ csrf_field() }}
         </form>
    </div>
</div>

<form>のパラメータのactionで指定している/demo/public/user/uploadがルートとなります。

となると、routes.phpでは、

...
Route::get('upload', 'User\UserController@getUpload');
Route::post('upload', 'User\UserController@postUpload');
...

のようになります。getの方は前回の画面を表示するとして、postは、サーバーでアップされるファイルの処理となります。

namespace App\Http\Controllers\User;

use Illuminate\Http\Request;

use App\Http\Requests;
use App\Http\Controllers\Controller;

use Validator;

class UserController extends Controller
{
...
	public function getUpload()
	{
		return view('user/upload');
	}

	public function postUpload(Request $request)
	{
		...
		$request->file('file')->move(public_path('images'), 'test.jpg');
	}
}

ファイルをアップロードすると、phpは、/tmpのディレクトリに一時的なファイルを作成します。上の29行目では、それをpublic/imagesのディレクトリにtest.jpgと命名してファイルを保存します。

最後に、上のpostUpload()の関数の最後において、通常あるredirect()文がないことに気づきましたか?
この関数は、Dropzone.jsによりajaxでコールされるので、画面を更新するためのredirect()は要らないのです。

ファイルのアップロード(3)Dropzone.jsでアップロード途中経過を表示

今まで話したファイルアップロードは、基本でもっとも簡単にプログラムできるものです。

しかし、アップロードするファイルのサイズが大きく、アップロードに時間がかかるようになると、送信ボタンを押してからじ~っと何も起きない画面を見ているのは、退屈でもありちょっと心配ですね。これ、ちゃんと動いているのかなと。

要は、「アップロード中です」とか「80%アップロード完了」したとか、しかもそれをビジュアルで伝えてくれれば最高です。それを行ってくれるのが今回紹介するツール、Dropzone.jsです。

upload1

「アップロードするファイルをここへドラッグしてください」にファイルをドラッグすると、

upload2

このように、アップロードの画像ファイルのサムナイルの中にアップロードの途中経過をバーで表示してくれます。

ファイル完了時は、

upload3

必要な設定は、

https://github.com/enyo/dropzone/archive/master.zip

よりダウンロードして解凍してから、assets/jsのディレクトリに入れて、画面のテンプレートを以下のように編集します。

<div class="form-group{{ $errors->has('file') ? ' has-error' : '' }}">
    <div>
        <form
             method="POST"
             action="/demo/public/user/upload"
             class="dropzone"
             id="imageUpload"
             enctype="multipart/form-data">
             {{ csrf_field() }}
         </form>
    </div>
</div>

気づきましたか、
<input type="file" name="file">

<input type="submit">
がないことに。

ちなみにデフォルトのファイル変数名はfileですが、設定変更できます。

そして、レイアウトのテンプレートを以下のように編集します。

...
<head>
...
<link href="{{ url('assets/css/dropzone/dropzone.min.css') }}" rel="stylesheet" type="text/css">
...
</head>
<body>
...
 
<script src="{{ url('assets/js/dropzone/dropzone.min.js') }}"></script>
 
<script type="text/javascript">
      
Dropzone.options.imageUpload = {
    dictDefaultMessage: 'アップロードするファイルをここへドロップしてください',
    acceptedFiles: '.jpg, .jpeg',
    maxFilesize: 5 // 5MBまで
}
</script>
...

上の設定では、ファイルをドロップする場所のメッセージ、アップロードを許すMIMEあるいは拡張子(jpegのみ)、最大のファイルのサイズ(5MBまで)としています。他にもいろいろな設定があります。以下を参照にしてください。

http://www.dropzonejs.com/#configuration

将来は、これらの設定を使用した複雑な例を紹介する予定です。

ファイルのアップロード(2)画像ファイルだけをアップ

ファイルのアップロードと言っても、なんのフォーマットのファイルでもアップをしてよいというわけではありません。たいていは画像ファイルのアップロードになりますが、それでもGIFはアップしてもらいたくない、JPEGだけが欲しいとか。

そのときは、バリデーションを使用してアップしたファイルのMIMEの情報をもとにプログラムで制限します。

まず、jpegだけのファイルをOKとしましょう。

...
class UserController extends Controller
{
 ...
    public function postUpload(Request $request)
    {
        $validator = Validator::make($request->all(), [
            'file' => 'required|max:10240|mimes:jpeg'
        ]);

        if ($validator->fails())
        {
            return back()->withInput()->withErrors($validator);
        }

        return redirect('user/upload');
    }
}

ここ、jpgでなくjpegであることに注意してください。ファイルの拡張子はたいてい、.jpgですが、ユーザーのマシンのOSが送信するMIMIEは、image/jpegです。

複数の画像フォーマットを許すなら、

...
class UserController extends Controller
{
 ...
    public function postUpload(Request $request)
    {
        $validator = Validator::make($request->all(), [
            'file' => 'required|max:10240|mimes:jpeg,gif,png'
        ]);

        if ($validator->fails())
        {
            return back()->withInput()->withErrors($validator);
        }

        return redirect('user/upload');
    }
}

と指定します。

ファイルのアップロード(1)ファイルのサイズの制限:制限なしでも制限ある

入力フォーム画面でのファイルのアップロードは、他のテキストの入力と違っていろいろなことを考慮する必要があり、開発はそう簡単ではありません。PHPのマニュアルでも、説明のためだけに一章を費やしています。

もちろん、Laravelを使うことで開発はかなり簡単になるのだけれど、注意する点やアップしたファイルをどう使用するか...などたくさんのトピックあります。以下、いくつか興味あるトピックをリストしました。

  • ファイルのサイズの制限:制限なしでも制限ある
  • ファイルのMIMEタイプの制限:画像ファイルだけをアップ
  • ファイルのアップの進行状況表示 :いつファイルのアップが終わるのかな
  • ファイルのウィルスの検出:そんなこと可能かな
  • アップしたファイルをメールで送信:届かないのはどうして
  • アップしたファイルをダウンロード:MIMEからファイルの拡張子を作成
  • アップしたファイルを配信:Amazonのウェブサービスを使用する

ファイルのサイズの制限:制限なしでも制限ある

まず、ファイルアップロードのフォームをプログラムしましょう。

<form
    method="POST"
    action="/demo/public/user/upload"
    class="form-horizontal"
    role="form"
    enctype="multipart/form-data">
    {{ csrf_field() }}

		<div class="form-group{{ $errors->has('file') ? ' has-error' : '' }}">
			<label class="col-md-4 control-label">File</label>

			<div class="col-md-6">
				<input type="file" name="file">
				@if ($errors->has('file'))
					<span class="help-block">
						<strong>{{ $errors->first('file') }}</strong>
					</span>
				@endif
			</div>
		</div>
		<div class="form-group">
			<div class="col-md-6 col-md-offset-4">
                    <input class="btn btn-primary" type="submit" value="Upload">
			</div>
		</div>
</form>

このフォームのコントローラで、アップするファイルの制限を課します。

namespace App\Http\Controllers\User;

use Illuminate\Http\Request;

use App\Http\Requests;
use App\Http\Controllers\Controller;
use App\User;

use Auth;
use Validator;

class UserController extends Controller
{
 ...
 
    public function getUpload()
    {
        return view('user/upload');
    }

    public function postUpload(Request $request)
    {
        $validator = Validator::make($request->all(), [
            'file' => 'required|max:10240'
        ]);

        if ($validator->fails())
        {
            return back()->withInput()->withErrors($validator);
        }

        return redirect('user/upload');

    }
}

'file' => 'required|max:10240'ここ大事です。maxで指定する最大のファイルのサイズの単位は、バイトではなくキロバイト(KByte)です。つまり、10240は。10メガバイト(10M)となります。

さて、これでファイルをアップロードしてみましょう。8MBのファイルをアップしてみます。しかし、送信ボタンを押した後、TokenMismatchExceptionのエラーとなってしまいます。

どうしてアップできないのでしょう?

制限しているのはプログラムだけではなく、サーバーのPHPの設定でも制限があるからなのです。Unix系のサーバーなら、/etc/php.iniにおいて以下の2か所を編集する必要あります。8M20Mとでも設定しましょう。

...
post_max_size = 8M
upload_max_filesize = 8M
...

これで、ウェブサーバーを再スタートしてみてください。

1つ残る疑問は、どうしてエラーがPHPの制限のエラーでなくTokenMismatchExceptionのエラーなのでしょう。これは、制限を超えることにより、送信した入力の値が空となり、Laravelが期待するCSRF(クロスサイトリクエストフォージェリ)のトークンの値が入って来なかったからです。

Top