If you benefit from web2py hope you feel encouraged to pay it forward by contributing back to society in whatever form you choose!

Introduction: download function

By default, download() function does some permission checks (if they are set) and sends the uploaded files to the client.

The problem: download prevents client side caching

The problem is that download() function sends the following http headers, that prevents client side caching:

  • Expires: Thu, 27 May 2010 05:06:44 GMT
  • Pragma: no-cache
  • Cache-Control: no-store, no-cache, must-revalidate, post-check=0, pre-check=0

This may be good for some dynamic content, but, for example, a client browsing a site with several non static images, will see how each image loads every time the page is shown, slowing down navigation.

@Caching download

Caching download() with @cache will not help, it prevents permission checks to be made, and cache is done at server side, we need some at client side.

One Solution: fast_download

A best approach will be using a custom download function, allowing client side caching, so once the browser downloaded a file, that file doesn't need to be downloaded again soon.

This can be done with a custom download function (ie. fast_download), doing simple security checks and modifying http headers to favor client side caching:

  • Last-Modified: Tue, 04 May 2010 19:41:16 GMT
  • Expires removed
  • Pragma removed
  • Cache-control removed

Using Last-Modified allows If-Modified-Since http requests, whose speed up downloads preventing sending again unmodified contents. response.stream handles if-modified-since and range requests automatically.

Other Solution: let webserver handle downloads

This aproach need some webserver (apache) config files tweaking, so is not portable nor easily configurable. Using fast_download should be a better alternative, that requires no custom configuration and works quickly with almost all webservers, and the user would not notice any performance difference.

fast_download code

So, in controller, default.py add:

def fast_download():
   # very basic security (only allow fast_download on your_table.upload_field):
   if not request.args(0).startswith("yourtable.upload_field"):
       return download()
   # remove/add headers that prevent/favors client-side caching
   del response.headers['Cache-Control']
   del response.headers['Pragma']
   del response.headers['Expires']
   filename = os.path.join(request.folder,'uploads',request.args(0))
   # send last modified date/time so client browser can enable client-side caching
   response.headers['Last-Modified'] = time.strftime("%a, %d %b %Y %H:%M:%S +0000", time.localtime(os.path.getmtime(filename)))
   return response.stream(open(filename,'rb'))

In your view, remember to make URLs using fast_download instead of download function:

URL(r=request, c='default', f='fast_download', args=your_table.upload_field)

Thanks massimo for the advice and comments, for the full thread see: http://groups.google.com/group/web2py/browse_thread/thread/0c10235cb16c476f/0f2dd71668626b6d?show_docid=0f2dd71668626b6d&fwc=1

Related slices

Comments (4)

  • Login to post



  • 0
    thadeusb 14 years ago
    Don't forget x-sendfile as noted here -> http://thadeusb.com/weblog/view/let_the_web_server_stream_your_files_not_responsedownload

  • 0
    reingart 14 years ago
    x-sendfile seems to be a non-standard feature, supported by some servers (ie, not included for apache under debian lenny by now). Anyway, I think the problem is not how fast you transfer the data, but how often you send the file. This slice tries to send the file once, thus improving navigation speed (not transfer speed). In many occasions, this may be enough. I couldn't test if x-sendfile favors client side caching (modifying headers accordingly) Maybe this slice should be called 'smart_download' instead of 'fast_download' :-)

  • 0
    thadeusb 14 years ago
    x-sendfile handles cache controls if you configure the server correctly. apache has a module you can install it comes with cherokee, and nginx

  • 0
    select 14 years ago
    very nice, i inserted the code and on the second load the images were just there however I wonder if I view the image directly I will just see garbage characters is this due to a missing mimetype? couldnt that be guessed with the mimetype module and inserted into the header?

Hosting graciously provided by:
Python Anywhere