Скачивание файлов в PHP

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

Начнем с простого способа. Пусть наш скрипт получает имя файла через какой-либо из параметров запроса. Это может быть реально набранный URL, а может быть и переписанный сервером при помощи mod_rewrite. Скрипт вызывает функцию file_download() с параметром $filename. Кроме прямой передачи в запросе $filename может также вычисляться на основе исходного идентификатора из запроса или дополняться путем в дереве папок сервера.

Самый легкий способ обработки запросов к скачиваемым файлам - это простое перенаправление на них.

function file_download($filename) {
// Проверяем существование файла
  if (file_exists($filename)) {
// Здесь пишем код, который будет обрабатывать каждую загрузку файла.

//  Перенаправляем клиента на файл.
    header('Location: ' . $filename);
  } else {
// Если файл не найден, сообщаем клиенту об этом через заголовки HTTP
    header($_SERVER["SERVER_PROTOCOL"] . ' 404 Not Found');
    header('Status: 404 Not Found');
  }
// Прерываем дальнейшее выполнение скрипта, чтобы не отправлять мусор в ответе клиенту
  exit;
}

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

Для этого потребуется усложнить нашу функцию закачки.

function file_download($filename, $mimetype='application/octet-stream') {
  if (file_exists($filename)) {
// Отправляем требуемые заголовки
    header($_SERVER["SERVER_PROTOCOL"] . ' 200 OK');
// Тип содержимого. Может быть взят из заголовков полученных от клиента
// при закачке файла на сервер. Может быть получен при помощи расширения PHP Fileinfo.
    header('Content-Type: ' . $mimetype);
// Дата последней модификации файла       
    header('Last-Modified: ' . gmdate('r', filemtime($filename)));
// Отправляем уникальный идентификатор документа,
// значение которого меняется при его изменении.
// В нижеприведенном коде вычисление этого заголовка производится так же,
// как и в программном обеспечении сервера Apache
    header('ETag: ' . sprintf('%x-%x-%x', fileinode($filename), filesize($filename), filemtime($filename)));
// Размер файла
    header('Content-Length: ' . (filesize($filename)));
    header('Connection: close');
// Имя файла, как он будет сохранен в браузере или в программе закачки.
// Без этого заголовка будет использоваться базовое имя скрипта PHP.
// Но этот заголовок не нужен, если вы используете mod_rewrite для
// перенаправления запросов к серверу на PHP-скрипт
    header('Content-Disposition: attachment; filename="' . basename($filename) . '";');
// Отдаем содержимое файла
    echo file_get_contents($filename);
  } else {
    header($_SERVER["SERVER_PROTOCOL"] . ' 404 Not Found');
    header('Status: 404 Not Found');
  }
  exit;
}

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

function file_download($filename, $mimetype='application/octet-stream') {
  if (file_exists($filename)) {
    header($_SERVER["SERVER_PROTOCOL"] . ' 200 OK');
    header('Content-Type: ' . $mimetype);
    header('Last-Modified: ' . gmdate('r', filemtime($filename)));
    header('ETag: ' . sprintf('%x-%x-%x', fileinode($filename), filesize($filename), filemtime($filename)));
    header('Content-Length: ' . (filesize($filename)));
    header('Connection: close');
    header('Content-Disposition: attachment; filename="' . basename($filename) . '";');
// Открываем искомый файл
    $f=fopen($filename, 'r');
    while(!feof($f)) {
// Читаем килобайтный блок, отдаем его в вывод и сбрасываем в буфер
      echo fread($f, 1024);
      flush();
    }
// Закрываем файл
    fclose($f);
  } else {
    header($_SERVER["SERVER_PROTOCOL"] . ' 404 Not Found');
    header('Status: 404 Not Found');
  }
  exit;
}

Продолжение следует...

ограничение кол-во загрузок с IP

Привет. Большое спасибо за этот мануал: http://shaman.asiadata.ru/node/314
Вот только одно не получается реализовать:
Пользователи скачивают большие файлы (по 30 мб) и как только человек получает через ф-ю file_download нужный header (header('Location: ' . $filename_url);), то начинается загрузка файла, длящаяся минут 5, но при этом register_shutdown_function-то тоже вызывается и поток считается завершенным. В то время как человеку еще качать и качать. Есть ли какая-либо возможность вызывать register_shutdown_function только после завершения закачки?

