Рубрика «PHP»

Скачивание файлов по временным ссылкам

Наверное каждому приходилось сталкиваться с временными ссылками при скачивании фильмов, музыки, программ и т.п. Зачем это делается? Да чтобы другие сайты не размещали ссылки на файлы, которые расположены на нашем сайте. Давайте посмотрим, как написать скрипт, который будет генерить временные ссылки.

В качестве хранения информации о файлах и временных ссылках, будем использовать БД. Таблица files хранит информацию о файлах:

CREATE TABLE `files` (
  `id` INT(10) PRIMARY KEY,
  `title` VARCHAR(255) NOT NULL DEFAULT '',
  `description` TEXT NOT NULL DEFAULT '',
  `filename` VARCHAR(64) NOT NULL DEFAULT '',
  `mimetype` VARCHAR(8) NOT NULL DEFAULT ''
) ENGINE=INNODB DEFAULT CHARSET=cp1251;

Здесь

  • id - уникальный ID файла
  • title - название файла, например, “Текстовой редактор NotePad++
  • description - описание файла, например, “Бесплатный редактор текстовых файлов (замена стандартного Блокнота) с поддержкой синтаксиса большого количества языков программирования, ориентирован для работы в операционной системе MS Windows
  • filename - имя файла для скачивания, например, NotePadPP.zip
  • mimetype - MIME-тип файла

Таблица downloads хранит информацию о временных ссылках:

CREATE TABLE `downloads` (
  `file_id` INT(10) NOT NULL DEFAULT 0,
  `uniq_id` VARCHAR(32) NOT NULL DEFAULT '',
  `puttime` DATETIME NOT NULL DEFAULT '0000-00-00 00:00:00'
) ENGINE=INNODB DEFAULT CHARSET=cp1251;

Здесь

  • file_id – уникальный ID файла
  • uniq_id – временная ссылка
  • puttime - время создания ссылки

Файлы для скачивания расположены в директории DOCUMENT_ROOT/download/files/. Эта директория должна быть защищена с помощью .htaccess:

Order Allow,Deny
Deny from All

Скрипт, который будет выполнять всю работу - выводить список файлов, генерить временные ссылки, и отдавать файлы на скачивание - DOCUMENT_ROOT/download/index.php

<?php

// Соединяемся с сервером БД
mysql_connect ( 'localhost', 'root', '' );
mysql_query( 'SET NAMES cp1251' );
mysql_select_db ( 'downloads' );

// удаляем устаревшие записи в таблице БД downloads
$query = 'DELETE FROM downloads WHERE puttime < (NOW() - INTERVAL 12 HOUR)';
mysql_query( $query );

$actions = array( 'fileslist', 'getlink', 'download' );

$action = 'fileslist';
if( isset( $_GET['action'] ) and in_array( $_GET['action'], $actions ) ) $action = $_GET['action'];

switch( $action ) {
  case 'fileslist':      // список файлов для скачивания
    fileslist(); break;
  case 'getlink':        // создаем временную ссылку
    getlink(); break;
  case 'download':       // отдаем файл на скачивание
    download()break;
}

function fileslist() {

  echo '<h3>Файлы для скачивания</h3>'."\n";
  $query = 'SELECT id, title, description, mimetype FROM `files` WHERE 1 ORDER BY title';
  $res = mysql_query( $query );

  echo '<table border="1">'."\n";
  echo '<tr><th>№</th><th>Наименование</th><th>Описание</th><th>Тип</th><th>Скачать</th></tr>'."\n";
  $i = 1;
  while( $file = mysql_fetch_array( $res ) ) {
    echo '<tr>';
    echo '<td>'.$i.'</td>';
    echo '<td>'.$file['title'].'</td>';
    echo '<td>'.$file['description'].'</td>';
    echo '<td>'.$file['mimetype'].'</td>';
    echo '<td><a href="'.$_SERVER['PHP_SELF'].'?action=getlink&id='.$file['id'].'">Скачать</a></td>';
    echo '</tr>'."\n";
    $i++;
  }
  echo '</table>'."\n";
}

