Скрипт для закачки файла с поддержкой докачки
Зачастую бывает необходимость в том, чтобы твой сайт умел отдавать файлы кому-то с умом, т.е. не просто отдавать на скачку, а поддерживать при этом возможность скачки в несколько потоков и докачки файла в случае умышленного или неумышленного обрыва соединения (такими программами, как ReGet, FlashGet и т.п.). Также может быть желание встроить счетчик скачиваний файла и т.п. Сегодня мы рассмотрим пример скрипта, позволяющего нам воплотить все перечисленные выше желания в реальность.
Перерыв Интернет и подобрав несколько примеров таких скриптов я написал на основе их свой собственный рабочий скрипт (точнее, собственную функцию), поскольку ни один из найденных мной (не буду говорить про них) не работал стабильно для больших файлов (я говорю о скачивании файлов размером, примерно, в гигабайт).
В некоторых случаях у меня стабильно скачивались файлы размером примерно до 100 Мб. Если размер оказывался выше, то файл при скачке «бился», т.е. оригинал и скачанная копия не совпадали. Но обо всем по порядку.
Отдаем браузеру файл без поддержки докачки
Попробуем просто отдать файл браузеру:
header('Content-Type: application/octet-stream'); header('Content-Disposition: attachment; filename=file.txt'); echo file_get_contents('test.jpg'); exit;
В этом примере мы сформировали два заголовка для браузера, первый из которых сообщает ему о типе содержимого (в данном случае – поток каких-то байтов), которое будет отображено, второй заголовок заставляет его выдать нам окно с именем файла для его сохранения на локальном диске. В FireFox оно выглядит примерно так:
Третьей строчкой представленного примера мы просто считываем все содержимое файла и выводим его браузеру, который в свою очередь сохраняет это содержимое на локальном диске пользователя в случае его согласия скачать файл. Потом нам обязательно нужно сделать exit, иначе к файлу приклеится информация, которая вовсе не содержалась в оригинале, т.е. любой вывод в браузер. Чтобы предотвратить какой-либо вывод мы и завершаем работу скрипта.
Этот пример годится для скачивания маленьких файлов (размером, ну, скажем, до 5-10 Мб). А те, кто сидит в Интернете по модемному соединению (такие еще есть?), уменьшат эту цифру до килобайт эдак 500. Понимаю этих людей, сам таким был когда-то
В этот пример можно встроить код для учета кол-ва скачиваний файла, например. Или ведения лога скачиваний. Что душе угодно или нужно в зависимости от ситуации.
Поддержка докачки
А как же быть с большими файлами? Пусть например 300-400 Мб. Бывает и на достаточно устойчивых линиях связи с Интернетом бывают обрывы (да и никто не застрахован, что случайно из компьютера кто-нибудь не выдернет сетевой шнур во время скачивания :)). С большими файлами возникает сразу несколько проблем. Во-первых, мы не сможем считать такой объем информации разом с помощью любой php-функции для чтения файлов, поскольку всегда есть ограничение памяти, установленное с помощью memory_limit в php.ini. На большинстве хостингов этот объем обычно устанавливается в 32Мб, т.е. скрипт php не сможет скушать больше оперативной памяти сервера, чем установлено в этом ограничении. Во-вторых, как я уже сказал, есть проблема времени скачивания. Может, пользователю нужно уйти и выключить компьютер, приостановив при этом закачку, а потом – прийти, включить машину и продолжить. Если обрыв соединения – можно продолжить закачку с оборванного места.
Получается, нам нужен скрипт, который отдавал бы браузеру содержимое файла порциями. Привожу код функции, НЕ работающей устойчиво для довольно больших файлов (чтобы Вы знали, как НЕ следует делать):
function func_download_file($filepath, $mimetype = 'application/octet-stream') { $fsize = filesize($filepath); $ftime = date('D, d M Y H:i:s T', filemtime($filepath)); $fd = @fopen($filepath, 'rb'); if (isset($_SERVER['HTTP_RANGE'])) { $range = $_SERVER['HTTP_RANGE']; $range = str_replace('bytes=', '', $range); list($range, $end) = explode('-', $range); if (!empty($range)) { fseek($fd, $range); } } else { $range = 0; } if ($range) { header($_SERVER['SERVER_PROTOCOL'].' 206 Partial Content'); } else { header($_SERVER['SERVER_PROTOCOL'].' 200 OK'); } header('Content-Disposition: attachment; filename='.basename($filepath)); header('Last-Modified: '.$ftime); header('Accept-Ranges: bytes'); header('Content-Length: '.($fsize - $range)); if ($range) { header("Content-Range: bytes $range-".($fsize - 1).'/'.$fsize); } header('Content-Type: '.$mimetype); $downloaded = 0; while (!feof($fd) && !connection_status() && ($downloaded < $fsize)) { echo fread($fd, 512000); $downloaded += 512000; flush(); } fclose($fd); exit; }
Суть этой функции – установка правильных заголовков для программы скачивания, которые сообщают этой программе, поддерживается ли докачка, и формируют информацию для дальнейшей отдачи нужного фрагмента файла.
Обратите внимание на цикл в конце функции. Я провел эксперименты по скачиванию больших файлов (как я уже писал выше, файлов, размером более 100 Мб) и могу сказать, что этот цикл почему-то «бьет» файлы при скачивании. Я не стал вникать, почему это происходит, и пошел другим путем – заменил цикл простым вызовом функции fpassthru – она отдает содержимое файла в браузер, отсчитывая от текущего положения указателя в файле (который устанавливается с помощью fseek). Снова проверив, обнаружил, что теперь функция работает нормально и для файлов размером в 1 Гб.
Итак, привожу правильный вариант этой функции (с комментариями):
// $filepath – путь к файлу, который мы хотим отдать // $mimetype – тип отдаваемых данных (можно не менять) function func_download_file($filepath, $mimetype = 'application/octet-stream') { $fsize = filesize($filepath); // берем размер файла $ftime = date('D, d M Y H:i:s T', filemtime($filepath)); // определяем дату его модификации $fd = @fopen($filepath, 'rb'); // открываем файл на чтение в бинарном режиме if (isset($_SERVER['HTTP_RANGE'])) { // поддерживается ли докачка? $range = $_SERVER['HTTP_RANGE']; // определяем, с какого байта скачивать файл $range = str_replace('bytes=', '', $range); list($range, $end) = explode('-', $range); if (!empty($range)) { fseek($fd, $range); } } else { // докачка не поддерживается $range = 0; } if ($range) { header($_SERVER['SERVER_PROTOCOL'].' 206 Partial Content'); // говорим браузеру, что это часть какого-то контента } else { header($_SERVER['SERVER_PROTOCOL'].' 200 OK'); // стандартный ответ браузеру } // прочие заголовки, необходимые для правильной работы header('Content-Disposition: attachment; filename='.basename($filepath)); header('Last-Modified: '.$ftime); header('Accept-Ranges: bytes'); header('Content-Length: '.($fsize - $range)); if ($range) { header("Content-Range: bytes $range-".($fsize - 1).'/'.$fsize); } header('Content-Type: '.$mimetype); fpassthru($fd); // отдаем часть файла в браузер (программу докачки) fclose($fd); exit; }
Вполне возможно, что я в чем-то здесь ошибаюсь, и с удовольствием приму ваши предложения и замечания в комментариях к этому посту :), чтобы улучшить эту функцию и сделать ее более устойчивой к большим объемам данных.
До встречи!
В cakephp уже есть View, которое называется Media.
Хорошая статья
Так а каким образом тогда можно посчитать количество скачиваний файла? В начале поста об этом упоминалось, а в функции я что-то этого не заметил. Опишите, хотя бы примерный алгоритм, как добавить в эту функцию еще и счетчик скачиваний (именно скачиваний, а не вызовов этой функции).
Можно, например, вести таблицу в базе, в которой для определенного имени файла вести поле, например, downloads_count, и поле количества скачанных байт для этого файла, bytes_downloaded. Внутри приведенной функции скачивания добавить код после fclose, который будет считать количество скачанных байт всего с помощью базы данных (за все время существования ссылки на этот файл) и делить на размер файла. Получившееся вещественное число (а может быть и целое), округленное в меньшую сторону, покажет кол-во скачиваний. Если нужно учитывать кол-во уникальных скачиваний, добавляем еще одну таблицу, чтобы как-то идентифицировать каждого пользователя (IP-адрес, браузер, … - вариантов много). Этот алгоритм я сейчас написал сходу. Может есть варианты и лучше. Если есть - предлагайте 😉
Если использовать такой вариант, то погрешность, мне кажется, будет достаточно большой. т.к. не мало людей прерывает закачки или они у них срываются сами, после чего скачивают песню вновь.
В зависимости от того, сколько процентов от объема файла было скачано, меняется и погрешность.
Из 10 мб файла к примеру человек скачал 400 кб, а потом отключил. - это еще не страшная погрешность, а вот есть скачал 7-9 мб и по какой-то причине прервалась закачка, то это уже существенно влияет расчет скачанных файлов.
В любом случае, спасибо. Это описание подтолкнуло на новые мысли.
Если найду решение меня удовлетворяющее - поделюсь
Спасибо! С удовольствием посмотрю это решение. Можно даже отдельный пост ему посвятить
спасибо,
хорошо что есть еще те, кто не ленится делится своими знаниями
Так это не про закачку, а про скачивание. Я думал про upload с возможностью возобновления.
А это все просто. К тому же, если отдавать файл порциями - можно регулировать скорость скачивания. Но это очень сильно будет жрать ресурсы системы.
Следующие конструкции в примерах
$range = $_SERVER[‘HTTP_RANGE’];
$range = str_replace(‘bytes=’, ”, $range);
$range = str_replace(‘-‘, ”, $range);
загоняют в $range границы диапазона в виде одного числа.
Так, из ‘bytes=163091-317864′ получается $range=’163091317864′.
Скорее всего, требовалось не это …
Да, тут предполагается, что в $range содержится только нижняя граница диапазона, т.е. “bytes=163091-“. Поэтому, мы тут обрезаем bytes= и тире. По крайней мере FlashGet и ReGet дают HTTP_RANGE без верхнего диапазона.
А в виде готового модуля этот скрипт случайно не существует? для друпал или вордпресс
Google в помощь 😉
Не отслеживал какой софтиной пользователь забирал файл (предполагается браузером), ситуция ‘bytes=163091-317864? реально возникла. Пришлось допиливать.
Хм, спасибо за замечание. Надо будет исправить.
Реально работает!
Нашёл до этого скрипт, очень похожий, но без $range. Докачка не работала.
Доработал по ЭТОМУ примеру - всё ОК!
Благодарю!
что делать если выдается ошибка
Internal Server Error
The server encountered an internal error or misconfiguration and was unable to complete your request.
wert, попробуйте вот тут почитать: [ссылка]
искал просто заголовок браузеру? дающий команду на скачивание:
header(‘Content-Disposition: attachment; filename=file.txt’);
Thanks!
Спасибо, за толковый скрипт, а то есть аналогичные и не корректно работающие, например вместо $_SERVER пишут $HTTP_SERVER_VARS а из за этого HTTP_RANGE не читается и дозакачка не производится. Может $HTTP_SERVER_VARS[“HTTP_RANGE”] это лишь для старых версий пхп… или наоборот..
и прийму в рассмотрение fpassthru($fd);
Воспользовался вашим скриптом , на локальном серваке все работает как нужно , но стоило мне перенести на хостинг, как появилась проблема , имя файла не передаеться, те файл скачивается с именем donload.php , хотя в самом начале при отдаче файла , браузер распознает его как медиа файл, и предлагает прослушать.
Советую допилить еще койчего. Заголовок “Content-Range” отдавать нужно лишь тогда, когда выдается ответ “206 Partial Content”. Сейчас он отдается и при ответе “200 OK”. Соответственно, строчке 32 добавить условие, и сделать ее таким образом:
if ($range) header(“Content-Range: bytes $range-“.($fsize - 1).’/’.$fsize);
Кроме этого полезно избавиться от заголовка “Content-transfer-encoding: binary”. Он не нужен в принципе здесь.
Браузерам на эти недоработки пофиг, а вот яндекс после одного из апдейта полгода назад поругался на несоответствие заголовков протоколу HTTP и выбросил из индекса урлы отдаваемые этим скриптом (отдавались через этот скрипт PDF-ы, а они хорошо индексируются).
Спасибо, исправил
Единственное в MIMETYPE указал “audio/mpeg”.
Чтобы совпадало по типу.
Больше ничего не трогал.
Перепроверили, вроде всё ок уже, извините за беспокойство. Но после скачивания большого файла +1 к скачке почему-то не идёт. Маленькие плюсует отлично! Установили set_time_limit(0). Надеемся поможет. Как совсем решим проблему, сразу отпишусь.
А реально реализовать счётчик скачиваний с прямой отдачей файла? Не через PHP?
$range = str_replace(“-“, “”, $range);
заменить на
$range0 = explode(“-“, $range);
$range = $range0[0];
или что-то в этом роде
Так скрипт, если не успеет отдать за 30 секунд всё содержимое (большого) файла, он (PHP) выдаст ошибку на ограничение в 30 секунд (по умолчанию). Или я не прав???Напишите пожалуйста ответ на
2Mike:
Не так. Он (PHP) выдаст ошибку, если скрипт не успеет за 30 секунд ПОДГОТОВИТЬ данные перед отправкой. 30 секунд распространяются именно на эту стадию, а отдача данных может происходить и гораздо большее время в зависимости от размера данных и скорости канала.
Попробовал и предложенную Вами функцию, столкнулся с такой
проблемой:
Файл “бьется” в случае загрузки менеджерами закачек. Размеры файлов
скачанных браузером и DM(Download Master) совпадают, но в последнем
ошибка “Неожиданный конец архива”.
Тестировалось на различных размерах файлов (от 3Мб до 100Мб) - итог
один в случае с браузером ОК, в случае с DM ERROR.
Подскажите, в чем может быть причина битости?
P.S. На Download Master грешить не стоит, т.к., файлы с других сайтов и по другим ссылкам он качает на УРА
“один в случае с браузером ОК, в случае с DM ERROR”.
В случае с DM возникает еще одна проблема - DM получает ссылку не на файл для скачивания, а на php-скрипт …
Приходится отключать интеграцию его в браузер.
Ну, собственно, скрипт-то ориентирован именно на браузер.
Но если DM перехватывает закачку, то реально - проблема. Не то качает!
Nickolas
А как решить проблему?
Поясню:
1. DM не перехватывает закачку. Я просто добавляю ссылку вручную в список закачек. (ссылка имеет вид download.php?id=)
2. Если в функции отключить поддержку докачки, то всё работает отлично. Т.е., файл бьется в то время, когда DM делит файл на несколько потоков.
Как побороть эту бяку? Уже спасибо.
…и кстати отдаю скриптом удаленный (файл на другом сервере) файл… Вначале узнаю по его заголовкам размер файла и потом в данную функцию передаю этот размер..
может как то это влияет?
Думаю здесь нет решения. Во всяком случае с использованием fseek
функция fseek не применима для работы с удаленными файлами, поскольку файл должен быть доступен через файловую систему сервера.
Остается копировать удаленный к себе, а уж потом отдавать.
Может кто то знает другой вариант решения задачи?
Здравствуйте!
Спасибо за статью!
У меня два вопроса:
- Какие файлы и папки можно удалить за ненадобностью из скачаной мной с сайта библиотеки объёмом 11 мб?
- Не повлияет ли на безопасность наличие в подключаемых файлах (htmlcolors.php, tcpdf.php, unicode_data.php, eng.php и т.п.) наличие объявленых глобальных переменных?
С уважением, Василий
Для того, чтобы работал первый вариант, причем на отлично необходимо в начале добавить строку set_time_limit(0) , чтобы не выбивало закачку через каждые сек 20-30.
интересно, если в конце не добавить закрытие потока и выход, то на файле 1.4 гб Опера не докачивает где-то мб 8, а FlashGet берет откуда-то 5кб сверх размера файла 😉 интересный и не понятный мне парадокс)
Первый вариант и правда работает лучше!
Во втором у меня не работает докачка, а в первом как часы (ну в смысле хорошие часы )
А вот по поводу set_time_limit(0) не понял. Куда нужно ее вставлять?
Просто в начало функции вставить эту строку и все?
Проблема работы скрипта на реальном сайте!
Когда тестировал на localhost (Денвер), все работало нормально, но вот при попытки скачать через скрипт (оба варианта себя ведут одинаково) с реально сайта (с сервера хостинга), происходит следующая проблема:
Файл лежит у хостера в формате zip, а скачивается SFX zip, что приводит к не читаемости файла!
Подскажите, в чем может быть дело?
set_time_limit(0) ставится перед функцией. Он убирает ограничение времени работы скрипта.
Проблему с архивом можно решить следующим способом:
- на сервере какие-то специфические настройки по отдаче через PHP, узнайте, поправьте, но прежде попробуйте следующее..
- установите правильный mimetype в header(‘Content-Type: ‘.$mimetype); ZIP mime type — application/zip.
- пропишите название самостоятельно в header(‘Content-Disposition: attachment; filename=file.zip’); Именно ZIP и задайте его строго, чтобы никакие настройки не переименовывали его.
Спасибо!
С Content-Type: уже по всякому экспериментировал.
Но ведь проблем с типом файла нет!
Приходит нормальный .zip, только не распаковывается! А если заглянуть в свойства архива, то там вместо ZIP указано SFX ZIP и размер SFX-модуля пишет 3 байта.
Восстановление архива проходит нормально, после чего архив спокойно начинает распаковываться. Ну это и понятно, т.к. скачанный архив имеет такой же объем, что и оригинал!
Если такая проблема, только у меня, то тогда пойду к хостингу “разговаривать” ))
Еще раз спасибо!
toДмитрий:
Подозреваю, что принимающий браузер проявляет излишний “интеллект”, заменяя лишь расширение имени файла с “.zip” на “.exe”
Попробуйте:
1. вручную изменить расширение имени принятого файла с “.exe” на “.zip”
2. принять файл другим браузером.
Спасибо за совет, но в моем случае, скорее наоборот, с .exe на .zip, т.к. у меня сохраняется .zip.
Попробовал поменять с .zip на .exe, но SFX архивом, файл не стал ((
Но это уже не важно, походу у хостинга, что-то настроили, сейчас думал у себя на сайте выложить данный скрипт для скачивания, чтобы Вы могли тестить, но как оказалось файл пришел нормальным zip, без SFX.
Оперативно ребята среагировали на мой запрос ))
Сейчас еще протестирую, потом отпишусь.
Никакого преобразования файла ни скрипт, ни браузер не делают. Если какие изменения и происходят, то только - с именем файла или его расширением.
Для предотвращения такого хулиганства каким-то браузером добавил в начале - явное указание типа передаваемого файла
header(“Content-Type: application/rar”);
Все! ))
Всем, спасибо за помощь!
Качает нормальные .zip
Не знаю, что там было у хостера не так, но теперь все отлично!
Оба варианта, отлично работают (если хост правильно настроен!)!
Спасибо автору статьи, за столь полезные скрипты!
Остался один вопрос:
Где в этом скрипте, можно подсчитать сколько байт было загружено?
Делаю опцию, которая покажет сколько было обращений к файлу и сколько было полных скачиваний.
Т.е. обратились к скрипту, файл для скачивания найден, записали в базу “+1 обращение” (т.е. еще не скачали, а только обратились). Далее скачали ровно столько байт, сколько сам файл весит, записали “+1 скачивание” (т.е. теперь скачали).
А то бывает, что пользователь нажал отмена при закачивании, а я считаю, что скачали, а на самом деле только обратились. Ну и лимит на скачивание, тоже удобно делать. А то так пользователь три раза прерывал и восстанавливал закачку (по разным причинам), а у меня уже “- 3 лимита”, что очень не правильно!
Пытался привязываться к $downloaded и сравнивать его с $fsize, но не то. $range - имеет значение, только когда была пауза.
Помогите, пожалуйста, а что-то я не соображу (
Для учета количества успешных скачиваний надо считать количество окончаний скачивания, а не количество переданных байт.
Можно например последний байт передавать отдельно и подсчитавать количество переданных последних байтов - это и будет количество скачиваний.
Nicky спасибо за совет, но вот пример реализации крайне необходим )
Реально, никогда с файлами не работал, как и чего считать понятия не имею!
Не, ну после байт определить можно, но как его из общего потока выдрать да еще потом и обратно запихнуть…?
Если есть опыт, помогите
Доброе время суток,у меня появилась проблема отдача больших файлов, нашел ваш скрипт- по описанию подходит, если можно напишите подробнее коментарии , с примерами функций и как должны быть прописаны. если не здесь то на емеил!
прошу смльо не пинать я в этом деле чайник. не работает скрипт, запускаю на локалхост, браузер выдает чистую страницу, на другом подобном скрипте скачивание работает.
подскажите может не правильно указываю путь к файлу, или на сервере что то не так?
$filepath = ‘file.rar/';
$mimetype = ‘.rar';
скрипт лежит в той папке что и фаил.
А как узнать что файл скачан?!
У меня генерирутся файлы на скачивание во временную папку. После скачивания мне бы их удалить!
Использовал разные скрипты отдачи файлов от самых простых (из 2 строчек) до подобных (поддерживающих докачку). Везде сталкивался с проблемой битья файлов. Сервер стоит на Windows XP, скрипты в UTF-8, тест провожу на файле картинке png с прозрачным фоном, файл скачивается вроде весь, но каждый раз по разному: при открытии может весь показываться, а может только половина. Открывал полученный файл в текстовом редакторе и сравнивал с оригиналом: в полученном файле присутствуют блоки с пустыми строками, из-за этого в нем больше строк, но размер тот же и файлы заканчиваются одинаково.
В чем может быть проблема?
проблема вроде бы решилась: уменьшил 512000 до 8192.
еще заметил, что нагрузка на CPU прилично скачет,
но при скачивание на прямую без скрипта нагрузка приблизительно такая же.
еще прочитал что fpassthru($fd) закрывает файл по завершении функции и получается fclose($fd) не нужен.
могу предложить такой вариант:
создавай в временной папке папку с хэшем для сессии в которую будешь кидать файлы для скачивания.
по истечении времени сессии удаляй папку с хэшем.
это можно делать скриптом по расписанию.
нужно $mimetype = ‘application/rar’;
Возможно я не прав, но качалки часто скачивают в несколько потоков частями и последний байт может быть не последним скачан.
Именно подсчитать полных загрузок без учета не до конца скачаных помоему не получится. Лучше делать подсчет при нажатии на ссылку для скачивания: 1 клик - одна попытка чкачать, а если закачка будет ставиться на паузу или прерываться вы никогда не сможете потом догадаться когда он ее докачает полностью. Можно вести запись для текущей сессии сколько и какие части были скачаны, но в таком случае скрипт будет работать корректно только при единовременной закачке файла за одну сессию.
Ты бы сделал еще готовый зип архивчик с готовым своим решением, цены бы не было)
Автор, люблю тебя! Великолепная статья, очень помогла, долго не мог найти настолько удачный скрипт. Тестировал на файлах объёмом до 8 Гб - шикарно работает - быстро и докачка поддерживается! Спасибо!
а какой будет скрипт не скачивания с сервера а скачивания на сервере больших файлов с какого то url например фильмы когда бывает проблемы с обрывом интернета? Спасибо.