<a href="http://myvirtualhost.localhost/uploads/flower.jpeg" download> Nice flower</a>
Where:
- the virtual host directory is pointing to ...www\symfony_prj\web
- "uploads" directory is found under Symfony's "web" directory
In this approach anyone can access the files. In many situations this is not the desired behavior.
Serving protected files with Symfony2
First we should let Apache know that access to the files should be blocked, so in the "uploads" file I will add a .htaccess file with just one row:
deny from allIf you try again to access the link you get an error that your browser cannot find it.
If I would use plain PHP the solution for serving the files could be the one described here http://php.net/manual/en/function.readfile.php
<?php
$file = 'monkey.gif';
if (file_exists($file)) {
header('Content-Description: File Transfer');
header('Content-Type: application/octet-stream');
header('Content-Disposition: attachment; filename="'.basename($file).'"');
header('Expires: 0');
header('Cache-Control: must-revalidate');
header('Pragma: public');
header('Content-Length: ' . filesize($file));
readfile($file);
exit;
}
But I am using Symfony, and I have access to the HttpFoundation component:The HttpFoundation component defines an object-oriented layer for the HTTP specification.The official documentation about serving files can be found here:
http://symfony.com/doc/current/components/http_foundation/introduction.html#serving-files
One of the options described is using a BinaryFileResponse.
The link from our page will not point direct to the file, instead will be a regular Symfony route to a controller.
The controller looks like this:
/** * Serve a file * * @Route("/download/{id}", name="file_download", requirements={"id": "\d+"}) * @Method("GET") */ public function downloadFileAction(Request $request, File $file) { /* * $basePath can be either exposed (typically inside web/) * or "internal" */ $filename= $file->getName(); $basePath = $this->container->getParameter('my_upload_dir'); $filePath = $basePath.'/'.$filename; // check if file exists $fs = new FileSystem(); if (!$fs->exists($filePath)) { throw $this->createNotFoundException(); } // prepare BinaryFileResponse $response = new BinaryFileResponse($filePath); $response->trustXSendfileTypeHeader(); $response->setContentDisposition( ResponseHeaderBag::DISPOSITION_ATTACHMENT, $filename, iconv('UTF-8', 'ASCII//TRANSLIT', $filename) ); return $response; }
I have a File class mapped with Doctrine, Symfony is smart enough to transform the route "id" parameter to the actual File object instance. In the File object only the name of the file is saved, and it can be retrieved with the method getName().
The function "setContentDisposition" can receive as parameter the constant:
ResponseHeaderBag::DISPOSITION_ATTACHMENT and is asking for download, or you can force download with ResponseHeaderBag::DISPOSITION_INLINE
This article is inspired from the above mentioned documentation links and from this blog post: http://symfonybricks.com/en/brick/how-to-force-file-download-from-controller-using-binaryfileresponse