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

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

В качестве хранения информации о файлах и временных ссылках, будем использовать БД. Таблица 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 );
 
}
 
?>

Комментариев: 26

  1. Артём Курапов:

    Я нечто похожее использую у себя в библиотеке aleria.net, но не основную часть. readfile ведь умрёт если файл очень большой а скачивается он медленно.

  2. admin:

    Артём Курапов, Вы правы - умрет по timeout. Наверное, есть смысл использовать

    set_time_limit(0);

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

  3. iphone:

    Спасибо! как раз я работаю над проектом в которм есть такая задача.

  4. ygen:

    Давно хотел прикрутить подобнрую фишку для скачивания по временным ссылкам… буду разбираться,спасибо.

  5. Денис:

    Подскажите не могу настроить скрипт выдает ошибку Warning: mysql_num_rows(): supplied argument is not a valid MySQL result resource in /home/hitchar/public_html/okmp3.ru/download/index.php on line 98
    Я фаил залил в папку files занес его через phpadmin в БД
    все нормально до моментка где нцжно скачать фаил т..е переходишь скачать потом в течение 12 часов это ссылка действительно и когда на нее жмешь она выдает эту ошибку

  6. admin:

    Подскажите не могу настроить скрипт выдает ошибку Warning: mysql_num_rows(): supplied argument is not a valid MySQL result resource in /home/hitchar/public_html/okmp3.ru/download/index.php on line 98
    Перевожу: переданный функции mysql_num_rows() аргумент не является результатом запроса к базе данных. Это моя вина: я написал
    NOW() - INTERVAL 12 HOURS
    а надо
    NOW() - INTERVAL 12 HOUR
    Исправил.

  7. alexey:

    заинтересовали решил попробовать,наткнулся на ->
    Warning: mysql_fetch_array(): supplied argument is not a valid MySQL result resource in W:\home\localhost\www\DOCUMENT_ROOT\download\index.php on line 35

    в коде:

    function fileslist() {


    ->>> while( $file = mysql_fetch_array( $res ) ) {

    перепробовал все,не могу понять в чем проблема.
    вылезает при загрузке.
    примерно так:

    Файлы для скачивания

    Warning: mysql_fetch_array(): supplied argument is not a valid MySQL result resource in W:\home\localhost\www\DOCUMENT_ROOT\download\index.php on line 35
    № Наименование Описание Тип Скачать

  8. admin:

    alexey, нужен перевод? Предупреждение: переданный функции mysql_fetch_array() аргумент не является результатом запроса к базе данных. Это означает, что запрос к БД не был выполнен - скорее всего, ошибка в синтаксисе SQL-запроса. PHPFAQ.RU поможет:
    Ничего не работает! Что делать??? Поиск ошибок и отладка
    При проблемах с MySQL (supplied argument is not a valid MySQL result resource) под строкой, где произошла ошибка, обязательно надо вывести на экран mysql_error() и сам запрос - чтобы его можно было выполнить из консоли или через phpmyadmin.

  9. Дмитрий:

    Респект создателю блога. Реально найти такое , надо много полазить по гуглу

  10. sergey:

    Ели наше эту статью…
    Оказалось, что изменился урл… хорошо, что догадался )))
    blog.webmasterschool.ru/php/250/ сейчас в гугле
    http://blog.webmasterschool.ru/?p=250 реальный урл.

    Теперь говорю т всей души спасибо… Скрипт пригодился :)

  11. Naya:

    Вот это скрипт. Круто не каждый до этого догадается.

  12. Ala:

    Хороший скриптик, я его скопировал попробую использовать

  13. статус:

    Если файлы разместить на одном хостинге, а сам сайт будет на другом, получится ли реализировать что-то подобное?
    Я уже около месяца сушу голову, как это можно сделать так, чтобы скачивание осуществлялось с другого хостинга, но в браузере отображался урл самого сайта.
    ЗЫ. С файлами еще не работал с плоскости ПХП.

  14. admin:

    Скрипт, который будет отдавать файл с другого сервера

    header('Content-type: application/pdf');
    readfile('http://otherserver.com/files/somefile.pdf');
  15. master:

    admin, данный скрипт надо будет хорошо переписать, чтобы он отдавал файлы с другого сервера.
    К примеру такие функции, как file_exists(), filesize(), filemfime(), fseec()… и хедерсам торба.

  16. admin:

    admin, данный скрипт надо будет хорошо переписать, чтобы он отдавал файлы с другого сервера
    master, так что Вас останавливает?

  17. Yury:

    Спасибо. Полезная информация. А вообще наверное проще скрипт антилич (запрет скачивания файла с других доменов). Реализован на файлах например на http://acvarif.net.ru
    Наверное на базе Вашего скрипта вполне можно формировать ссылки для скачивания электронных товаров в своем или не своем магазине. Будет время, если Вы не возражаете, попробую сделать его на файлах.

  18. Yury:

    Отличная статья. Такой кстати я использую для формирования временных линков на скачивание шаблонов сайтов на базе acvarif-cms.

  19. Виталий:

    Отлично, всегда хотел нечто подобное, но руки все никак не доходили )) Спасибо

  20. Ильдар:

    Большое спасибо за скрипт! Очень помог.

  21. Сергей:

    Огромнейшее спасибо за скрипт. Разобрался и подключил. Все работает на ура

  22. Дмитрий:

    Решил использовать ваш скрипт в интернет-магазине на базе ocscommerce.
    Возникает проблема, когда уже работает функция downloads(). Выдаются ошибки Warning: Cannot add header information в каждой строке, где попытка заменить заголовок, начиная с … header( ‘HTTP/1.1 200 OK’ );…
    и дальше по всем…
    Файл не скачивается, а ниже этих предупреждений открывается как в блокноте…

  23. Дмитрий:

    Разобрался! Нужно алгоритм downloads() размещать в отдельном файле и кроме алгоритма там недолжно быть ничего.

  24. Дмитрий О.:

    Спасибо. Сам бы не написал никогда) Лень..

  25. Man:

    Как раз то, что искал. Буду генерить ссылки, отлично.

  26. Александр:

    А зачем делать через MySQL ? нагрузку и тд. Если можно сделать проще , base64_encode($url . “||” . time()); и выставить 3600сек. шифрануть , получить ссылку и делать дешифровку обратно . Думаю так даже лучше будет .

Оставьте свой отзыв