I'm using Typo3 with extbase and fluid. I have a Controller with an action called downloadAction()
. After calling the action the system tries to render the download-template (but I just want to start a download).
public function downloadAction($id) {
// create file
// send header
// dump file
// exit
}
How can I dump the created download-File
and send a download header instead of the normal render process? What is the best way?
Thanks
I did this in a project some months ago, it's pretty straight forward.
/**
* @param string $fileName
* @return void
*/
public function downloadAction($fileName) {
$file = $this->settings['uploadFolder'] . 'uploadedPhotos/' . $fileName;
if(is_file($file)) {
$fileLen = filesize($file);
$ext = strtolower(substr(strrchr($fileName, '.'), 1));
switch($ext) {
case 'txt':
$cType = 'text/plain';
break;
case 'pdf':
$cType = 'application/pdf';
break;
case 'exe':
$cType = 'application/octet-stream';
break;
case 'zip':
$cType = 'application/zip';
break;
case 'doc':
$cType = 'application/msword';
break;
case 'xls':
$cType = 'application/vnd.ms-excel';
break;
case 'ppt':
$cType = 'application/vnd.ms-powerpoint';
break;
case 'gif':
$cType = 'image/gif';
break;
case 'png':
$cType = 'image/png';
break;
case 'jpeg':
case 'jpg':
$cType = 'image/jpg';
break;
case 'mp3':
$cType = 'audio/mpeg';
break;
case 'wav':
$cType = 'audio/x-wav';
break;
case 'mpeg':
case 'mpg':
case 'mpe':
$cType = 'video/mpeg';
break;
case 'mov':
$cType = 'video/quicktime';
break;
case 'avi':
$cType = 'video/x-msvideo';
break;
//forbidden filetypes
case 'inc':
case 'conf':
case 'sql':
case 'cgi':
case 'htaccess':
case 'php':
case 'php3':
case 'php4':
case 'php5':
exit;
default:
$cType = 'application/force-download';
break;
}
$headers = array(
'Pragma' => 'public',
'Expires' => 0,
'Cache-Control' => 'must-revalidate, post-check=0, pre-check=0',
'Content-Description' => 'File Transfer',
'Content-Type' => $cType,
'Content-Disposition' => 'attachment; filename="'. $fileName .'"',
'Content-Transfer-Encoding' => 'binary',
'Content-Length' => $fileLen
);
foreach($headers as $header => $data)
$this->response->setHeader($header, $data);
$this->response->sendHeaders();
@readfile($file);
}
exit;
}
You can define a special PageType for download requests:
download = PAGE
download {
typeNum = 1249058993
10 < tt_content.list.20.efempty_pi1
config {
disableAllHeaderCode = 1
xhtml_cleaning = 0
admPanel = 0
additionalHeaders = Content-type:application/octet-stream
}
}
}
"efempty" must be replaced by your extension's name.
You are now able to trigger the download like this:
<f:link.action pageType="1249058993" action="download" controller="ControllerName">Download</f:link.action>
Update:
This actually doesn't work with activated compression because the Content-Length will still represent the uncompressed size. eID could be used as workaround in fact there is even a official call for that:
eID = "dumpFile" see typo3/sysext/core/Resource/PHP/FileDumpEID.php
However this call won't force a download but instead "dump" it. I've made a ticket @ Forge to fix this (which was accepted and is targeted for LTS 7): https://forge.typo3.org/issues/67111
Old answer:
As for now the easiest way to accomplish this is:
TypoScript Constants
# ASCII "download" in Numbers (4-15-23-14-12-15-1-4) - See http://rumkin.com/tools/cipher/numbers.php
plugin.tx_extensionname.view.formatToPageTypeMapping.download = 4152314121514
TypoScript Setup
tx_extensionname_download = PAGE
tx_extensionname_download {
typeNum < plugin.tx_extensionname.view.formatToPageTypeMapping.download
config {
disableAllHeaderCode = 1
xhtml_cleaning = 0
admPanel = 0
debug = 0
no_cache = 1
}
10 = USER
10 {
userFunc = TYPO3\CMS\Extbase\Core\Bootstrap->run
extensionName = ExtensionName
pluginName = PluginName
vendorName = VENDOR
controller = ControllerName
action = download
view < plugin.tx_extensionname.view
persistence < plugin.tx_extensionname.persistence
settings < plugin.tx_extensionname.settings
}
}
Controller Action
/**
* Download
*
* @param \VENDOR\ExtensionName\Domain\Model\Model $model
* @return void
* @ignorevalidation $model
*/
public function downloadAction($model) {
if ($model->getFile()) {
$model->getFile()->getOriginalResource()->getOriginalFile()->getStorage()->dumpFileContents(
$model->getFile()->getOriginalResource(),
$asDownload = TRUE,
$alternativeFilename = $model->getFile()->getOriginalResource()->getName()
);
exit;
}
$this->throwStatus(
$statusCode = 404,
$statusMessage = 'Not Found'
);
}
Fluid
<f:link.action controller="ControllerName" pluginName="PluginName" action="download" arguments="{model: '{model.uid}'}" format="download" title="{model.file.originalResource.title}">Download</f:link.action>
The benefits vs the other mentioned answers are as following:
- Proper headers (including mime/type)
- Extbase formats instead typenum
Side note: iPhone ignores the Content-Disposition headers (always inline)
About the "exit" concerns i've not tested it but with the page type it might work if you would use a own PHPView (StandaloneView?).
How can I dump the created download-File and send a download header instead of the normal render process?
As long as headers haven't already been sent, there is nothing at all stopping you from emitting your headers, sending the content and actually calling exit
, though you might want to check for output buffering and clear any open buffers before doing so. You might also want to inspect the list of headers that will be sent.