Just 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…
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!
#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…
I hope it was useful for you! Do give your feedback. If you face any problem, feel free to ask us!
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
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 ๐
thanks dude ..it is the easy tutorial that i been search
and you explained it clearly..
This is really helpful for internet marketing consulting. Thank you for sharing!
Hi interesting code, but is possible link the files from a database? thank you
I love you! You’re a life saver!
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
I don’t think so! For larger files the best way is, indeed direct download link of the file itself.
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
I think this tutorial might help you http://www.intechgrity.com/automatically-copy-images-png-jpeg-gif-from-remote-server-http-to-your-local-server-using-php/
What is a realistic maximum filesize that could be transferred with this approach?
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.
this is the hard code Download Information PDF, how can I pass the string value here instead of file=pdfs/information.pdf?
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.
Sadly this script would be vulnerable to Directory traversal Attack
Thank you. I have updated the snippet to fix it.