How I PHP: X-SendFile

by Arnold Daniels on 03/7/2008

It’s time for a new article in the ‘How I PHP’ series, which are about methods I use, but aren’t commonly known. This time it is about an Apache module ‘mod_xsendfile’.

Normally when want to let a user download a file, you simply stick it in a dir under the document root and let Apache do the rest.

However in some cases that is not good enough. You might need to do some authenticate first or you need to lookup the actual file name. In that case you would use PHP, which would result in a script looking like this:

1
2
3
4
5
6
7
8
9
authenticate(); # authenticate and authorize, redirect/exit if failed
$file = determine_file();
 
if (!file_exists($file)) trigger_error("File '$file' doesn't exist.", E_USER_ERROR);
 
header("Content-type: application/octet-stream");
header('Content-Disposition: attachment; filename="' . basename($file) . '"');
header("Content-Length: ". filesize($file));
readfile($file);
authenticate(); # authenticate and authorize, redirect/exit if failed
$file = determine_file();

if (!file_exists($file)) trigger_error("File '$file' doesn't exist.", E_USER_ERROR);

header("Content-type: application/octet-stream");
header('Content-Disposition: attachment; filename="' . basename($file) . '"');
header("Content-Length: ". filesize($file));
readfile($file);

This means PHP has to read in the file, which goes through the output buffer, is flushed to Apache and processed before send to client. In this small I didn’t specify any other headers Apache normally sends like last-modified. If I want to actually make the caching based on last-modified work, I need to check the if-modified-since request header, check the mtime of the file and send a 304 result header. (I’m to lazy right now to write a code example for that, sorry)

Wouldn’t it be nicer to tell Apache, please send that file, and be done with it. Well, you can. When you enable mod_xsendfile in Apache, you can send an X-SendFile header, which is processed by Apache.

1
2
3
4
5
6
authenticate(); # authenticate and authorize, redirect/exit if failed
$file = determine_file();
 
header("X-Sendfile: $somefile");
header("Content-type: application/octet-stream");
header('Content-Disposition: attachment; filename="' . basename($file) . '"');
authenticate(); # authenticate and authorize, redirect/exit if failed
$file = determine_file();

header("X-Sendfile: $somefile");
header("Content-type: application/octet-stream");
header('Content-Disposition: attachment; filename="' . basename($file) . '"');

Note that this technique was copied from Lighttp, so if you’re using that it will also work.

Arnold Daniels

I've spend a big part of my life behind a computer, learning about databases (MySQL), programming (PHP) and system administration (Linux). Currently I playing with HTML5, jquery and node.js.

More Posts

Follow Me:
TwitterLinkedIn

