An MIT licensed code snippet which, allows you to force a download of a file, Give that download file a name, Limit the speed of the download in KB/s and allow resumable downloads for users.
<?php
/*
* -------------------------------------------------------
* forceDownload
* -------------------------------------------------------
* @Version: 1.0.0
* @Author: FireDart
* @Link: http://firedartstudios.com/
* @GitHub: https://github.com/FireDart/snippets/Downloads
* @License: The MIT License (MIT)
*
* Allows you to:
* + Force a download of a file
* + Give that download file a name
* + Limit the speed of the download in KB/s
* + Allow resumable downloads
*
* -------------------------------------------------------
* WARNING
* -------------------------------------------------------
* MAKE SURE YOU SANATIZE DOWNLOAD PATHS IF IT IS A USER
* INPUT, OTHERWISE ATTACKS CAN GAIN ACCESS TO YOUR INTERAL
* FILE SYSTEM.
*
* -------------------------------------------------------
* Recommendations
* -------------------------------------------------------
* This script does provide the ability to limit the users
* download rate, however, it is suggested that you use an
* alternative in apache2, nginx, Lighttpd or what other
* software you are using to host your site.
*
* apache2 (No built in feature use mod_bw)
* - http://bwmod.sourceforge.net/
* - sudo apt-get install libapache2-mod-bw
* - sudo a2enmod bw
* - Guide to install:
* - http://freedif.org/bandwidth-restriction-how-to-limit-the-download-speed-for-your-visitors/
* nginx
* - http://wiki.nginx.org/NginxHttpCoreModule#limit_rate
* Lighttpd
* - http://redmine.lighttpd.net/projects/lighttpd/wiki/Docs_TrafficShaping#Selective-traffic-shaping-plugin15-svn
*
* -------------------------------------------------------
* Requirements
* -------------------------------------------------------
* PHP 5.3.0+
*
* -------------------------------------------------------
* Usage
* -------------------------------------------------------
* Basic
* forceDownload("path/to/file.ext");
*
* All features
* forceDownload("path/to/file.ext", "myFileName.ext", 1000);
*
*/
/*
* forceDownload
*
* Forces the page to download the specified file
*
* @param str $download The path to the file you want to force a download
* @param str $name The name of the file the user will see when he downloads
* @param int $speed The speed in kb/s of the download, (ex. 1000KB/s = 1MB/s)
* @return mixed
*/
function forceDownload($download, $name = null, $speed = null) {
try {
// Try and disable gzip compression for IE
if(ini_get('zlib.output_compression')) {
ini_set('zlib.output_compression', 'Off');
}
// Can the file been found?
if(!file_exists($download) && !is_file($download)) {
throw new Exception('Unable to find specified download, are you point to a valid file?');
}
// Get a name for our file
if($name == null) {
$name = basename($download);
}
// Get file extension
// This is split apart to follow PHP strict standard
$extension = explode('.', $download);
$extension = end($extension);
$extension = strtolower($extension);
// Security check
$blacklist = array("php", "ini", "conf");
if(in_array($extension, $blacklist)) {
throw new Exception('Sorry but this type of file is blacklisted and can\'t be downloaded unless the administrator white lists it.');
}
// Check that the provided $name is the same file type
// This is split apart to follow PHP strict standard
$nameExten = explode('.', $name);
$nameExten = end($nameExten);
$nameExten = strtolower($nameExten);
if($nameExten != $extension) {
throw new Exception('The name of the file you supplied does not match the original download extension.');
}
// Get file size
$fileSize = filesize($download);
// Content-Types
$ctype = "application/octet-stream";
// List of specific ctypes, add more if can
$ctypes = array(
// General
"txt" => "text/plain",
"htm" => "text/html",
"html" => "text/html",
"css" => "text/css",
"js" => "application/javascript",
"json" => "application/json",
"xml" => "application/xml",
// Archive
"zip" => "application/zip",
"rar" => "application/x-rar-compressed",
"7z" => "application/x-7z-compressed",
"exe" => "application/octet-stream",
"msi" => "application/x-msdownload",
"cab" => "application/vnd.ms-cab-compressed",
// Images
"jpeg" => "image/jpg",
"jpg" => "image/jpg",
"png" => "image/png",
"gif" => "image/gif",
"webp" => "image/webp",
"bmp" => "image/bmp",
"ico" => "image/vnd.microsoft.icon",
"tiff" => "image/tiff",
"tif" => "image/tiff",
"svg" => "image/svg+xml",
"svgz" => "image/svg+xml",
// Audio
"mp3" => "audio/mpeg",
"flac" => "audio/x-flac",
"ogg" => "audio/ogg",
"wma" => "audio/x-ms-wma",
// Video
"mp4" => "video/mp4",
"mkv" => "video/x-matroska, audio/x-matroska",
"webm" => "video/webm, audio/webm",
"ogv" => "video/ogv",
"wmv" => "video/x-ms-wmv",
"mpg" => "video/mpeg",
"avi" => "video/x-msvideo",
// Adobe
"pdf" => "application/pdf",
"psd" => "image/vnd.adobe.photoshop",
"ai" => "application/postscript",
"eps" => "application/postscript",
"ps" => "application/postscript",
// MS Office
"doc" => "application/msword",
"rtf" => "application/rtf",
"xls" => "application/vnd.ms-excel",
"ppt" => "application/vnd.ms-powerpoint",
// LibreOffice
"odt" => "application/vnd.oasis.opendocument.text",
"ods" => "application/vnd.oasis.opendocument.spreadsheet",
);
// Use specific Content-Type if we can
if(isset($ctypes[$extension])) {
$ctype = $ctypes[$extension];
}
// We got everything we need, send headers & start file download
// We will be sending chunks for more reliable transfer & to allow resumable downloads
$file = fopen($download, "rb"); // rb, b is for Windows but might as well use it on Linux
if(isset($speed) && is_int($speed)) {
// We make the script sleep for a 1 second below for every chunk
$chunk = 1024 * $speed;
} else {
// 8KB so we don't kill the server, this will go as fast as the script executes (not per second)
$chunk = 1024 * 8;
}
// Start processing file
if(!$file) {
throw new Exception('Could not open file.');
} else {
// Set headers
header('Content-Description: File Transfer');
// Size
if(isset($_SERVER['HTTP_RANGE'])) {
file_put_contents("logs.txt", $_SERVER['HTTP_RANGE'] . "\n", FILE_APPEND);
// Learn about range:
// http://tools.ietf.org/id/draft-ietf-http-range-retrieval-00.txt range1, range2/total
list($param, $range) = explode('=', $_SERVER['HTTP_RANGE']); // bytes=0-1024,123-124/total
// Bad request - range unit is not 'bytes'
if(strtolower(trim($param)) != 'bytes') {
header("HTTP/1.1 400 Invalid Request");
exit;
}
// Split ranges
$range = explode(',', $range);
// We only deal with the first requested range
$range = explode('-', $range[0]);
// Bad request - 'bytes' parameter is not valid
if(count($range) != 2) {
header("HTTP/1.1 400 Invalid Request");
exit;
}
// Check range for valid values
if(empty($range[0])) {
$offset = 0;
} else {
$offset = $range[0];
}
if(empty($range[1])) {
$end = $fileSize - 1;
} else {
$end = $range[1];
}
if($offset > 0 || $end < ($filesize - 1)) {
header("HTTP/1.1 206 Partial Content");
}
// Allow resumable downloads
header("Accept-Ranges: bytes");
header("Content-Range: bytes $offset-$end/$fileSize");
} else {
$offset = 0;
}
header("Content-Length: " . $fileSize);
// IE 6 fix
header("Pragma: public");
header("Cache-Control: public, must-revalidate, post-check=0, pre-check=0");
// Prevent Caching
header("Expires: -1");
// Type
header("Content-Type: " . $ctype);
// Attach file
header("Content-Disposition: attachment; filename=\"$name\"");
// Do we need to start at a specific amount?
fseek($file, $offset);
// Begin download
ob_start();
while(!feof($file)) {
echo fread($file, $chunk);
flush();
ob_flush();
// If a speed limit is set, limit every 1 second
if(isset($speed)) {
sleep(1);
}
}
fclose($file);
exit;
}
// Catch all errors and report back
} catch(Exception $e) {
echo $e->getMessage();
}
}







