File->download() usage

Hi,

I am trying to create a template which shows all available files in a list’ with download links.
file->download() immediately downloads all the files, so I understand that I need to trigger the download with routing.
But I don’t have a clue how this is done.

Unfortunately the example in the docs is not exactly what I need: http://getkirby.com/docs/cheatsheet/file/download

Maybe someone can point me in the right direction?

Best,
Chris

1 Like

The method file->download() does only trigger the download of one file. So you have to trigger multiple downloads.

This solution came to my mind:

  • add a special route to e.g. sitename/load-all instead of sitename/latest of the example
  • trigger a download for each file:
c::set('routes', array(
  array(
    'pattern' => 'sitename/load-all',
    'action'  => function() {

      $files = page('sitename')
                ->files()
                ->filterBy('extension', 'zip');
      foreach($files as $f) {
        $f->download();
      }
    }
  )
));

I have not tested this code. It is only the first thing that came to my mind.

By the way. A normal download of a single file can be done with a simple link. But I understood your question, that you want another link for all files, right?

1 Like

Hey,
thanks for your reply!

Sorry, I may have not been clear about what I want to achieve: I indeed want to download single files with a link. When I directly link to the file with $file->url() the browser doesn’t get all the headers to force a download. Instead the browser may try to open the file in a browser window (depending on the mime type).

I know that there are different techniques described how to force a download with php but since the functionality obviously is built into kirby, I’d love to go this route.

But your example of triggering a download for all files may come in handy one day or another.

Best,
Chris

The problem is that the browser displays some files rather than adding them to the download queue.

Perhaps it is possible to add a route which:

  • catches all links below your site
  • gets the uid (last part)
  • searches for a filename same as the uid
  • triggers a download via php.
    I think this would only be possible if there are no file extensions in the requested url catched by the route and because of this there shouldn´t be files with the same name and other file extension.
    But this would only be easy if there are no subpages. Otherwise there must be a little bit more code.

Is that what you want?

Then it would be something like this:

c::set('routes', array(
  array(
    'pattern' => 'sitename/(:any)',
    'action'  => function($uid) {
      $page('sitename')->files->findBy('name', $uid)->download();
    }
  )
));

(untested code)

Or add some other pattern which should direct to your files.
Or do some javascript/ jQuery which is executed when clicking on the download link, which sends a request to the server with the filename/ path it wants to download. This would be more universal, but requires sone user side scripting. Take a look here: http://getkirby.com/docs/cheatsheet/helpers/get

I’m not sure that’s the best approach. Firstly, have you tested your code sample that prompts all files to be downloaded? I would have thought that after sending the first lot of headers the code would throw an error as you try to send a second round of header information. Is that not the case?

The difficulty is that unless it’s undocumented, files don’t have a native uri, so can’t be found simply. While I may be misunderstanding the $site->file($filename) function, it seems to only find those within the /content folder.

The simplest approach seems to be to send the page uri within the route, and the file name separately. For example, in your template:

<?php foreach($page->files() as $file) : ?>
    <a href="download/<?php echo $page->uri() ?>?file=<?php echo $file->filename() ?>"><?php echo $file->name() ?></a>
<?php endforeach; ?>

Your route could then be the following:

c::set('routes', array(
    array(
        'pattern' => 'download/(:all)',
        'action'  => function($uid) {
            $file = page($uid)->file(get('file'));
            $file->download();
        }
    )
));

Also untested. You would also want to add some check and better 404 handling to this code.

Thank you, that worked perfect with one big problem: it seems that only smaller files are being downloaded.
When trying to download a file with around 350MB the request takes a while to finally throw this error in console: Failed to load resource: Frame load interrupted

I tested other files up to 100MB which all work fine. What can cause this?

You might also consider the HTML5 download attribute if you are generating a list of links to download files. It prevents the browser from opening the clicked file as a separate browser tab and you can specify a more user friendly name for each download file.

<a href="/files/examplefilename.pdf" download="example.pdf">Download File</a>

I made something like a download section for attached documents using this in my template (without any other code):

<!-- a list of documents which are attached to the task if there are some -->
<?php if($page->documents()->count() >= 1): ?>
<div class="uk-width-small-1-1 uk-width-medium-1-2 uk-width-large-1-2">
	<h3 class="space-top">angehängte Dokumente</h3>
	<!-- here could be something to alert, because the field is empty -->
	<ul class="files">
		<!-- this shows the pdfs, which are attached -->
		<?php foreach($page->documents()->filterBy('extension', 'pdf') as $pdf): ?>
			<li class="pdf">
				<a href="<?php echo $pptx->url() ?>">
					<?php echo $pptx->filename() ?> (<?php echo $pptx->niceSize() ?>)
				</a>
			</li>
		<?php endforeach ?>
	</ul>
</div>
<?php endif ?>

But you are just linking to the PDFs with $file->url() which may lead the browser to open the PDF directly instead of forcing a download, right?

Yeah, this may work but is the HTML5 download attribute already implemented in all browsers? I think the php-route is more safe. If only it would work for larger files :frowning:

depends on your filetype. this is not limited to pdfs :wink:

you can watch it live here: http://schulcomputer.org/powerpoint/schritt-4-die-praesentation-erstellen

True, this not only works for PDFs. I am trying to link to video files. These are generally opened in a browser window, though.

You could try this https://css-tricks.com/snippets/htaccess/force-files-to-download-not-open-in-browser/ and combine both things :wink:

or http://99webtools.com/blog/php-force-file-download/

I guess one thing I didn’t bring up in my previous response was to ask why it is you want to force a download… If the goal is to view the document then it’s no bad thing to simply be opened, and people who want to save it can right click to achieve that, or save it once it’s open in the browser. You’re removing functionality rather than adding it.

However, using application/octet-stream is indeed the way you would need to go about changing this behaviour. You may need to change the headers kirby is sending with download() though too, so this may not be a magic bullet fix.

I want to make the download template foolproof for my clients that don’t know about right-click → download as…

I’ll give this a shot, thanks for the link:

I’m still curious why my current code won’t work for files above a certain file size.

Hi

I think the PHP force download method fails on large files because (and I don’t fully understand this) in order to force the download, PHP effectively loads the file then pushes it out, so you will be hitting a memory limit at some point and the process fails.

Mike