Счетчик скачиваний файлов на PHP с применением htaccess и MySQL

Здравствуйте, уважаемые подписчики и посетители блога 4remind.ru. В этой заметке я предлагаю вашему вниманию PHP-скрипт, служащий для подсчета количества скачиваний файлов с сайта с использованием базы данных MySQL и применением файла Apache .htaccess. Счетчик может быть полезным для тех, кто выкладывает на своих сайтах файлы для скачивания и хочет вести учет по количеству скачиваний либо конкретного файла, либо всех доступных для скачивания файлов. Например, это могут быть файлы фотографий, плагинов, утилит, приложений, аудио, видео, экранных заставок.

Счетчик скачиваний файлов на PHP с использованием htaccess и MySQL


Для ведения статистики кроме PHP-скрипта нам понадобится еще таблица в базе данных MySQL. Но кроме того, нам еще понадобится модуль Apache mod_rewrite, который должен быть активным. Модуль mod_rewrite доступен на большинстве Apache-серверов, а если нет, то придется его подключить самостоятельно.

Итак, переступим к делу.
Допустим, что наш PHP-скрипт счетчика скачиваний файлов будет называться downloads.php. Тогда для начала нам нужно будет в файле .htaccess добавить правило редиректа, как показано в следующем примере:

#Правило редиректа для скрипта счетчика скачиваний файла
RewriteEngine on
RewriteRule ^(.*).(zip|pdf|rar)$ /downloads.php?file=$1.$2 [R,L]

После добавления этого правила имя каждого файла с расширением .zip, .pdf, .rar будет передаваться в виде GET-параметра для вызова PHP-скрипта. Например, если файл называется bonus.zip, то с учетом установленного нами правила редиректа мы получим на выходе ссылку /downloads.php?file=bonus.zip. Кроме указанных в примере расширений можно добавлять другие, разделяя их символом вертикальной черты «|».

Для того, чтобы повысить безопасность и чтобы ссылка редиректа не показывалась, в .htaccess нужно использовать только параметр [L] и к ссылке добавить параметр-ключ, например «key=secretkey23709676»:

#Правило редиректа для скрипта счетчика скачиваний файла
RewriteEngine on
RewriteRule ^(.*).(zip|pdf|rar)$ /downloads.php?file=$1.$2&key=secretkey23709676 [L]

Стоит учесть, что по приведенному выше примеру файл PHP-скрипта downloads.php должен будет находиться в корневой директории сайта. Если же Вы захотите его расположить в другом месте, то в файле .htaccess нужно будет произвести соответствующие изменения. То же самое касается и файлов для скачивания. Если они находятся не в корневой директории, то путь к ним нужно будет указать либо в файле .htaccess, либо в самом скрипте downloads.php.

Теперь настала пора создать в базе данных таблицу для учета скачиваний файлов. Открываем phpMyAdmin, выбираем нужную нам базу данных и выполняем в ней следующий SQL-запрос:

CREATE TABLE `downloads` (
    `filename` varchar(255) NOT NULL,
    `dcount` int(11) NOT NULL,
    PRIMARY KEY  (`filename`)
)

Теперь необходимая нам таблица в базе данных создана и пора на сайт закачать файл downloads.php. Вот код PHP-скрипта этого файла:

<?php

// проверка секретного ключа, смотрите второй вариант .htaccess выше
if( $_GET['key'] != "secretkey23709676" ){
	die("Go out, lamer!");
}

// подключаемся к базе данных
mysql_connect("localhost", "username", "password")
	or die ("ERROR! Can't connect to database!");
mysql_select_db("dbname"); // выбираем нужную базу данных

$filename = $_GET['file'];
$mainpath = $_SERVER['DOCUMENT_ROOT']."/"; // путь к каталогу файлов
$pathtofile = $mainpath . $filename; // путь к файлу

$typesoffiles = array("zip","pdf","rar");
 
if( !in_array(substr($filename, -3), $typesoffiles )) {
    echo "Недопустимый для скачивания тип файла.";
    exit;
}
 
if( $fdwn = fopen ($pathtofile, "r" )) {

    $filename = mysql_real_escape_string( $filename );

    // добавляем и увеличиваем счетчик загрузки файла
    $result = mysql_query("SELECT COUNT(*) AS filecount FROM downloads WHERE filename='" . $filename . "'");
    $counterdata = mysql_fetch_array($result);
    $qry = "";
 
    if ($counterdata['filecount'] > 0) {
        $qry = "UPDATE downloads SET dcount = dcount + 1 WHERE filename = '" . $filename . "'";
    } else {
        $qry = "INSERT INTO downloads (filename, dcount) VALUES ('" . $filename . "', 1)";
    }
	
    $statresult = mysql_query($qry);
 
    // теперь отдаем файл по частям
    $fsize = filesize($pathtofile);
    $pathtofile_parts = pathinfo($pathtofile);
 
    header("Content-type: application/octet-stream");
    header('Content-Disposition: filename="'.$pathtofile_parts["basename"].'"');
    header("Content-length: $fsize");
    header("Cache-control: private"); // используйте это для открытия файла напрямую
    while(!feof($fdwn)) {
        $download_buffer = fread($fdwn, 2048); // буфер для скачивания файла частями
        echo $download_buffer;
    }
}

fclose ($fdwn);
exit;
?>

Только не забудьте в первых строках скрипта заменить значения localhost, username, password и dbname на ваши для доступа к вашей базе данных. Да, и еще, Вы наверно заметили, что для повышения безопасности выше рекомендовалось в .htaccess добавлять к запросу параметр «key=secretkey23709676» (значение Вы сами свое конечно придумаете). Поэтому и в скрипте учтена проверка этого параметра-ключа в первых строках.

В примерах показаны коды только для базового ознакомления с алгоритмом, но Вы можете модифицировать их под свои конкретные нужды.

Для тех, кто уже хорошо знаком с командами запросов MySQL, один из посетителей блога по имени Олег предложил сократить количество запросов к базе данных до одного, вот так:

$qry = "INSERT INTO downloads (filename, dcount) VALUES ('" . $filename . "', 1) ON DUPLICATE KEY UPDATE dcount= (dcount+1)";

В этом запросе ON DUPLICATE KEY означает: при дублировании ключевого поля (в нашем случае ключевое поле `filename`, так как при создании таблицы указано: PRIMARY KEY = `filename`) увеличить на 1 числовое (int) поле `dcount`.

Удачи Вам в использовании счетчиков скачиваний файлов на языке PHP и использования .htaccess и MySQL!

Примечание
В некоторых случаях счетчик скачивания файлов может увеличиваться не на единицу, а на две (или больше, но это уж очень редкий случай), и связано это как правило с использованием мульти-поточных качалок типа Download Master, FlashGet и тому подобных.
Способы решения подобной проблемы Вы можете просмотреть в комментариях к этой статье. Ну а если у Вас есть свои решения, то поделитесь пожалуйста ими, добавив код в комментарии. Многие будут за это Вам благодарны.

Метки: , , , , , ,
Другие статьи похожей тематики:

Поделитесь материалом с другими, воспользуйтесь этими кнопками:
Получать обновления и новые материалы блога по E-mail

20 комментариев к “Счетчик скачиваний файлов на PHP с применением htaccess и MySQL”

  1. Xsaika:

    Полезный скрипт! Попробую прикрутить к своему форуму. Правда вот не совсем понял отличий в параметрах .htaccess в строке RewriteRule [R,L] и [L]. В чем будет разница и насколько это важно?

    • Xsaika, параметр, а точнее флаг [R] означает, что URL будет перенаправлен и будет отображаться в браузере, т.е. будут видны и GET-параметры в строке запроса, где будет виден и приведенный в примере секретный ключ &key=secretkey23709676. Это конечно недопустимо, в плане безопасности. Поэтому, чтобы скрыть от глаз пользователя перенаправление, рекомендую убрать из набора [R,L] флаг R и оставить только [L].
      Флаг [L] предотвращает переопределение нашего правила следующими строками с правилами в файле .htaccess, если конечно такие там есть. Его в принципе тоже можно не добавлять, если у вас больше нет правил с директивой RewriteRule.

  2. Андрей:

    Спасибо за скрипт. Попробовал его внедрить (с небольшими изменениями). Если можно несколько вопросов
    1. Команда $mainpath = $_SERVER['DOCUMENT_ROOT'].’/'; — возвращает неправильный путь. Пришлось прописать вручную
    2. SQL-запрос почему-то вписывает в базу 2 строки, я так понимаю выполняется 2 раза (по времени разница 2-3 сек) не знаю как отследить где он еще раз вызывается…
    3. У меня по умолчанию закачку перехватывает программа-качалка. Можно ли как-то отследить закачал пользователь файл или отменил?

    • Андрей, постараюсь ответить по порядку:
      1. затрудняюсь сказать что у вас не так, возможно все зависит от того, где файл находится;
      2. это происходит именно потому, что дважды вызывается скрипт, т.к. у вас происходит перехват закачки программой-качалкой (см. ответ в п.3);
      3. попробуйте использовать функцию «connection_aborted()» в пределах цикла «while», а процесс записи счетчика в БД перенесите в место после цикла «while»:

      while(!feof($fdwn)) {
      
        if (connection_aborted()) {
          fclose($fdwn);
          exit;
        }
      
        $download_buffer = fread($fdwn, 2048);
        echo $download_buffer;
      }
      
      $statresult = mysql_query($qry);
      

      В этом случае (по идее) скрипт не будет увеличивать счетчик и добавлять в базу 2 строки, если программа-качалка перехватила загрузку.
      Возможно еще нужно будет уменьшить размер буфера в строке «$download_buffer = fread($fdwn, 2048);», если файлы у вас размером меньше 2048 байт.

      Да, и еще можно подправить одну строчку формирования заголовка в скрипте на:

      header('Content-Disposition: Attachment; filename="'.$pathtofile_parts["basename"].'"');
      
  3. Андрей:

    Большое спасибо за столь скорый и основательный ответ. Проверил, действительно при загрузке средствами браузера — добавляется одна строчка. Качалкой — 2. Тег «Attachment» привел к тому, что по крайней мере из Оперы не происходит перехват на качалку (это сомнительное достижение, все таки многие предпочитают качать именно качалкой и могут забивать адрес вручную). А при скачке через даунлоад-мастер все равно пишет 2 строки (код «if (connection_aborted()) — не помог»). Запись в базу я ставил изначально после цикла while, изменение размера буфера тоже безрезультатно (тренировался на файле в 1 МБ)…

  4. Андрей:

    Попробовал сделать так:

    $i=0;
    $rbuf=2048;
      while(!feof($fd)) {
        $buffer = fread($fd, $rbuf);
        echo $buffer;  
        $i++;	    
    }
    if ($i>=$fsize/$rbuf){
      // запись в базу
    }
    

    Итог: все равно в базе 2 записи после качалки — скрипт исполняется 2 раза. Почему? Как Предотвратить?

  5. Андрей:

    Немного разобрался, но не знаю насколько красиво. Проблема с качалкой в том, что любая качалка пытается качать в несколько потоков (думаю на больших файлах можно и 5 записей получать), а скрипт исполняется на каждый такой запрос от качалки. Сначала пробовал блокировать одновременную скачку с одного IP, но так явно не красиво. В результате остановился на варианте отслеживания запроса от клиента на закачку не с нулевой позиции.

    if (getenv('HTTP_RANGE')=="") {// Установлена или нет переменная HTTP_RANGE 
      if (!$fd = fopen ($fullPath, "r")) { 
          echo "No such file or directory.";
          exit;
    }
      // теперь отдаем файл по частям
    header("Content-type: application/octet-stream"); 
    header("Content-Disposition: Attachment; filename=".$path_parts["basename"]); 
    header("Content-length: $fsize"); 
    header("Cache-control: private");//используется для прямого открытия файла
    $i=0;
    while(!feof($fd)) { 
        if (connection_aborted()) {
          fclose($fd);
          exit;
        }
        $buffer = fread($fd, $rbuf);// буфер для скачивания файла частями
        echo $buffer;  
        $i++;	    
    } 
    fclose ($fd);
      if ($i >= $fsize/$rbuf){
      include "config.php"; 
      //тут работаем с базой
      //
    }}
    else {
      // Получить значение переменной HTTP_RANGE 
      preg_match ("/bytes=(\d+)-/", getenv('HTTP_RANGE'), $m); 
      $csize=$fsize-$m[1];  // Размер фрагмента 
      $p1=$fsize-$csize;    // Позиция, с которой начинать чтение файла 
      $p2=$fsize-1;         // Конец фрагмента 
      if (!$fd = fopen ($fullPath, "r")) { 
          echo "No such file or directory.";
          exit;
      }
      fseek ($fd, $p1);// Установить позицию чтения в файле 
    
      header("HTTP/1.1 206 Partial Content"); 
      header("Connection: close");
      header("Content-type: application/octet-stream"); 
      header("Content-Disposition: Attachment; filename=".$path_parts["basename"]); 
      header("Content-Range: bytes ".$p1."-".$p2."/".$fsize);
      header("Content-length: $csize"); 
     // header("Cache-control: private");
      while(!feof($fd)) { 
        if (connection_aborted()) {
          fclose($fd);
          exit;
      }
        $buffer = fread($fd, $rbuf);// буфер для скачивания файла частями
        echo $buffer;
    }
    fclose ($fd);
    }
    

    В итоге империческим путем удалось установить, что кроме более правильного подсчета теперь наш скрипт поддерживает докачку.
    Замеченные проколы:
    - если на маленьком файле (1МБ) при закачке через браузер попробовать отменить закачку — счетчик сработает — видимо браузер успевает откешировать.
    - если качая через даунлоад-мастер остановить закачку до начала собственно закачки, а потом продолжить ее — счетчик будет инкрементирован 2 раза. Если остановить на середине — то только один раз — не помогает ни анализ if connection_aborted ни попытка подсчета частей.
    Владимиру спасибо за скрипт. Не знаю насколько ценно мое дополнение, но можно хотя бы указать в статье об обнаруженных проблемах

    • Андрей,
      спасибо, что поделились своими соображениями и скриптом!
      Я думаю ваше решение найдет применение (мне например оно интересно).

      Наиболее подходящего и универсального решения я все же пока так и не нашел, но от себя могу предложить вот примерно такую схему работы:

      1. при первом обращении к скрипту сам скрипт добавляет в БД «индикатор», например значение = 1 и время, включая секунды;

      2. как только закачка завершилась, то счетчик скачиваний увеличиваем на единичку, а если соединение оборвалось, то в БД «индикатор» сбрасывается в значение = 0, но счетчик закачек не увеличиваем;

      3. при каждом обращении к скрипту, сам скрипт проверяет значение и время у «индикатора» в БД, и если время например не превысило 5 секунд или значение = 1, то процесс скачивания скрипт не запускает (т.е. счетчик скачиваний в БД не меняет значение);

      Однако у предложенного мной метода тоже есть изъяны — например счетчик не будет меняться, если файл одновременно закачивался с разных компов в течении установленного интервала времени. Хотя и для этого можно добавить проверку например User-Agent, IP или еще что-то…

      Андрей писал: «но можно хотя бы указать в статье об обнаруженных проблемах«.
      -> примечание о возможных проблемах уже добавлено в конце статьи.

  6. Focus:

    А как сделать, чтоб можно было передать в ссылке на файл дополнительные GET параметры?, чтоб их .htaccess передал скрипту. например ссылка на скачивание выглядела бы так: site.ru/downloads/superfile.zip?user=memberuser , а ешо лучше просто ключ без параметра: site.ru/downloads/superfile.zip?memberuser

    чую что в htaccess но он для меня тёмный less :)

  7. Focus:

    Вроде сам допёр, вот такая строчка вышла:
    RewriteRule ^(.*).(zip|pdf|rar)&(.*)$ /downloads.php?file=$1.$2&$3&key=secretkey123123 [L]
    Наверно для безопасности ешо регулярное выражение какоенить вставить надо в скобки вместо .* В чем я так-же разбираюсь как в htaccess :)

    • Конечно можно и регулярку вставить вместо * , но это зависит от ваших требований к ссылке, запросу который обрабатывается с помощью RewriteRule.

      Например чтобы имена файлов содержали ТОЛЬКО цифры и латинские буквы, то вместо первой звездочки с точкой в скобках добавьте вот это:
      ([a-zA-Z0-9]+)

  8. Олег:

    Вместо:

    // добавляем и увеличиваем счетчик загрузки файла
    $result = mysql_query("SELECT COUNT(*) AS filecount FROM downloads WHERE filename='" . $filename . "'");
    $counterdata = mysql_fetch_array($result);
    $qry = "";
    
    if ($counterdata['filecount'] > 0) {
       $qry = "UPDATE downloads SET dcount = dcount + 1 WHERE filename = '" . $filename . "'";
    } else {
       $qry = "INSERT INTO downloads (filename, dcount) VALUES ('" . $filename . "', 1)";
    }
    

    одним запросом:

    $qry = "INSERT INTO downloads (filename, dcount) VALUES ('" . $filename . "', 1) ON DUPLICATE KEY UPDATE dcount= (dcount+1)";
    

    нечего базу данных по 3 раза дергать из-за 1 значения

    • Отлично, Олег! Спасибо за код! В примерах в статье было специально по частям все сделано, и именно для того, чтобы новичкам было легче понять как, что и когда происходит.

      • Олег:

        тогда напишите пользователям, что ON DUPLICATE KEY означает: при дублировании ключевого поля (в нашем случае ключевое поле `filename`, так как при создании таблицы указано: «PRIMARY KEY (`filename`) ) увеличить на 1 числовое(int) поле `dcount`»

  9. Олег:

    Ещё, с htacces-ом я не совсем понял, зачем secrtkey. Если боязнь, что переход на страницу download.php не с Вашего сайта, а, скажем, с локальной страницы (счётчик-то всё равно увеличится) поможет $_SERVER['HTTP_REFERER']:
    если его хост не соответствует хосту вашего сайта, ip внести в чёрный список и досвидос:
    header(‘location:anywhere’);

    • Ну, это уже немного другая тема, то есть про черные списки и т.д. На сайтах с высокой активностью посетителей можно по ошибке или случайности надобавлять в черный список целые сети… Может позже напишу об этом.

  10. Олег:

    Ещё, извиняюсь за упорство, как узнать реальное количество закачек? Если чел перешел на страницу downloads, не факт, что он дождался загрузки(я сам частенько после начала скачки, особенно, когда сперва не понял, что это- ссылка на загрузку, обрываю закачку).
    Уведомление по завершении загрузки, имхо, важнее (это Вам по силам?)

    • Для этого думается мне можно немного изменить концовку скрипта. Там в цикле «while( !feof($fdwn) )» есть проверка «if (connection_aborted())», так вот запрос в БД нужно перенести вниз, чтобы счетчик в БД НЕ увеличивался, если был обрыв или отмена закачки.
      А насколько важно пользователя еще предупреждать, что закачка завершилась? Ведь большинство качают не из консоли, а браузерными качалками и прочими, которые показывают закачался ли фал или нет. Даже «wget» показывает процесс закачки в консоли.
      Хотя, в принципе, если уж кому захочется, то можно и показать сообщение, и опять же, конечный статус закачки можно получать в том же «while( !feof($fdwn) )».

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

Оставить комментарий

Подписаться на обновления блога 4remind.ru по RSS
Новости блога в социальных сетях

="4remind.ru

Rambler's Top100
Рейтинг@Mail.ru


Яндекс.Метрика