Skip to content

Instantly share code, notes, and snippets.

@fsantini
Created July 19, 2013 12:31
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save fsantini/6038802 to your computer and use it in GitHub Desktop.
Save fsantini/6038802 to your computer and use it in GitHub Desktop.
This page can be used as a "proxy" file download page that counts the downloads of every file storing them in a MySQL/MariaDB database.
<?php
/* File download counter.
* This code is copyright of Francesco Santini <francesco.santini _at_ gmail.com> and can be used and distributed freely,
* provided that you cite the author, according to the Creative Commons 3.0 - Attribution (cc-by) license.
*
* This page can be used as a "proxy" file download page that counts the downloads of every file storing them in a MySQL/MariaDB database.
* Whenever you want to let the user download a specific file, say "files/arch.tgz", use the link download.php?path=files/arch.tgz.
* This can be made transparent to the user by using the .htaccess file (see below)
* To read the statistics, call the page with the parameters show=true and your password, like this: download.php?show=true&pass=page_password.
*
* Initialization and configuration
* 1) place the download.php script in an accessible folder of your website (for example the root folder)
* 2) create a table in your MySQL database with the following SQL template (Replace the table name if needed):
CREATE TABLE IF NOT EXISTS `Downloads` (
`Path` varchar(255) NOT NULL,
`Count` int(11) NOT NULL,
`LastDownload` timestamp NOT NULL default CURRENT_TIMESTAMP on update CURRENT_TIMESTAMP,
UNIQUE KEY `Path` (`Path`)
) ENGINE=MyISAM DEFAULT CHARSET=latin1;
* 3) Modify the options below in the "config" section:
* - the $db* variables contain the database connection details
* - the $password variable contains a password to access the statistics (don't use a sensitive one, it will be transmitted plaintext when you display the stats!)
* - the $allowed_paths array contains a list of ABSOLUTE paths where download is possible. The paths must be in the form as returned by the PHP function realpath().
* 4) [OPTIONAL] Create a .htaccess file for transparent download. If your http server supports it, you can use mod_rewrite to make the download through the script transparent.
* For example, I have the files that can be downloaded in the files/ folder of my server. I have this .htaccess file in the root folder of my server:
RewriteEngine On
RewriteRule ^files/(.*?)$ download.php?path=files/$1
* This rule transparently translates http://www.francescosantini.com/files/something.ext into http://www.francescosantini.com/donwload.php?path=files/something.ext
*/
/*
* Part of this code is based on: http://www.richnetapps.com/php-download-script-with-resume-option/ Following the original copyright notice
* Copyright 2012 Armand Niculescu - MediaDivision.com
* Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
* 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
* THIS SOFTWARE IS PROVIDED BY THE FREEBSD PROJECT &quot;AS IS&quot; AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE FREEBSD PROJECT OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
// config
$password='page_password';
$dbName = 'mySqlDbName';
$dbHost = 'mySqlDbHost';
$dbUser = 'mySqlUser';
$dbPass = 'mySqlPass';
$dbTable = 'Downloads';
$allowed_paths = array(
'/web/htdocs/mysite/home/files/',
'/some/other/path/'
);
// functionality
function isAllowed($file)
{
global $allowed_paths;
$path = realpath($file);
foreach ($allowed_paths as $allowed_path)
{
// check if the real path starts with the allowed_path
if (stripos($path, $allowed_path) === 0)
{
return true;
}
}
return false;
}
function getMime($file)
{
if (function_exists('mime_content_type'))
return mime_content_type($file);
elseif (class_exists('finfo'))
{
$finfo = new finfo(FILEINFO_MIME);
return $finfo->file($file);
} else
return "application/octet-stream";
}
function show_status($path)
{
global $dbName, $dbHost, $dbUser, $dbPass;
$db = new mysqli($dbHost, $dbUser, $dbPass, $dbName);
$query="SELECT * FROM $dbTable";
if ($path != '')
$query = $query . " WHERE Path='$path'";
$results = $db->query($query);
// HTML part
?>
<html><head><meta charset='utf-8'/><title>Download stats</title>
<style>
td
{
border: 1px solid #AAAAAA;
border-collapse: collapse;
padding: 5px;
}
table
{
border: 1px solid black;
text-align: center;
margin:auto;
}
</style>
</head>
<h1 style="text-align: center">Download stats</h1>
<table>
<tr style="font-weight: bold;">
<td>Path</td><td>Count</td><td>Last download</td>
</tr>
<?php
while ($row=$results->fetch_array()): ?>
<tr>
<td><?= htmlentities($row['Path']) ?></td>
<td><?= $row['Count'] ?></td>
<td><?= $row['LastDownload'] ?></td>
</tr>
<?php endwhile ?>
</table>
</body>
</html>
<?php
$results->close();
$db->close();
}
function send_file($path)
{
$mm_type=getMime($path); // modify accordingly to the file type of $path, but in most cases no need to do so
$is_attachment = isset($_REQUEST['stream']) ? false : true;
$file_size = filesize($path);
$file = @fopen($path,"rb");
if (!$file)
{
// file couldn't be opened
header("HTTP/1.0 500 Internal Server Error");
exit;
}
header("Pragma: public");
header("Expires: 0");
header("Cache-Control: must-revalidate, post-check=0, pre-check=0");
header("Cache-Control: public");
header("Content-Description: File Transfer");
header("Content-Type: " . $mm_type);
header("Content-Length: " .(string)($file_size) );
// set appropriate headers for attachment or streamed file
if ($is_attachment)
header('Content-Disposition: attachment; filename="'.basename($path).'"');
else
header('Content-Disposition: inline;');
header("Content-Transfer-Encoding: binary\n");
//check if http_range is sent by browser (or download manager)
if(isset($_SERVER['HTTP_RANGE']))
{
list($size_unit, $range_orig) = explode('=', $_SERVER['HTTP_RANGE'], 2);
if ($size_unit == 'bytes')
{
//multiple ranges could be specified at the same time, but for simplicity only serve the first range
//http://tools.ietf.org/id/draft-ietf-http-range-retrieval-00.txt
list($range, $extra_ranges) = explode(',', $range_orig, 2);
}
else
{
$range = '';
header('HTTP/1.1 416 Requested Range Not Satisfiable');
exit;
}
}
else
{
$range = '';
}
//figure out download piece from range (if set)
list($seek_start, $seek_end) = explode('-', $range, 2);
//set start and end based on range (if set), else set defaults
//also check for invalid ranges.
$seek_end = (empty($seek_end)) ? ($file_size - 1) : min(abs(intval($seek_end)),($file_size - 1));
$seek_start = (empty($seek_start) || $seek_end < abs(intval($seek_start))) ? 0 : max(abs(intval($seek_start)),0);
//Only send partial content header if downloading a piece of the file (IE workaround)
if ($seek_start > 0 || $seek_end < ($file_size - 1))
{
header('HTTP/1.1 206 Partial Content');
header('Content-Range: bytes '.$seek_start.'-'.$seek_end.'/'.$file_size);
header('Content-Length: '.($seek_end - $seek_start + 1));
}
else
header("Content-Length: $file_size");
header('Accept-Ranges: bytes');
set_time_limit(0);
fseek($file, $seek_start);
// output divided in chunks for efficiency
while(!feof($file))
{
print(@fread($file, 1024*8));
ob_flush();
flush();
if (connection_status()!=0)
{
@fclose($file);
exit;
}
}
// file save was a success
@fclose($file);
exit;
/*
readfile($path); // outputs the content of the file
exit();*/
}
function send_404()
{
header('HTTP/1.0 404 Not Found'); ?>
<html><head><meta charset='utf-8'/><title>File not found!</title></head>
<body>
<H1>File not found!</H1>
</body>
</html>
<?php
}
if (isset($_REQUEST['show']))
{
if (!isset($_REQUEST['pass']) || strcmp($_REQUEST['pass'], $password) != 0)
{
exit();
}
if (isset($_REQUEST['path']))
$path=$_REQUEST['path'];
else
$path='';
show_status($path);
exit();
}
if (!isset($_REQUEST['path']))
{
exit();
}
$path = $_REQUEST['path'];
if (!file_exists($path) || !isAllowed($path))
{
send_404();
exit();
}
$db = new mysqli($dbHost, $dbUser, $dbPass, $dbName);
// if something goes wrong, just give the file
if ($db->connect_errno)
{
send_file($path);
}
$result = $db->query("SELECT Count FROM $dbTable WHERE Path='$path'");
if (!$result)
{
// new record
$db->query("INSERT INTO $dbTable VALUES('$path', 0, NULL)");
} else
{
$row = $result->fetch_row();
$count=$row[0]+1;
$result->close();
$db->query("REPLACE INTO $dbTable VALUES('$path', $count, NULL)");
}
$db->close();
send_file($path);
?>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment