Простое решение для раздачи файлового архива через временные WEB-ссылки
Недавно в нашей организации возникла задача предоставления различным пользователям доступа для загрузки объемных видеоматериалов из нашего внутреннего видеоархива. Штатное корпоративное "облако" на базе сервиса SeaFile, как оказалось не вполне подходит для работы с файлами такого размера (в несколько десятков гигабайт).
Короче возникла острая потребность в простом и неприхотливом решении для раздачи содержимого директории файлового сервера через индивидуальные временные Веб-ссылки. Поначалу я был в полной уверенности, что в течение пары часов найду в сети что-то, что можно легко прикрутить для своих нужд. Однако результат меня слегка разочаровал: попадались либо монстры типа OpenMediaVault, либо приложения для десктопов на базе Windows. Пара решений которые более-менее подходили имели проблемы с русской кодировкой, что в эпоху utf8 выглядит немного странно.
С навязчивым ощущением изобретателя велосипеда пришлось вспоминать приемы Веб-программирования, которым я по хорошему уже лет десять как не занимался. Полноценная система мне была не нужна, скорее хотелось сделать такую вот web-утилитку, которую можно быстро прикрутить по месту куда угодно.
Если говорить тезисно, то решение получилось следующим:
Отказ от реестра в пользу подписанных ссылок. Сервер формирует ссылку куда упаковывает путь к файлу загрузки, временную метку и подписывает все это закрытым ключом. Это позволяет существенно упростить всю системы в целом.
При загрузке файла все эти параметры извлекаются из URL и предварительно "валидируются".
Вся система представлена одним единственным скриптом index.php, который доступен по разным URL (
/admin
и/download
).Если доступ происходит по пути
/admin
, то скрипт работает в режиме "админки", он показывает файловый каталог и временные ссылки, которые можно выслать пользователю. В остальных случаях скрипт работает в режиме загрузки файлов.Путь URL
/admin
необходимо надлежащим образом защитить средствами http-сервера.
В данной реализации все настроечные параметры хранятся с скрипте, какие-либо дополнительный файлы конфигураций отсутствуют. Это сделано опять же для простоты развертывания следуя концепции простой веб-утилитки. Хотя если образование вам не позволяет принять такой подход, можете переписать код чтобы все было "правильно". Правда и смысл подписанных ссылок тогда тоже частично теряется. В такую систему уже можно добавить реестр и сделать ссылки короткими.
В заключение привожу код скрипта, пример его развертывания на базе http-сервера Apache, и пару скриншотов. Надеюсь представленное решение будет полезно "профильной аудитории":
NameVirtualHost *:80
<VirtualHost *:80>
ServerName download.myconpany.ru
AddHandler php5-script .php
AddType text/html .php
DirectoryIndex index.php
# place download script here: /var/www/download
Alias /admin /var/www/download
Alias /download /var/www/download
<LocationMatch /admin >
AuthType Basic
AuthName "Restricted Content"
AuthUserFile /var/pwdb/.htpasswd
Require valid-user
Order Allow,Deny
Allow from 10.0.0.0/8
</LocationMatch>
</VirtualHost>
<?php
class MyErrorException extends Exception { }
class MyBreakException extends Exception { }
///////////////////////////////////////////////////////////////////////
// Настроечные параметры //////////////////////////////////////////////
// ////////////////////////////////////////////////////////////////////
$EXP_DAYS = 3; // время жизни ссылкок загрузки в днях
$TOPDIR = "/data/download"; // расположение файлового архива
$KEY=('7lkh4kwsl0451382'); // ключ подписи ссылок загрузки (поменять!)
$curdir = $TOPDIR;
$BODY = "";
$HEAD = "Каталог загрузки";
$uri_parts = explode('?', $_SERVER['REQUEST_URI'], 2);
$access = end(explode('/', trim($uri_parts[0],'/')));
try {
if ( $access != "admin" ) {
$HEAD = "Загрузка файла";
list ($p,$t) = my_path_decode($_GET['download']);
if (abs(time()-$t) > $EXP_DAYS * 24 * 3600) {
throw new MyErrorException("ссылка устарела");
}
$f = basename($p);
$file = secure_path("$TOPDIR/$p");
if ($file == false) {
throw new MyErrorException("плохой путь");
}
$size = 0;
if (!file_exists($file)) {
throw new MyErrorException("файл $f не найден");
}
$size = filesize($file);
if(isset($_GET['start'])) {
set_time_limit(3600);
$chunksize = 5 * (1024 * 1024); //5 MB (= 5 242 880 bytes) per one chunk of file.
$handle = fopen($file, 'rb');
if ($handle == false ) {
throw new MyErrorException("не могу прочитать файл");
}
header('Content-Description: File Transfer');
header('Content-Type: application/octet-stream');
header('Content-Disposition: attachment; filename="'.basename($file).'"');
header('Expires: 0');
header('Cache-Control: must-revalidate');
header('Pragma: public');
header('Content-Length: ' . $size);
while (!feof($handle))
{
print(@fread($handle, $chunksize));
ob_flush();
flush();
}
fclose($handle);
exit;
}
$qs = $_SERVER['QUERY_STRING'];
$size = custom_number_format($size);
$BODY .= "$f ($size) <BR><A href='?$qs&start=on'>[Загрузить файл]</A><A title='Скопировать ссылку в буфер обмена' href='#' onclick='copyURL()'>[Скопировать ссылку]</A><BR>\n";
$BODY .= "<BR><i>Сылка действует до " . date("d.m.Y H:i", $t+24*3600*$EXP_DAYS) . " </i>";
//goto END_OF_HEAD;
throw new MyBreakException();
}
$path = trim(isset($_GET['path']) ? $_GET['path'] : "" ,'/');
if ( $access != "admin" ) {
throw new MyErrorException("Нарушение прав доступа !!!");
}
if ($path) {
$curdir = secure_path($TOPDIR."/".$path);
if ($curdir == false) {
throw new MyErrorException("Плохой путь!");
}
}
if (is_dir($curdir) and $dh = opendir($curdir)) {
$dirs = array();
$files = array();
while (($file = readdir($dh)) !== false) {
if ($file == "." or $curdir == $TOPDIR and $file == "..") {
continue;
}
elseif (is_dir("$curdir/$file")) {
array_push($dirs,$file);
}
elseif (is_file("$curdir/$file")) {
array_push($files,$file);
}
}
closedir($dh);
asort($dirs);asort($files);
$BODY .= "<TABLE>\n";
if ($curdir != $TOPDIR) {
$BODY .= "<TR><TD>[TOP]<TD> <A href='?'>TOP <BR><A><TD></TR>\n";
}
foreach ($dirs as $dir) {
if ($dir == "..") {
$p = trim(preg_replace("/\/?[^\/]*$/", "", $path),'/');
}
else {
$p = trim("$path/$dir",'/');
}
$p = urlencode($p);
$BODY .= "<TR><TD>[DIR]<TD><A href='?path=$p'>$dir</A><TD></TR>\n";
}
foreach ($files as $file) {
$s = filesize("$TOPDIR/$path/$file");
$s = custom_number_format($s);
$d = urlencode(my_path_encode(trim("$path/$file",'/')));
$BODY .= "<TR><TD>[FILE]<TD>$file ($s)<TD><A href='../download?download=$d'>[Временная ссылка]</A>\n";
}
$BODY .= "</TABLE>\n";
}
else {
throw new MyErrorException("Невозможно отрыть директорию для текщего пути $path");
}
}
catch (MyBreakException $e) {
}
catch (MyErrorException $e) {
$BODY = err_msg($e->getMessage());
}
function err_msg($msg) {
return "<FONT color=red> ОШИБКА: $msg</FONT><BR>\n";
}
function custom_number_format($n, $precision = 2) {
if ($n < 1024) {
$n_format = number_format($n) . 'B';
} else if ($n < 1024*1024) {
$n_format = number_format($n / 1024, $precision) . 'K';
} else if ($n < 1024*1024*1024) {
$n_format = number_format($n / (1024*1024), $precision) . 'M';
} else {
// At least a billion
$n_format = number_format($n / (1024*1024*1024), $precision) . 'G';
}
return $n_format;
}
function secure_path ($path) {
global $TOPDIR;
$p = realpath($path);
if (startsWith($p,$TOPDIR)) {
return $p;
}
return false;
}
function startsWith ($string, $startString)
{
$len = strlen($startString);
return (substr($string, 0, $len) === $startString);
}
function my_path_encode ($path) {
global $KEY;
$body = json_encode(array($path,time()));
$hash = md5($body.$KEY);
$out = json_encode(array($hash,$body));
return base64_encode($out);
}
function my_path_decode ($epath) {
global $KEY;
list ($hash,$body) = json_decode(base64_decode($epath));
if ( $hash !== md5($body.$KEY) ) {
throw new MyErrorException("ссылка не существует");
}
list($path,$time) = json_decode($body);
return array($path,$time);
}
?>
<html>
<head>
<script language="JavaScript">
function copyURL () {
var dummy = document.createElement('input'), text = window.location.href;
document.body.appendChild(dummy);
dummy.value = text;
dummy.select();
document.execCommand('copy');
document.body.removeChild(dummy);
alert('Ссылка с копирована в буфер обмена');
}
</script>
</head>
<body>
<H1> Файловый архив</H1>
<H2> == <?php echo $HEAD; ?> ==</H2>
<HR>
<?php echo $BODY; ?>
<HR>
== Здесь будет ваша подпись ==
</body>
</html>