Delete a file after download in Laravel 5

A while back I wrote about downloading a file in Laravel and then removing the file. This prevents us from having to clean up the storage directory full of files with a cronjob. I recently had this issue come back up at work and now we are on Laravel 5. Let’s revisit this issue.

The problem with deleting a file is that we cannot do it until after our client has downloaded the entire file. Furthermore what if we want to download a file and then refresh the page? This is not possible with server side. We might use a plugin like $.fileDownload() to help with this. But there is another way without the need for javascript.

class MyController
{
	public function index()
	{
		return view('my.index');
	}

	public function download()
	{
		$file = $this->createSomeReport();

		return Response::download($file);
	}
}

What ends up happening on the above code is a file is generally downloaded on the page and no refresh occurs. This is because browsers are smart enough to pick up on file content-type HTTP headers. So how do we get our page to refresh without javascript? We have to create the file on the server, then refresh the page. Then download the file.

	public function download()
	{
		$filepath = $this->saveReportTo(storage_path() . '/reports');

		Session::flash('file.download', $filepath);

		return Redirect::back();
	}

Then inside resources/views/layouts/application.blade.php we could do this.

<html>
  <head>
     @if (Session::has('file.download'))
       <meta http-equiv="refresh" content="0;url={{ route('file-download') }}">
     @endif
  </head>
  <body>
  	@yield('content')
  </body>
</html>

And the my.blade.php might look like follows

extends('layouts.application')

@section('content')
	<a href="my/download">Download file now</a>
@stop

Now let’s take a look at the routes for this application.

Route::get('my', 'MyController@index');

Route::post('my/download', 'MyController@download');

Route::get('file-download', ['as' => 'file-download', function() {
	return Response::download(Session::get('file.download'));
}]);

Another problem is when we create a file, we may want to remove it. This is typically the case when generating custom reports for users. We don’t want to keep these reports lingering around to fillup our disk space. To do this in laravel 5 we can use terminable middleware. It would look something like this…

use Closure;
use Illuminate\Contracts\Routing\TerminableMiddleware;

class RemoveDownloadedFileMiddleware implements TerminableMiddleware {

    public function handle($request, Closure $next)
    {
        return $next($request);
    }

    public function terminate($request, $response)
    {
    	$file = Session::get('file.download', null);

    	if ($file) unlink($file);
    }
}

We should put our file-download route inside of a group to include this middleware termination.

Route::group(['middleware' => ['RemoveDownloadedFileMiddleware'], function() {
	Route::get('file-download', ['as' => 'file-download', function() {
		return Response::download(Session::get('file.download'));
	}]);
});

That should about cover it. Now after the user downloads the file, it will disapear forever. We could do other things besides unlink the file in the middleware though. We might cache it. In fact, we could skip this entire process and make a cache remover that removes files older than 30 days anytime a new file is downloaded. There’s always more than one way to skin a chicken.

post by K.D. on 05/29/2015