I'm using Slim 4. I am trying to download a file, but when I click on the link, instead of the file downloading, the browser window loads a lot of gibberish.
Project structure
app (Controller is in here)
public (public site is here)
routes
uploads (file to download is here)
vendor
views
Route:
$app->get('/adm/download/', DownloadController::class . ':download')->setName('adm.download');
Controller:
public function download($request, $res, $args) {
$file = "/home/acct/public_html/project/uploads/application/ourfile.jpg";
$response = $res->withHeader('Content-Description', 'File Transfer')
->withHeader('Content-Type', 'application/octet-stream')
->withHeader('Content-Disposition', 'attachment;filename="'.basename($file).'"')
->withHeader('Content-Transfer-Encoding', 'binary')
->withHeader('Expires', '0')
->withHeader('Cache-Control', 'must-revalidate, post-check=0, pre-check=0')
->withHeader('Pragma', 'public')
->withHeader('Content-Length', filesize($file));
ob_clean();
flush();
readfile($file);
return $response;
}
View:
<a href="{{ url_for('adm.download') }}">Download file</a>
I have tried removing ob_clean() and flush() and moving it to different positions. Nothing works. The file is never downloaded.
The gibberish you describe is most likely the raw JPEG file, because it's a binary picture format and that's what binary looks like when you try to display it as text. But, why is browser trying to display it as text, if you set application/octet-stream
and binary
, and why is it displaying it in line rather than downloading, if you set attachment
?
Because you add headers to $response
but you never populate it with file contents. Instead, you call readfile() to write file into output buffer. So, unless you have an active output buffer, this will happen:
readfile()
starts emitting response body, so it prints builtin and previously set headers, then appends file contents.\Slim\ResponseEmitter::emit()
and ignores your headers altogether, because doing otherwise will trigger a Cannot modify header information - headers already sent by PHP warning.If you are using Slim, you need to use it all the way through. The most straightforward way to is to populate response body:
$response
->getBody()
->write(file_get_contents($file));
But there's a dedicated method for files:
$streamFactory = new \Slim\Psr7\Factory\StreamFactory();
$response = $res->withHeader('Content-Description', 'File Transfer')
->withHeader('Content-Type', 'application/octet-stream')
->withHeader('Content-Disposition', 'attachment;filename="' . basename($file) . '"')
->withHeader('Content-Transfer-Encoding', 'binary')
->withHeader('Expires', '0')
->withHeader('Cache-Control', 'must-revalidate, post-check=0, pre-check=0')
->withHeader('Pragma', 'public')
->withHeader('Content-Length', filesize($file))
->withBody($streamFactory->createStreamFromFile($file));
return $response;