function getlink() {

  // если не передан уникальный ID файла - значит пользователь попал сюда по ошибке
  if( !isset( $_GET['id'] ) ) {
    header( 'Location: '.$_SERVER['PHP_SELF'].'?action=fileslist' );
    die();
  }
  $id = (int)$_GET['id'];

  // прежде чем генерить временную ссылку, проверяем, что есть такая запись в таблице БД
  $query = 'SELECT 1 FROM `files` WHERE id='.$id;
  $res = mysql_query( $query );
  if( mysql_num_rows( $res ) == 0 ) {
    header ( 'HTTP/1.1 404 Not Found' );
    die()
  }
 
  $uniq_id = md5( uniqid(rand(), 1) );
  $query = "INSERT INTO downloads (file_id, uniq_id, puttime)
            VALUES ("
.$id.", '".$uniq_id."', NOW())";
  mysql_query( $query );
 
  $link = $_SERVER['PHP_SELF'].'?action=download&id='.$id.'&code='.$uniq_id;
  echo '<p>Для загрузки файла перейдите по <a href="'.$link.'">этой ссылке</a>. ';
  echo 'Ссылка действительна в течение 12 часов.</p>'."\n";

}

function download() {
 
  // если не передан уникальный ID файла - значит пользователь попал сюда по ошибке
  if( !isset( $_GET['id'] ) ) {
    header( 'Location: '.$_SERVER['PHP_SELF'].'?action=fileslist' );
    die();
  }
  $id = (int)$_GET['id'];
 
  if( !isset( $_GET['code'] ) )  {
    header( 'Location: '.$_SERVER['PHP_SELF'].'?action=fileslist' );
    die();
  }
 
  if( !preg_match( '#[a-f0-9]{32}#', $_GET['code'] ) )  {
    header ( 'HTTP/1.1 404 Not Found' );
    die();
  }
 
  $query = "SELECT 1 FROM downloads WHERE file_id=".$id."
            AND uniq_id='"
.$_GET['code']."' AND puttime > (NOW() - INTERVAL 12 HOUR)";
  $res = mysql_query( $query );
  if( mysql_num_rows( $res ) == 0 ) {
    header ( 'HTTP/1.1 404 Not Found' );
    die()
  }
 
  $query = 'SELECT filename, mimetype FROM `files` WHERE id='.$id;
  $res = mysql_query( $query );
  if( mysql_num_rows( $res ) == 0 ) {
    header ( 'HTTP/1.1 404 Not Found' );
    die()
  }
  list( $filename, $mimetype ) = mysql_fetch_row( $res );
 
  // если файла нет
  if( !file_exists( './files/'.$filename ) ) {
    header ( 'HTTP/1.1 404 Not Found' );
    die();
  }
 
  // получаем размер файла
  $fsize = filesize( './files/'.$filename );
  // дата модификации файла для кеширования
  $ftime = date( 'D, d M Y H:i:s T', filemtime( './files/'.$filename ) );
  // смещение от начала файла
  $range = 0;
 
  // пробуем открыть
  $handle = @fopen( './files/'.$filename, 'rb' );

  // если не удалось
  if( !$handle ){
    header ( 'HTTP/1.1 404 Not Found' );
    die();
  }
 
  // если запрашивающий агент поддерживает докачку
  if( $_SERVER['HTTP_RANGE'] ) {
    $range = $_SERVER['HTTP_RANGE'];
    $range = str_replace( 'bytes=', '', $range );
    $range = str_replace( '-', '', $range );
    // смещаемся по файлу на нужное смещение
    if ( $range ) fseek( $handle, $range );
  }
 
  // если есть смещение
  if( $range ) {
    header( 'HTTP/1.1 206 Partial Content' );
  } else {
    header( 'HTTP/1.1 200 OK' );
  }
 
  header( 'Content-Disposition: attachment; filename="'.$filename.'"' );
  header( 'Last-Modified: '.$ftime );
  header( 'Content-Length: '.($fsize-$range) );
  header( 'Accept-Ranges: bytes' );
  header( 'Content-Range: bytes '.$range.'-'.($fsize - 1).'/'.$fsize );

  switch( $mimetype ) {
    case 'pdf' : $ctype = 'application/pdf'; break;
    case 'zip' : $ctype = 'application/zip'; break;
    case 'doc' : $ctype = 'application/msword'; break;
    case 'xls' : $ctype = 'application/vnd.ms-excel'; break;
    case 'gif' : $ctype = 'image/gif'; break;
    case 'png' : $ctype = 'image/png'; break;
    case 'jpeg':
    case 'jpg' : $ctype = 'image/jpg'; break;
    case 'mp3' : $ctype = 'audio/mpeg'; break;
    case 'wav' : $ctype = 'audio/x-wav'; break;
    case 'mpeg':
    case 'mpg' :
    case 'mpe' : $ctype = 'video/mpeg'; break;
    case 'mov' : $ctype = 'video/quicktime'; break;
    case 'avi' : $ctype = 'video/x-msvideo'; break;
    default    : $ctype = 'application/octet-stream';
  }
 
  header( 'Content-Type: '.$ctype );
 
  readfile( './files/'.$filename );
 
  fclose( $handle );
 
}
 
?>

Ограничение скорости скачивания файлов средствами PHP

Допустим, у нас есть сайт, предоставляющий файлы для скачивания. Но для зарегистрированных пользователей нет никаких ограничений, а для остальных, чтобы не перегружать сервер, хотелось бы ввести некоторые ограничения. Причина для ограничения скорости скачивания может быть и иной: пользователь, оплативший скачивание без ограничений - получает файл на максимальной скорости, а тот, кто пожадничал - будет скачивать очень медленно. Посмотрим, как это можно реализовать с помощью PHP:

<?php
// лимит времени выполнения
set_time_limit(0);

// скорость скачивания - 128КБ в сек.
$speed = 1024*128;
// имя файла
$filename = 'doc.pdf';
// размер файла
$filesize = filesize($filename);
// смещение от начала файла
$range = 0;

while( is_already_download() ) {
  // спим пока у пользователя есть активные потоки
  sleep(1);
}

// открываем файл на чтение
$f = fopen($filename, 'rb');

if (isset($_SERVER['HTTP_RANGE'])) { // поддерживается ли докачка
  $range = $_SERVER['HTTP_RANGE'];
  $range = str_replace('bytes=', '', $range);
  $range = str_replace('-', '', $range);
  if ($range) fseek($f, $range);
}
 
// если есть смещение
if ($range) {
  header($_SERVER['SERVER_PROTOCOL'].' 206 Partial Content');
} else {
  header($_SERVER['SERVER_PROTOCOL'].' 200 OK');
}

header( 'Last-Modified: '.date('D, d M Y H:i:s T', filemtime($filename)) );
header('Content-Length: '.($filesize-$range));
header('Accept-Ranges: bytes');
header('Content-Range: bytes '.$range.'-'.($filesize - 1).'/'.$filesize);
header('Content-Type: application/pdf');
header('Content-Disposition: attachment; filename="'.$filename.'"');

while( !feof($f) ) {
  echo fread($f, $speed);
  flush();
  sleep(1); // засыпаем
}

// закрываем файл
fclose($f);

// удаляем информацию о соединении из БД
mysql_query("DELETE FROM `sessions` WHERE `session_ip`='".$_SERVER['REMOTE_ADDR']."' LIMIT 1");

function is_already_download() {
  // проверяем на наличие соединений от пользователя
  $res = mysql_query("SELECT `session_ip` FROM `sessions` WHERE `session_ip`='".$_SERVER['REMOTE_ADDR']."' LIMIT 1");
  if (mysql_num_rows($res)) {
    return true;
  } else { // если запись отсутствует, то добавляем
    mysql_query ("INSERT INTO `sessions` VALUES ('".$_SERVER['REMOTE_ADDR']."')");
    return false;
  }
}
?>

Однако пользователь без привилегий может воспользоваться менеджером закачки, который позволяет скачивать файл в несколько потоков. Таким образом, он сможет без труда обойти наше ограничение. На этот случай есть функция is_already_download(), которая проверяет наличие уже установленных соединений.

В данном случае мы используем таблицу БД sessions, в которй всего одно поле - IP-адрес скачивающего. При наличии IP-адреса скачивающего в таблице отдаем true, в противном случае записываем его и отдаем false.

Ссылки по теме:

Как избавиться от сообщений “headers already sent”

Предположим, вы пытаетесь отправить HTTP-заголовок или cookie с помощью функции header(), session_start() или setcookie(), но PHP выдает сообщение “headers already sent” (”заголовки уже отправлены”). Эта ошибка возникает, если вы отправили содержимое браузеру до вызова функции header(), session_start() или setcookie(). PHP посылает заголовки автоматически, как только скрипт начинает выдавать браузеру информацию. Перепишите свой код так, чтобы вывод содержимого происходил после отправки заголовков:

// Правильно
setcookie("name", $name);
echo "Hello, $name";
// Неправильно
echo "Hello, $name";
setcookie("name", $name);
// Правильно
<?php setcookie("name", $name); ?>
<html><title>Hello</title>

Любое HTTP-сообщение имеет заголовок и тело, которые отправляются браузеру именно в таком порядке. Начав отправку тела, вы больше не можете отправлять заголовки. Поэтому, если вы вызовете функцию setcookie() после вывода HTML-кода, PHP не сможет отправить надлежащий заголовок cookie.

Вы также должны убрать из подключеных файлов завершающие пробельные символы. Когда вы подключаете php-файл с пустыми строчками вне тегов <?php ?>, эти строчки отправляются браузеру.

Другой способ гарантировать отсутствие завершающих пробельных символов в подключаемом файле является отказ от использования закрывающего тега ?>. Если подключаемый файл содержит только PHP-код, этот метод избавит вас от необходимости возвращаться к этому файлу и удалять из него нечаянно поставленные пробельные символы.

Если вы хотите полностью избавить себя от беспокойства по поводу пустых строчек, вызывающих отправку заголовков, включите буферизацию вывода. Она не позволит PHP немедленно отправлять вывод клиенту. Если вы буферизируете свой вывод, то можете перемешивать заголовки и текст сообщений. Однако в этом случае у пользователей может возникнуть впечатление, что ваш сервер медленно обслуживает их запросы, потому что браузер будет выводить содержимое на экран с некоторой задержкой.