1

I wrote a file download script in PHP which enables users to download any specified file from my server. The script is working pretty fine for .txt, .pdf and .jpg etc. files.

But when the user is trying to download any .mp3 or .mp4 files then though script downloads the files but the file is unusable. The file is very less in size as compared to my original file. The file can't be opened in any media player - it is corrupted.

I don't know what is wrong with my this download script. Here's the full code:

Interface Page (index.php):

<a href="download.php?file=tutorial.pdf">Download Tutorial (pdf)</a><br /><br />
<a href="download.php?file=music.mp3">Download Music (mp3)</a><br /><br />
<a href="download.php?file=video.mp4">Download Video (mp4)</a>

PHP Script (download.php):

<?php
if(isset($_GET['file']) && !empty($_GET['file']))
{
    $file = $_GET['file'];
    $file_without_spaces = str_replace(' ', "%20", $file);
    $path_parts = pathinfo($file);
    $file_name = $path_parts['basename'];
    $file_path = "files/" . $file_name;
    $file_extension = $path_parts['extension'];

    if(!is_readable($file_path))
    {
        die("File not found!");
    }

    // Figure out the correct MIME type
    $mime_types = array(
                        // Documents
                        "pdf" => "application/pdf",
                        "doc" => "application/msword",
                        "xls" => "application/vnd.ms-excel",
                        "ppt" => "application/vnd.ms-powerpoint",
                        "csv" => "application/csv",
                        "txt" => "text/plain",

                        // Archives
                        "zip" => "application/zip",

                        // Executables
                        "exe" => "application/octet-stream",

                        // Images
                        "jpg" => "image/jpeg",
                        "jpeg" => "image/jpeg",
                        "png" => "image/png",
                        "gif" => "image/gif",
                        "bmp" =>  "image/bmp",

                        // Audio
                        "mp3" => "audio/mpeg",
                        "wav" => "audio/x-wav",

                        // Video
                        "mpeg" => "video/mpeg",
                        "mpg" => "video/mpeg",
                        "mov" => "video/quicktime",
                        "avi" => "video/x-msvideo",
                        "mp4" => "video/mp4",
                        "3gp" => "video/3gpp"
                    );

    if(array_key_exists($file_extension, $mime_types))
    {
        $mime_type = $mime_types[$file_extension];
    }

    header("Content-type: " . $mime_type);
    header("Content-Disposition: attachment; filename=\"$file_without_spaces\"");
    readfile($file_path);
}
?>

I downloaded a big .mp4 file of size around 250 MB. It came out to be just 2 KB. I found out this error in my notepad:

Fatal error: Allowed memory size of...

Phil
  • 157,677
  • 23
  • 242
  • 245
Sachin
  • 1,646
  • 3
  • 22
  • 59
  • Have a look at the actual contents of the files downloaded. They might be plain text with a PHP error message inside – Phil Sep 14 '18 at 04:13
  • Sorry, can't. I downloaded a video file - not sure how to check the PHP error message inside. – Sachin Sep 14 '18 at 04:16
  • 1
    Just try opening it with a text editor – Phil Sep 14 '18 at 04:16
  • Also, I don't think you need to URL-encode the filename (ie, replace `' '` with `'%20'`) for the `Content-Disposition` header. I'd just use `filename=\"$file_name\"` – Phil Sep 14 '18 at 04:19
  • 1
    use specific header content type to mp4 and mp3 files... like below header("Content-Type: video/mp4"); header("Content-Length: ".filesize("path/to/mp4")); readfile("path/to/mp4"); header("Content-Type: audio/mp3"); header("Content-Length: ".filesize("path/to/mp3")); readfile("path/to/mp3"); – Akbar Soft Sep 14 '18 at 04:24
  • Sorry you used specific content types... sorry bro – Akbar Soft Sep 14 '18 at 04:25
  • @DejavuGuy good point about `Content-length` though – Phil Sep 14 '18 at 04:26
  • I started reading about the encoding of the `filename` parameter in `Content-disposition` and now I wish I hadn't; what a can of worms ~ https://stackoverflow.com/questions/93551/how-to-encode-the-filename-parameter-of-content-disposition-header-in-http – Phil Sep 14 '18 at 04:27
  • @Phil `I don't think you need to URL-encode the filename...` Yes you are right but I don't want any space in downloaded filename. Just that's why. I can leave `%20` line since I'm already using double quotes around my file name in header(). – Sachin Sep 14 '18 at 04:34
  • @DejavuGuy but I have already covered that in `$mime_types` – Sachin Sep 14 '18 at 04:36
  • @user5307298 I'm just trying to rule out possible file-name issues though it's probably not the problem. Have you had a look inside any of the corrupted files yet? – Phil Sep 14 '18 at 04:40
  • @Phil Yes I'm trying to look into it. The file was heavy - around 25 MB and it is taking time to open in notepad. – Sachin Sep 14 '18 at 04:41
  • 1
    did you set content-length ???? – Akbar Soft Sep 14 '18 at 04:42
  • @DejavuGuy good point, I didn't use it in my script – Sachin Sep 14 '18 at 04:43
  • @user5307298 oh, if it's that big, it's not an error message :) When you said _"The file is very less in size as compared to my original file"_. I thought you meant they were only a couple of kb. – Phil Sep 14 '18 at 04:43
  • Just noticed one more thing here; add `exit;` immediately after `readfile($file_path);` You could be adding some whitespace to the end of the file which may corrupt it. If that doesn't help, I'd start comparing file checksums. Perhaps your originals are corrupted on the PHP server – Phil Sep 14 '18 at 04:44
  • @Phil one of my video files was around 250 MB, but when I downloaded it, the file size came out to be just few MBs. No, the original files are not corrupted. I played all those without any issue at all. – Sachin Sep 14 '18 at 04:52
  • @Phil As I told you I downloaded a big .mp4 file of size around 250 MB. It came out to be just 2 KB. I found out this error in my notepad: `Fatal error: Allowed memory size of...` – Sachin Sep 14 '18 at 05:06
  • 1
    Guys, I resolved it! – Sachin Sep 14 '18 at 05:11
  • Ah right, so it is a PHP error. See the note here and turn off output buffering on your server ~ http://php.net/manual/function.readfile.php#refsect1-function.readfile-notes – Phil Sep 14 '18 at 05:12
  • 1
    @user5307298 could you please write up _how_ you solved it into the answer field below? There's nothing worse than looking for a solution to a problem and only finding a comment saying _"I solved it"_ – Phil Sep 14 '18 at 05:18
  • @Phil please see my answer – Sachin Sep 14 '18 at 05:50

2 Answers2

2

readfile() is a simple way to output small files. But for huge media files, it causes memory exhausting issue and output buffering. For more information, visit Why does readfile() exhaust PHP memory?. So to handle large file size, it is better to output the files in chunks. I added following code in my existing code to download big size files:

set_time_limit(0);
$file_handle = @fopen($file_path, "rb");
while(!feof($file_handle))
{
    print(@fread($file_handle, 1024 * 8));
    ob_flush();
    flush();
}
fclose($file_handle);

Now we no longer need readfile() in this snippet.

Sachin
  • 1,646
  • 3
  • 22
  • 59
  • Thanks for adding your solution but wouldn't it have been simpler to disable output buffering? – Phil Sep 14 '18 at 06:16
  • I'm doing the same. I can't turn off output buffering by editing in php.ini file. – Sachin Sep 14 '18 at 07:41
0

Until OP adds their own answer, I'm going to have a crack at this...

Fatal error: Allowed memory size of...

There's a very specific note in the PHP manual page for readfile(). It says...

readfile() will not present any memory issues, even when sending large files, on its own. If you encounter an out of memory error ensure that output buffering is off with ob_get_level()

I suspect this is the issue.

To disable output buffering, make sure the output_buffering runtime configuration is set to 0.


Some extra improvements for this particular question are...

  1. Prevent any accidental whitespace after the file by using exit

    readfile($file_path);
    exit;
    
  2. Rather than replace only spaces with %20, URL-encode the entire file basename

    header(sprintf('Content-Disposition: attachment; filename="%s"',
            urlencode($file_name));
    
Phil
  • 157,677
  • 23
  • 242
  • 245