There are 27 comments in this article:

  1. 8 March 2008Pavel says:

    Hi, would it be too much hassle for you to post some (memory usage, cpu usage and speed) benchmarks? Thanks in advance!

    ReplyReply
  2. 5 March 2009hese says:

    Hello,

    I have been trying to install and get it working with no succees…

    I have test.php:

    $file = “video.mp4″;
    header(“X-Sendfile: “. $file);
    header(“content-type: video/mp4″);

    and .htaccess file in same directory that contains:

    XSendFile on

    I’m using flowplayer (www.flowplayer.org) to read the mp4 file trough test.php and flowplayer gives an error “streamnotfound”.
    If i use readfile($file) in php-script. it works…

    apache2ctl -t -D DUMP_MODULES shows that xsendfile_module is installed.

    Do you have any ideas what would be wrong?

    I really really appreciate your help!

    ReplyReply
  3. 7 April 2009Fujiko Fujio says:

    Is there a way to use xsendfile for displaying inline images? I want to display the image in a browser instead of showing the save as dialog box.

    ReplyReply
  4. 7 April 2009Arnold Daniels says:

    Hi Fujiko,

    For a png, use: header(”Content-type: image/png″);

    ReplyReply
  5. 16 April 2009Xorax says:

    does someone has compiled this mod for apache 2.2 ?

    I tried but I have bad errors….

    ReplyReply
  6. 1 May 2009pharrington says:

    Xorax:

    what errors did you get when trying to install? remember that with apache2, the program to compile modules is

    apxs2

    instead of

    apxs

    also, if for whatever reason you need to manually create the config file (eg Ubuntu), the LoadModule line is:

    LoadModule xsendfile_module /usr/lib/apache2/modules/mod_xsendfile.so (substitute for the correct path if necessary)

    ReplyReply
  7. 29 May 2009TopFX says:

    I’m also trying to get this to work but cannot get it to install on an Ubuntu Plesk server, it fails to compile.

    The server is running Apache2, but if you do a locate search it only finds apxs and not apxs2 as pharrington suggests it should.

    I really need to get this working so any ideas would really be appreciated

    ReplyReply
  8. 30 May 2009Arnold Daniels says:

    On http://packages.ubuntu.com you can find which package to install for a specific executable or other file.

    http://packages.ubuntu.com/search?searchon=contents&keywords=apxs2&mode=exactfilename&suite=jaunty&arch=any

    If you’re running PHP as module, install ‘apache2-prefork-dev’.

    ReplyReply
  9. 16 June 2009Fran says:

    downloads got stalled at getting file information at least with IE6.0 (not tested with other IE releases)

    suggested hack : Cache-Control: must-revalidate doesn’t work,

    any help?

    thanks

    ReplyReply
  10. 30 October 2009tom says:

    so one of the downfalls i’ve found with this method is that, unlike with the stock PHP header methods, when you have more code after the xsend file code, it doesn’t wait until the filesend is done before executing the code… with the first example it waits until the filesend is done, but not with xsend. this is particularly useful if you need to log the fact that the user had a successful download. if the download failed then the code below the download would just not execute, so the user could download again.

    what i’m getting at– is there a way around this with xsendfile? i’m looking for a way to mark that a user has successfully downloaded a complete file in the database, so they can’t download again. as of now using the same code with the xsendfile modifications, it just always marks the file download as successful because of this.

    ReplyReply
  11. 8 January 2011Moe says:

    Hello,

    The code below works on my localhost to download a file. However, on top of my downloaded file there are 160 lines of HTML tags. How can I eliminate these tags and just get the plain file?
    I appreciate your advice.

    $fd = basename($csv);
    header(“Content-type: application/octet-stream”);
    header(“Content-Disposition: attachment; filename=\”".$fd.”\”");
    header(“Content-Description: Download”);
    readfile($csv);

    ReplyReply
  12. 9 January 2011Arnold Daniels says:

    You’re PHP framework is probably adding that. On top of you file add ob_start();. Than before $fd=.. add ob_end_clean();.

    I also advise to use x-sendfile instead of readfile(), if your webhost supports it.

    ReplyReply
  13. 2 March 2011Sima Kosmos says:

    Has anyone tried something like this? More of apache conf/tweaking approach but, feel free to criticize

    1.Create symlink to physical file including some hash of session id in symlink name or do database pairing of symlink-physical_file-session
    2.Redirect to that symlink (was that Options FollowSymLinks? in conf)
    3.Cron does removal of symbolic links that are not named like present sessions/database keys from time to time or the ones older than max_download_availability.

    Maybe do calc of clients average download speed to estimate max download time, and schedule deletion.

    The only question is what will apache send as file name? Symlink name or the physical file name? The second being preferable :)

    It is security by obscurity! But would it work?

    ReplyReply
  14. 2 March 2011Arnold Daniels says:

    That could work. You don’t need to do database pairing, just create a random filename, symlink and redirect to that.

    However I don’t see why you would prefer that ‘poor-man’ solution above x-senfile.

    ReplyReply
  15. 2 May 2012使用XSendfile提供文件下载 | 风雪之隅 says:

    [...] 今天, 我看到了一个有意思的文章: How I PHP: X-SendFile. [...]

  16. 2 May 2012php文件下载 | DevOps says:

    [...] 今天, 我看到了一个有意思的文章: How I PHP: X-SendFile. [...]

  17. 11 May 2012让PHP更快的提供文件下载 | 花 生 says:

    [...] 直接让Webserver直接把文件发送给用户呢? 今天, 我看到了一个有意思的文章: How I PHP: X-SendFile. 我们可以使用Apache的module mod_xsendfile, [...]

  18. 20 May 2012让PHP更快的提供文件下载 | 乱炖 says:

    [...] 今天, 我看到了一个有意思的文章: How I PHP: X-SendFile. [...]

  19. 5 June 2012让PHP更快的提供文件下载(转) | 南门有只猫 | Blog says:

    [...] 今天, 我看到了一个有意思的文章: How I PHP: X-SendFile. [...]

  20. 6 June 2012让PHP更快的提供文件下载 | Haython's Blog says:

    [...] 今天, 我看到了一个有意思的文章: How I PHP: X-SendFile. [...]

  21. 10 July 2012KIVU » Blog Archive » 不经过PHP这层, 直接让Webserver直接把文件发送给用户 says:

    [...] 今天, 我看到了一个有意思的文章: How I PHP: X-SendFile. [...]

  22. 22 July 2012Matej says:

    Thanks a milion, dude! :-)

    ReplyReply
  23. 13 June 2013让PHP更快的提供文件下载 | 爱分享 says:

    [...] 今天, 我看到了一个有意思的文章: How I PHP: X-SendFile. [...]

  24. 31 October 2013Apache2借助X-Sendfile实现文件下载控制 | grace says:

    […] How I PHP: X-SendFile […]

  25. 12 January 2014让PHP更快的提供文件下载 | TengBin's Blog says:

    […] 今天, 我看到了一个有意思的文章: How I PHP: X-SendFile. […]

  26. 14 January 2014如何让PHP文件下载效率更高 | Nice团队官方站点 says:

    […] 今天, 我看到了一个有意思的文章: How I PHP: X-SendFile. […]

  27. 15 January 2014Karl says:

    Thanks for this! It’s exactly what I needed. I added some implementation advice, for serving files in-line using php’s finfo (if you’re using PHP, obviously) to determine MIME type. Most other frameworks should be able to do something analogous.

    //figure out what kind of file it is
    $finfo = finfo_open(FILEINFO_MIME_TYPE);
    $finfo_mime = finfo_file($finfo, $file_abs_path);

    //serve file

    header(‘X-Sendfile: ‘ . $file_abs_path);
    header(‘Content-type: ‘ . $finfo_mime );

    Also, I noticed a small security issue you might want to note: If you _don’t_ configure apache to use mod_xsendfile for your script and your script sets it, you might leak absolute file paths thorough the header (since Apache wouldn’t be stripping it). Not a huge deal, but something to consider when implementing this.

    ReplyReply

Write a comment:


7 + = thirteen