Press "Enter" to skip to content

Making PHP Serve File Downloads Faster

Generally, we can lead users to download a file by directly pointing the URL at a file under the Document Root.
But doing this means we can't do things like statistics, permission checks, and so on. So, in many cases, we use PHP to do the forwarding and provide the file download for users.

<?php
    $file = "/tmp/dummy.tar.gz";
    header("Content-type: application/octet-stream");
    header('Content-Disposition: attachment; filename="' . basename($file) . '"');
    header("Content-Length: ". filesize($file));
    readfile($file);

But this has one problem: if the file has a Chinese name, some users may end up with a garbled filename after downloading.
So, let's make a modification (reference: :

<?php
    $file = "/tmp/中文名.tar.gz";
    $filename = basename($file);
    header("Content-type: application/octet-stream");
    //handle the Chinese filename
    $ua = $_SERVER["HTTP_USER_AGENT"];
    $encoded_filename = rawurlencode($filename);
    if (preg_match("/MSIE/", $ua)) {
	header('Content-Disposition: attachment; filename="' . $encoded_filename . '"');
    } else if (preg_match("/Firefox/", $ua)) {
	header("Content-Disposition: attachment; filename*=\"utf8''" . $filename . '"');
    } else {
	header('Content-Disposition: attachment; filename="' . $filename . '"');
    }
    header("Content-Length: ". filesize($file));
    readfile($file);

Hmm, that looks a lot better now. But there's still one problem, which is readfile. Although PHP's readfile tries to be as efficient as possible and not occupy PHP's own memory, in practice it still has to use MMAP (if supported), or read the file in a loop with a fixed buffer and output it directly.
When outputting, if it's Apache + PHP mod, it still needs to be sent to Apache's output buffer, and only then to the user. And for Nginx + fpm, if they're deployed separately, it even brings extra network IO.
So, can we bypass the PHP layer and let the web server send the file directly to the user?
Today, I saw an interesting article: How I PHP: X-SendFile.
We can use Apache's module mod_xsendfile to let Apache send the file directly to the user:

<?php
    $file = "/tmp/中文名.tar.gz";
    $filename = basename($file);
    header("Content-type: application/octet-stream");
    //handle the Chinese filename
    $ua = $_SERVER["HTTP_USER_AGENT"];
    $encoded_filename = rawurlencode($filename);
    if (preg_match("/MSIE/", $ua)) {
	header('Content-Disposition: attachment; filename="' . $encoded_filename . '"');
    } else if (preg_match("/Firefox/", $ua)) {
	header("Content-Disposition: attachment; filename*=\"utf8''" . $filename . '"');
    } else {
	header('Content-Disposition: attachment; filename="' . $filename . '"');
    }
    //let Xsendfile send the file
    header("X-Sendfile: $file");

The X-Sendfile header will be handled by Apache, and it will send the corresponding file directly to the client.
Lighttpd and Nginx also have similar modules — feel free to go look for them if you're interested 🙂

Be First to Comment

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.