пока в раздумьях

Я пока в раздумьях по этому поводу. Может таймаут выполнения скрипта PHP побольше выставить.
У себя пока такой баг не словил., хотя пытался.

Таймаут скрипта? ммм... надо

Таймаут скрипта? ммм... надо тогда привязать таймауты к IP... эээ... но файлы-то от 1 Mb до 200... да и скорость у всех разная. Кто-то на выделенке, а кто-то на диалапе. Не представляю как рассчитать в этих условиях таймауты.
Я так понимаю, что это не баг, а нормальная работа скрипта. При перенаправлении header'ом скрипт прекращает свою работу (вроде бы логично), естественно вызывая при этом shutdown функцию, которая удаляет метку для потока, который на самом деле существует. =(
Админ сервака упирается и не хочет рубить особо настырных юзеров ни на уровне апача, ни до него (скажем, iptables). В общем приходится как-то делать это программно. Все уже сделал... и htaccess хитрый написал, с редиректами и проверками куки, и перенаправление на скрипт с потоками... Единственное, в чем затык - преждевременно удаляются метки потоков. =(

Невнимательный я. :) Не сразу

Невнимательный я. :) Не сразу сообразил, что перенаправление закрывает соединение. Тогда им не получится воспользоваться в такой ситуации. При перенаправлении файлы доступны напрямую и ограничить число потоков можно только через Апач. В httpd.conf или в файлах .htaccess есть такой параметр. А средствами PHP только отдавая файл через скрипт. Тогда есть гарантия, что скрипт выполняется пока идет закачка.

Вот еще что.... Может

Вот еще что....
Может попробуешь заголовок Connection: Keep-alive
У меня пока нет времени тестировать, до файл-сервера своего только в выходные доберусь, но может здесь собака зарыта. Надо еще в RFC порыться.

Сейчас делаю новый движок для

Сейчас делаю новый движок для сайта файлового хостинга. Планирую программную отдачу файлов по ссылке живущей одни сутки.

Фичи будут такие: ограничение скорости закачки для анонимов, отдача файла в несколько потоков с возможностью докачки и частичного скачивания, ограничение числа таких потоков.

Там куча тонких моментов: контроль активности соединения, обработка и поддержка частичных GET-запросов.
По результатам буду писать заметки. В общем, через недельки две-три.

=)) Тоже решил писать

=)) Тоже решил писать программную отдачу. Но по быстренькому, на коленке. )) Надо как-то людям доступ-то к файлам вернуть.

>> В httpd.conf или в файлах .htaccess есть такой параметр.
Дык то то и оно, что в htaccess нету. Есть только в апаче и tcp/ip, но админ недаеццо.
Предлагает менять vds на дедик. Не пойму он хитрый или ленивый ))

Все админы ленивые и от

Все админы ленивые и от лишнего рубля тоже никто не откажется. :)

Спасибо за статью и

Спасибо за статью и комментарии, очень помогли в решении оной проблеме

Продолжение...

Большие файлы отдавать пхпешкой глупо!

Большие файлы отдавать пхпешкой глупо! сервер будет падать... проверено на ФО...
вешайте отдачу на сервер, пусть он отдает
в nginx сморите в сторону X-Accel-Redirect

Нужен контроль за

Нужен контроль за скачиванием, причём индивидуальный.

Я же ответил в других ваших

Я же ответил в других ваших темах как контролировать скорость и многопоточность...

Буду осваивать. :)

Буду осваивать. :)

Спасибо за статью,инфа

Спасибо за статью,инфа пригодилась.

HELP!

столкнулся с такой проблемой, нужно разрешить доступ к папке
"www.breda.net/lezhat_devki/index.php" и исполняемым там файлам, только с определённого хоста, то есть если чел пришёл с "www.breda.net/index.php", то разрешать исполнение, если тупо обратились к этой папке набором или сторонней ссылкой, то перенаправить на "www.breda.net/index.php"...... вот такая вот проблемка если может кто помочь отмыльте

re: HELP

