-
-
Save aznoisib/20c541c8e6b855cb7664cd60758fa80b to your computer and use it in GitHub Desktop.
UTF-8 file download function, support remote file passing(proxy) with http range header
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
/** | |
* PHP File download function. | |
* Version 1.4 | |
* | |
* Copyright (c) 2014 성기진 Kijin Sung | |
* Modified by ssut | |
* | |
* License: MIT License (a.k.a. X11 License) | |
* http://www.olis.or.kr/ossw/license/license/detail.do?lid=1006 | |
* | |
* This function supports following features: | |
* | |
* 1. Do not break UTF-8 character. (RFC2231/5987 Standards and considering browsers) | |
* 2. Remove special character when include characters that cannot used by OS. | |
* 3. Add Cache-Control and Expires header when you want to enable cache. | |
* 4. Fix the download error when use cache and IE <= 8. | |
* 5. Resumable download. (Auto detect Range header and auto generate Accept-Ranges header) | |
* 6. Fix the memory leak when download large file. | |
* 7. Support speed limitation | |
* | |
* How to : send_attachment('filename that provide to client', 'file path or remote url', [period of cache], [speed limit]); | |
* | |
* below, download 'foo.jpg' file from server to client named '사진.jpg'. | |
* send_attachment('사진.jpg', '/srv/www/files/uploads/foo.jpg'); | |
* | |
* below, download 'bar.mp3' file from server with 24-hour caching and 300KB/s limit. | |
* send_attachment('bar.mp3', '/srv/www/files/uploads/bar.mp3', 60 * 60 * 24, 300); | |
* | |
* Return : true when successfully sent else false. | |
* | |
* Caution : 1. please execute 'exit' when end the transfer. (file can be broken) | |
* 2. don't ensure that php version is very low(< 5.1) or not UTF-8 environment. | |
* 3. speed limitation is very dangerous when you using FastCGI/FPM. | |
* recommend to use web server's speed limitation. | |
* 4. some android versions does not support UTF-8 encoding. | |
*/ | |
function send_attachment($filename, $server_filename, $expires = 0, $speed_limit = 0) { | |
$remote = false; | |
// check filename | |
if (strpos($server_filename, 'http') === false) { | |
if (!file_exists($server_filename) || !is_readable($server_filename)) { | |
return false; | |
} | |
if (($filesize = filesize($server_filename)) == 0) { | |
return false; | |
} | |
if (($fp = @fopen($server_filename, 'rb')) === false) { | |
return false; | |
} | |
} else { | |
$remote = true; | |
$handle = curl_init($server_filename); | |
curl_setopt($handle, CURLOPT_RETURNTRANSFER, true); | |
curl_setopt($handle, CURLOPT_FOLLOWLOCATION, true); | |
curl_setopt($handle, CURLOPT_HEADER, true); | |
curl_setopt($handle, CURLOPT_NOBODY, true); | |
$response = curl_exec($handle); | |
$http_code = curl_getinfo($handle, CURLINFO_HTTP_CODE); | |
if ($http_code == 404) { | |
return false; | |
} | |
$filesize = curl_getinfo($handle, CURLINFO_CONTENT_LENGTH_DOWNLOAD); | |
curl_close($handle); | |
$ch = curl_init($server_filename); | |
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); | |
curl_setopt($ch, CURLOPT_BINARYTRANSFER, true); | |
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true); | |
} | |
$pass_remote = function($ch, $chunk) { | |
echo $chunk; flush(); | |
return strlen($chunk); | |
}; | |
// replace special characters | |
$illegal = array('\\', '/', '<', '>', '{', '}', ':', ';', '|', '"', '~', '`', '@', '#', '$', '%', '^', '&', '*', '?'); | |
$replace = array('', '', '(', ')', '(', ')', '_', ',', '_', '', '_', '\'', '_', '_', '_', '_', '_', '_', '', ''); | |
$filename = str_replace($illegal, $replace, $filename); | |
$filename = preg_replace('/([\\x00-\\x1f\\x7f\\xff]+)/', '', $filename); | |
// replace special spaces to normal spaces(0x20). | |
$filename = trim(preg_replace('/[\\pZ\\pC]+/u', ' ', $filename)); | |
// remove duplicates or dots. | |
$filename = trim($filename, ' .-_'); | |
$filename = preg_replace('/__+/', '_', $filename); | |
if ($filename === '') { | |
return false; | |
} | |
// get User-Agent from browser | |
$ua = isset($_SERVER['HTTP_USER_AGENT']) ? $_SERVER['HTTP_USER_AGENT'] : ''; | |
$old_ie = (bool)preg_match('#MSIE [3-8]\.#', $ua); | |
// add filename to header when filename only includes normal characters. | |
if (preg_match('/^[a-zA-Z0-9_.-]+$/', $filename)) { | |
$header = 'filename="' . $filename . '"'; | |
} | |
// < IE 9 or < FF 5 | |
elseif ($old_ie || preg_match('#Firefox/(\d+)\.#', $ua, $matches) && $matches[1] < 5) { | |
$header = 'filename="' . rawurlencode($filename) . '"'; | |
} | |
// < Chrome 11 | |
elseif (preg_match('#Chrome/(\d+)\.#', $ua, $matches) && $matches[1] < 11) { | |
$header = 'filename=' . $filename; | |
} | |
// < Safari 6 | |
elseif (preg_match('#Safari/(\d+)\.#', $ua, $matches) && $matches[1] < 6) { | |
$header = 'filename=' . $filename; | |
} | |
// Android | |
elseif (preg_match('#Android #', $ua, $matches)) { | |
$header = 'filename="' . $filename . '"'; | |
} | |
// other browsers assume that validate RFC/2231/5987 standards | |
// but, add old style filename information for special circumstances | |
else { | |
$header = "filename*=UTF-8''" . rawurlencode($filename) . '; filename="' . rawurlencode($filename) . '"'; | |
} | |
// cache is disallowed by client | |
if (!$expires) { | |
// Cannot use no-cache and pragma header when use old IE versions(<= 8) and SSL. | |
if ($old_ie) { | |
header('Cache-Control: private, must-revalidate, post-check=0, pre-check=0'); | |
header('Expires: Sat, 01 Jan 2000 00:00:00 GMT'); | |
} | |
else { | |
header('Cache-Control: no-store, no-cache, must-revalidate, post-check=0, pre-check=0'); | |
header('Expires: Sat, 01 Jan 2000 00:00:00 GMT'); | |
} | |
} | |
// cache is allowed by client | |
else { | |
header('Cache-Control: max-age=' . (int)$expires); | |
header('Expires: ' . gmdate('D, d M Y H:i:s', time() + (int)$expires) . ' GMT'); | |
} | |
// process range header for resume download | |
if (isset($_SERVER['HTTP_RANGE']) && preg_match('/^bytes=(\d+)-/', $_SERVER['HTTP_RANGE'], $matches)) { | |
$range_start = $matches[1]; | |
if ($range_start < 0 || $range_start > $filesize) { | |
header('HTTP/1.1 416 Requested Range Not Satisfiable'); | |
return false; | |
} | |
header('HTTP/1.1 206 Partial Content'); | |
header('Content-Range: bytes ' . $range_start . '-' . ($filesize - 1) . '/' . $filesize); | |
header('Content-Length: ' . ($filesize - $range_start)); | |
if ($remote) { | |
curl_setopt($ch, CURLOPT_WRITEFUNCTION, $pass_remote); | |
curl_setopt($ch, CURLOPT_RANGE, $range_start . '-' . ($filesize - 1)); | |
} | |
} else { | |
$range_start = 0; | |
header('Content-Length: ' . $filesize); | |
if ($remote) { | |
curl_setopt($ch, CURLOPT_WRITEFUNCTION, $pass_remote); | |
curl_setopt($ch, CURLOPT_RANGE, '0-' . $filesize); | |
} | |
} | |
// send other headers. | |
header('Accept-Ranges: bytes'); | |
header('Content-Type: application/octet-stream'); | |
header('Content-Disposition: attachment; ' . $header); | |
// clear output buffer. | |
// (blocks file broken and decrease memory usage) | |
while (ob_get_level()) { | |
ob_end_clean(); | |
} | |
// send a file each 64KB and clear output buffer. | |
// sometimes occurs memory leak when use readfile() function. | |
$block_size = 16 * 1024; | |
$speed_sleep = $speed_limit > 0 ? round(($block_size / $speed_limit / 1024) * 1000000) : 0; | |
$buffer = ''; | |
if ($range_start > 0 && !$remote) { | |
fseek($fp, $range_start); | |
$alignment = (ceil($range_start / $block_size) * $block_size) - $range_start; | |
if ($alignment > 0) { | |
$buffer = fread($fp, $alignment); | |
echo $buffer; unset($buffer); flush(); | |
} | |
} | |
while (!feof($fp) && !$remote) { | |
$buffer = fread($fp, $block_size); | |
echo $buffer; unset($buffer); flush(); | |
usleep($speed_sleep); | |
} | |
if ($remote && $ch) { | |
curl_exec($ch); | |
} | |
if (!$remote) { | |
fclose($fp); | |
} else { | |
curl_close($ch); | |
} | |
// true when successfully sent. | |
return true; | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment