Howto: Stream downloadable files using PHP and tell browser to save it instead of opening

download prompt using PHPJust a few days back, we were having some discussion at DigitalPoints forum about sharing and streaming download links via PHP. The problem was that if we have say a doc file and if we link that file directly then some browser may attempt to download it, or some will just show it! So, what we will learn today is how to stream a file using PHP, instead of direct linking and force the browser to download it with a custom name defined by us! So lets get into the topic…

#0: Understanding the file structure:

What we will do is create a file download.php and will pass the file to download through that php file! As we know, passing some information can be done via http GET or http POST method. We can use anything we like! But here, we will use GET method to make the download links bookmarkable! Here is the structure of the folder containing our download script and files to download…

php file structure for download.php

So what we have here are:

  • index.php a file from where we would link to the downloadable contents… Obviously through the download.php file.
  • download.php the main file for streaming downloadable contents.
  • files [folder]: A folder where other files are located which would be streamed!

Pretty much a simple structure! Lets see how we would link them together! But before, here is a working demo and the download package link!

Demo[download id=”5″ format=”1″]

#1: Understanding the logic behind the system:

  • We will pass some URL Variable to the download.php file which would tell the script which file to stream.
  • The download.php file will then read the specified file.
  • It will somehow tell the browser to download it and our browser will prompt a dialogue box to save the file.

That’s it! With the help of PHP’s header function and file pointers we can easily achieve the above algorithm… Lets see how we can do this.

#2: Linking the download file first:

As said before, we have to pass the location of the file we want to download. From the directory structure, it is clear that all the files will be inside the files directory, separated into sub directories according to their type! We will just pass the subdirectory name and the file name to the download.php file via a GET parameter file. So, the URL of a downloadable file will be something like this:

<a href="http://yoursite/download.php?file=docs/mydoc.doc">Download MyDoc</a>

Pretty simple right? Basically it passes the file location through the file parameter! The value can be anything according to your need! So, if, say you have a file information.pdf inside a sub-directory pdfs then you would link to it like this:

<a href="http://yoursite/download.php?file=pdfs/information.pdf">Download Information PDF</a>

So, we are now clear upto this!

#3: The code behind the download.php file:

Now the interesting part! Here is how you code the download.php file!

<?php
/**
 * Download Prompt for any file using PHP Header Function
 *
 * @author Swashata <swashata4u@gmail.com>
 * @link https://www.intechgrity.com/?p=537
 * @license GPLv2 or Higher
 *
 */
// Name of the directory where all the sub directories and files exists
$file_directory = 'files';
// Get the file from URL variable
$file = @$_GET['file'];
// No request parameter set?
if ( empty( $file ) ) {
	// Set response code
	http_response_code( 400 ); // Bad Request
	exit( 'Invalid Request' );
}
// Try to seperate the folders and filename from the path
$file_array = explode( DIRECTORY_SEPARATOR, $file );
// Count the result
$file_array_count = count( $file_array );
// Trace the filename
$filename = basename( $file_array[ $file_array_count - 1 ] );
// Set the file path w.r.t the download.php...
// It could be different for you
$file_path = dirname( __FILE__ ) . DIRECTORY_SEPARATOR . $file_directory . DIRECTORY_SEPARATOR . $file;
// Sanitize and check for valid path
// Prevent directory traverse attacks
// We whitelist this path only
$valid_directory = dirname( __FILE__ ) . DIRECTORY_SEPARATOR . $file_directory;
// Let us see the actual path of the file being requested
// Attacker could use ../ to perform a directory traverse attack
$actual_path = realpath( $file_path );
// Calculate the filepath from the actual path
$calculated_file_path = substr( $actual_path, strlen( $valid_directory . DIRECTORY_SEPARATOR ) );
// Make it compatible with Windows
$calculated_file_path = str_replace( array( '/', '\\' ), array( DIRECTORY_SEPARATOR, DIRECTORY_SEPARATOR ), $calculated_file_path );
$file = str_replace( array( '/', '\\' ), array( DIRECTORY_SEPARATOR, DIRECTORY_SEPARATOR ), $file );
// Check if the request file is within valid directory
if ( $file != $calculated_file_path || ! file_exists( $file_path ) || is_dir( $file_path ) ) {
	// Error
	http_response_code( 404 );
	exit( 'The request URL was not found.' );
} else {
	// Tell the filename to the browser
	header( "Content-disposition: attachment; filename={$filename}" );
	// Stream as a binary file! So it would force browser to download
	header( 'Content-type: application/octet-stream' );
	// Read and stream the file
	readfile( $file_path );
}

This snippet has been updated on 2nd of March, 2017 to fix the directory traverse attack.

I have commented the code for better understanding! But are a few tips:

  • $file_directory = “files” The main directory where all the files are stored. You can change it if you like.
  • $file_path = dirname(__FILE__).’/’.$file_directory.’/’.$file; It sets the absolute path of the file location! We can also work with relative path, but still it is a better approach to use absolute! Read more about dirname function here…
  • if(file_exists($file_path)) Checks to see if the file exists before streaming! If not exists, then simply outputs the error!
  • header(“Content-disposition: attachment; filename={$filename}”) Sets the filename of the download! You can set it to anything or just use the original filename
  • readfile($file_path); Reads the file and writes it to the output buffer! This actually does the magic of sending a doc file from a php file ๐Ÿ˜‰

And thats it! Dont forget to view the demo and download the source code…

Demo[download id=”5″ format=”1″]

I hope it was useful for you! Do give your feedback. If you face any problem, feel free to ask us!

16 comments

  1. Edwin

    Great tutorial Swashata, I really like the detailed explanation you provided.

    One really good functionality that comes to mind immediately with your download script is that you can use it to track how many times a file is downloaded.

    It’s also the same technique my estores use where their digital content is stored in a directory which is not directly accessible from a browser and once a customer has finished payment, the content is ‘streamed’ and is allowed to be downloaded.

    Can I repost your article on little handy tips and link it to this page? The information here will be very useful for the readers!

    Cheers,

    Edwin

    • Swashata Post author

      Sure you can Edwin! Thanks for dropping in ๐Ÿ™‚ And the count is possible if we are using some database to store the information! We will discuss that on another article!

      PS: I have fixed the link to your site! You left a typo there ๐Ÿ˜‰

  2. daryll

    thanks dude ..it is the easy tutorial that i been search
    and you explained it clearly..

  3. Milan

    will it also work when there is a large number of data in the file to be download?

    Because I have used the similar way that you have mentioned here.

    Thats working fine for all the cases. But it gets failed in downloading the file containing the large number of data.

    So is there any possible solution to avoid reading the file ?

    Thanks,
    Milan

    • Swashata Post author

      I don’t think so! For larger files the best way is, indeed direct download link of the file itself.

  4. Rob

    If I don’t want to save the file, but I would to write in on the webserver (example a .csv file from the information retrieved) what would be the right way ?

    Thanks

  5. P.Boru

    What is a realistic maximum filesize that could be transferred with this approach?

    • Swashata Post author

      That should alteast be 64MB less than the maximum memory you allocate to php. If you have 256MB memory then probably 150-180MB of files.

    • Swashata Post author

      You can do it like ?file=anything. Then in the server, you can store the file path for the query “anything” within an array or database.

  6. Hung Chun Fai Franky

    Sadly this script would be vulnerable to Directory traversal Attack

Comments are closed.