мыло demon_aka@qip.ru

Проверьте переменную

Проверьте переменную $_SERVER['HTTP_REFERER']

Не работает в IE8

Полезный скрипт! В FF всё работает классно.
В IE8 появляется окно с вопросом "открыть/сохранить", если нажать "Открыть" файл прекрасно загружается в окне IE, а если "Сохранить", выскакивает окно загрузки и сразу окно ошибки: "Не удается загрузить dnld.php с www.site.ru. Не удается открыть этот узел..."
Я это понимаю, что IE8 пытается грузить файл с корня сайта.
Подскажите, что делать?

Для тех кто пытается скачать

Для тех кто пытается скачать файл через https с помощью Internet Explorer, касаемо ошибки "Не удается загрузить file.php с www.site.ru. Не удается открыть этот узел..."
Добавление вот таких заголовков поможет решить проблему:
header('Cache-Control: maxage=3600');
header('Pragma: private');

линки по теме:
http://support.microsoft.com/kb/323308
http://support.microsoft.com/kb/815313

Спасибо большое. То, что

Спасибо большое.
То, что нужно было.

Битые файлы

Отличная статья! Тестил на локалке, все отлично работает, когда залил на хостинг, файлы скачиваются битыми, в чем может быть проблема? Не могу использовать файлы после скачивания (((

Может кто-нибудь пожалуйста

Может кто-нибудь пожалуйста дать код готовый на скачивание вот с такой сыслки http://zalil.ru/дальше номер данного файла.И чтобы скачиваемый файл сохранялся в какую я захочу папку????Если кто то может сделать,скиньте код вот на этот эмейл-Lyulkov-roman@mail.ru,буду очень признателен,код хочу вставить Devel php studio.

Не в тему несколько, но...

Не в тему несколько, но... Если пользуетесь браузером Firefox, то поищите его плагины для скачивания с ютубов и прочих видеохостингов.

Увлекаюсь немного php в

Увлекаюсь немного php в свободное время, Ваша статья понравилась, интересно и доходчиво всё расписано. Спасибо!

Скажите пожалуйста, уже не

Скажите пожалуйста, уже не знаю что и делать:
При загрузке файла не показывает исходный размер файла, при чем проверял переменую в заголовке Content-Length размер присутствует но при загрузке не отображает.
Проверял на windows denver - показывает размер файла
а на сервере FreeBSD Apache 2.0 и php 5.2.17 - не отображает размер загружаемого файла

пробывал:
header("Content-Length: " . $fsize);
header("Content-Length: $fsize");
и так по вашему:
header('Content-Length: ' . ($fsize));

причем так реагируют все браузеры.

Даже не знаю, куда копать. На

Даже не знаю, куда копать. На днях у меня FreeBSD сервер появиться, посмотрю на нем...

Качает только один файл.

Почему-то если качается один файл, то другой не получается качать пока первый не скачается...и также по сайту в это время не удаётся ходить пока файл с этого сайта качается...Это почему так?

Про ограничение числа потоков

Про ограничение числа потоков я в другой заметке писал. Здесь ничего такого нет. Должно работать.

Терятся сессия

Теряется сессия когда функция скачивания файла вызывается. Как это обойти?

А подробнее можно?

А подробнее можно?

Код файла открывается в браузере

Доброго времени суток!
Большое Спасибо за Ваши полезные статьи!
Помогите разобраться со следующей проблемой.

Я при помощи формы передаю параметры на другую страницу download.php
В которой прописана последняя функция из данной статьи и после неё запускаю
file_download($filename);

Подскажите пожалуйста, почему у меня вместо закачки файла, открывается его код в браузере.

Заголовки в браузере какие?

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

Замечание

А после скачивание mp3 файлов пропадают тэги mp3 файла!!!

Видимо целый портал из-за

Видимо целый портал из-за этого не работает?

Спасибо

Работает, спасибо

Спасибо за разжованную и

Спасибо за разжованную и оформленную
инфу. Полезно для новичков

Совсем охуели

Вот кто бы не написал «Спасибо за статью», обязательно в каменте нахожу скрытые ссылки. Совсем уже спамеры разрабов за дебилов держат. Никакого уважения к старшим.