Пользователь
0,0
рейтинг
19 сентября 2012 в 18:53

Разработка → Отдаем файлы эффективно с помощью PHP

Если Вам потребовалось отдавать файлы не напрямую веб сервером, а с помощью PHP (например для сбора статистики скачиваний), прошу под кат.

1. Используем readfile()


Метод хорош тем, что работает с коробки. Надо только написать свою функцию отправки файла (немного измененный пример из официальной документации):

function file_force_download($file) {
  if (file_exists($file)) {
    // сбрасываем буфер вывода PHP, чтобы избежать переполнения памяти выделенной под скрипт
    // если этого не сделать файл будет читаться в память полностью!
    if (ob_get_level()) {
      ob_end_clean();
    }
    // заставляем браузер показать окно сохранения файла
    header('Content-Description: File Transfer');
    header('Content-Type: application/octet-stream');
    header('Content-Disposition: attachment; filename=' . basename($file));
    header('Content-Transfer-Encoding: binary');
    header('Expires: 0');
    header('Cache-Control: must-revalidate');
    header('Pragma: public');
    header('Content-Length: ' . filesize($file));
    // читаем файл и отправляем его пользователю
    readfile($file);
    exit;
  }
}

Таким способом можно отправлять даже большие файлы, так как PHP будет читать файл и сразу отдавать его пользователю по частям. В документации четко сказано, что readfile() не должен создавать проблемы с памятью.

Особенности:
  • Скрипт ждет пока весь файл будет прочитан и отдан пользователю.
  • Файл читается в внутренний буфер функции readfile(), размер которого составляет 8кБ (спасибо 2fast4rabbit)

2. Читаем и отправляем файл вручную


Метод использует тот же Drupal при отправке файлов из приватной файловой системы (файлы недоступны напрямую по ссылкам):

function file_force_download($file) {
  if (file_exists($file)) {
    // сбрасываем буфер вывода PHP, чтобы избежать переполнения памяти выделенной под скрипт
    // если этого не сделать файл будет читаться в память полностью!
    if (ob_get_level()) {
      ob_end_clean();
    }
    // заставляем браузер показать окно сохранения файла
    header('Content-Description: File Transfer');
    header('Content-Type: application/octet-stream');
    header('Content-Disposition: attachment; filename=' . basename($file));
    header('Content-Transfer-Encoding: binary');
    header('Expires: 0');
    header('Cache-Control: must-revalidate');
    header('Pragma: public');
    header('Content-Length: ' . filesize($file));
    // читаем файл и отправляем его пользователю
    if ($fd = fopen($file, 'rb')) {
      while (!feof($fd)) {
        print fread($fd, 1024);
      }
      fclose($fd);
    }
    exit;
  }
}

Особенности:
  • Скрипт ждет пока весь файл будет прочитан и отдан пользователю.
  • Позволяет сэкономить память сервера

3. Используем модуль веб сервера


3a. Apache

Модуль XSendFile позволяет с помощью специального заголовка передать отправку файла самому Apache. Существуют версии по Unix и Windows, под версии 2.0.*, 2.2.* и 2.4.*

В настройках хоста нужно включить перехват заголовка с помощью директивы:
XSendFile On

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

Описание возможных опций на сайте разработчика: https://tn123.org/mod_xsendfile/

Пример отправки файла:

function file_force_download($file) {
  if (file_exists($file)) {
    header('X-SendFile: ' . realpath($file));
    header('Content-Type: application/octet-stream');
    header('Content-Disposition: attachment; filename=' . basename($file));
    exit;
  }
}


3b. Nginx

Nginx умеет отправлять файлы из коробки через специальный заголовок.

Для корректной работы нужно запретить доступ к папку напрямую через конфигурационный файл:
location /protected/ {
  internal;
  root   /some/path;
}

Пример отправки файла (файл должен находиться в директории /some/path/protected):

function file_force_download($file) {
  if (file_exists($file)) {
    header('X-Accel-Redirect: ' . $file);
    header('Content-Type: application/octet-stream');
    header('Content-Disposition: attachment; filename=' . basename($file));
    exit;
  }
}

Больше информации на странице официальной документации

Особенности:
  • Скрипт завершается сразу после выполнения всех инструкций
  • Физически файл отправляется модулем самого веб сервера, а не PHP
  • Минимальное потребление памяти и ресурсов сервера
  • Максимальное быстродействие


Update: Хабраюзер ilyaplot дает дельный совет, что лучше слать не application/octet-stream, а реальный mime type файла. Например, это позволит браузеру подставить нужные программы в диалог сохранение файла.
Роман Паска @T2L
карма
30,2
рейтинг 0,0
Реклама помогает поддерживать и развивать наши сервисы

Подробнее
Реклама

Самое читаемое Разработка

Комментарии (57)

  • +27
    А можно передать отправку файла чему-нибудь более заточенному для работы со статикой и медленными клиентами, например nginx:
    header(«X-Accel-Redirect: ».$file);die;
    • +6
      Метод эквивалентный третьему, но с nginx. Удивлен, что не увидел его в статье.
      • +3
        Спасибо, надо обновить статью.
    • 0
      Всегда использовал этот метод, т.к. веб сервер и предназначен для отдачи файлов клиенту.
      Для каждой задачи должен использоваться подходящий инструмент.
      А первые 2 метода я бы не публиковал. Вижу строчку, которая будет необоснованно кушать лишнюю память и не читаю код дальше.
      • 0
        Первые два метода не только плохи, но и легко кладут сервер на лопатки при средней нагрузке.
        • 0
          Я думаю есть проекты, где нет возможности ставить свои модули для Apache или тот же Nginx.
          • 0
            Не верю. Приведите примеры. Берем Apache и связываем с nginx. Апач обрабатывает динамику, nginx берет на себя статику.
            Если хостинг не предоставляет необходимых инструментов, в топку такой хостинг.
            • +1
              Если есть уже nginx, то я не особо вижу смысла ставить Apache. Разве что в очень специфических случаях с нехваткой каких-то определенных модулей.
              • +1
                .htaccess? Если на сервере куча сайтов на различных движках — не хватит никаких рук переписывать кучи .htaccess в конфиги nginx.
                • 0
                  Это как раз и относится к специфическим случаям.
        • 0
          Не нужна даже средняя нагрузка. Достаточно большого файла и 1-2 клиентов на медленном канале, соответственно с бешеным (иначе на плохом канале совсем не жизнь) download manager, качающим в несколько потоков.
          • 0
            А разве первые два метода не отдают файл постепенно? Мне кажется, несколько потоков тут никакого профита не дадут.
            • 0
              В том то и дело, что они отдают постепенно и пока не отдадут полностью процесс (поток) висит. А кол-во таких процессов ограничено настройками веб-сервера, т.е. если качать кол-во файлов равное кол-ву процессов, то сайт перестанет отвечать.
              • 0
                Я о том, что 2 и даже 5 клиентов не положат сервер, так как качалка не будет качать в несколько потоков — смысл от них при последовательному скачиванию. Но да — это далеко не лучший вариант отдачи контента.
    • 0
      Подскажите, при этом методе работает докачка?
  • 0
    Кто-нибудь подскажет, как быть с IIS?
    У меня файл лежит на диске с уникальным именем, но в момент отдачи клиенту имя файла меняется на вменяемое, поэтому приходится делать что-то, типа первого варианта. А хотелось бы обойтись без php. Это реально?
    • +4
      Вы можете указать любое имя, например:

      header('Content-Disposition: attachment; filename=FAKE_NAME.TXT');
      

      Оно не должно совпадать с именем на сервере. Пользователю браузер должен показать именно FAKE_NAME.TXT
      • –1
        Это я знаю, хотелось как раз обойтись при отдаче файла БЕЗ PHP — каким-нибудь хитрым редиректом именно в IIS,
        что-нибудь типа
        header(«x_redirect:real_name,fake_name), чтобы после этого сработали какие-нибудь механизмы IIS или расширения и файл бы отдался штатными средствами, но с новым именем.
        • 0
          В заголовке.
          с помощью PHP
          • 0
            не ясно, за что минус человеку влепили. Явно вопрос был, об альтернативах XSendFile и X-Accel-Redirect под IIS.
            Посмотрите здесь.
            www.helicontech.com/ape/
      • 0
        А как быть с русскими символами вместо FAKE_NAME.TXT?
        • 0
          Можно попробовать отталкиваться от RFC2231 и заворачивать примерно так:
          header("Content-Disposition: attachment; filename*=\"utf8'ru-ru'кириллический utf8 текст\"");
        • 0
          Пользователь remal рекомендует статью на stackoverflow
  • –7
    Последний метод хорош, но негодится, если нужно ограничить доступ к файлу (только для авторизованных пользователей). Как решение — нужно создавать симлинк на файл с произвольным именем и периодически подчищать симлинки
    • +3
      Последний метод у меня как раз используется в системе, где все файлы приватные. Доступ к директории закрыт в .htaccess. Файлы отдаются PHP скриптом, который делает все проверки (в реальности у меня Drupal).
    • +1
      Извините, симлинки это тотальный капец, и главное зачем?
      Сверху nginx, за ним любой сервер. Предположим, что все «файлы» просятся из папки /files/. Например /files/some-file.file.
      В конфиге nginx'а говорим, что такие файлы нао спросить у backend'а по такому то пути. На беке, хоть средствами того же php, проверяем авторизацию пользователя, если все ок — выдаем заголовки с mime-type и т.п. и через x-accel-redirect выдаем на nginx реальное имя файла, вычисленное каким-то кастомным образом из some-file.file. Для таких файлов в ngixn делаем internal location. Если юзер не авторизован или еще что-то не так, выдаем backend'ом 403, 404 или что вам там нужно
  • 0
    В случае аудио или видеофайлов первый метод не подойдет, так как браузер запрашивают файлы по частям, передавая заголовок Range. Придется обрабатывать его вручную, как описано в статье, но проще воспользоваться заголовками X-Accel-Redirect / X-SendFile.
  • +9
    >>> Файл читается в внутренний буфер функции readfile(), размер которого нигде не указан (может кто подскажет)

    Функция readfile использует внутреннию функцию php_stream_passthru для отправки файла в output буффер. Сама по себе функция php_stream_passthru это алиас на функцию _php_stream_passthru в коде которого есть декларация буффера размером 8192 символов. Это означает что файл читаеться и отправляеться порция по 8 кБ.

    Пруф линки:

    github.com/php/php-src/blob/master/ext/standard/file.c#L1345
    github.com/php/php-src/blob/master/main/php_streams.h#L448
    github.com/php/php-src/blob/master/main/streams/streams.c#L1378
    • 0
      Спасибо! Добавлю в пост.
  • 0
    header('Content-Type: application/octet-stream');
    • +3
      Случайно отправил недописанный коммент.
      Я бы читал content-type из файла, а лучше хранить что бы файл не дергать из PHP лишний раз.
      • 0
        Реальное приложение так и делает :) Drupal кстати записывает mime type в таблицу файлов тоже. Всё как Вы и описали.
  • +9
    А еще можно читать HTTP-RANGE и отдавать то, что запросил клиент, тогда получим докачку файлов.
  • 0
    Допустим, мы скриптом отдаем картинку. В хэдере пишем Content-Type: image/jpeg, но не указываем сontent-disposition (не attachment). Браузер ее корректно отображает. Но при ручном сохранении картинка сохраняется под именем имя_скрипта.php. Как предложить корректное имя?
    • +1
      Можно сделать так:
      Делаем новый обработчик пути в системе, например /file/private. Далее все запросы к картинкам делаем через него в формате /file/private/path/inside/your/private/directory/picture.jpg. Обработчик пути должен выдать картинку исходя из параметров переданных скрипту.
      Работать будет только с включенным mod_rewrite.
      Для браузера путь картинки будет обычным, соответственно при сохранении будет показано имя файла.

      Идея нагло подсмотрена в Drupal (обработка приватных файлов).
      • 0
        Да, забыл про этот способ. Надо посмотреть как это реализовано у Drupal.

        Кстати, у vbulletin проблема с сохранением файла решена — там прикрепленные картинки также отдаются скриптом, но сохраняются под нормальным именем.
    • 0
      Кажется, таким поведением грешит только хром.
      Я пользуюсь как раз решением, описанным в комментарии выше (расширение в урле).
      От себя хочу добавить: в таком случае лучше ещё сделать и правильное имя файла, чтобы у сохраняющего не получились файлы типа picture(5).jpg
    • 0
      полные заголовки таких ответов бы привели что ли
  • +8
        // сбрасываем буфер вывода PHP, чтобы избежать переполнения памяти выделенной под скрипт
        // если этого не сделать файл будет читаться в память полностью!
        if (ob_get_level()) {
          ob_end_clean();
        }
    


    output buffering может быть вложенным, поэтому правильным вариантом будет такой:
    while (ob_get_level()) {
      ob_end_clean();
    }
    
    • +1
      while (@ob_end_clean());
      
    • 0
      кстати если PHP не сможет очистить буфер, то наверное лучше выйти из функции, иначе файл все равно будет читаться в память
    • 0
      как раз напоролся на это, спасибо!
  • +7
    Согласно заголовку: «Отдаем файлы эффективно с помощью PHP»
    стоило бы провести хоть маленькое исследование на предмет эффективности.
    • 0
      первые два метода — это как отдавать НЕ ЭФФЕКТИВНО
      так что тут проводить на эффективность нечего
      • 0
        мой опыт работы говорит тоже самое,
        но! вдруг, автор знает особое конфуособые настройки, чтобы эти методы нормально работали.
        и поэтому тестирование хоть какой либо нагрузки, помогло бы автору, закончить статью. и написать реалии использования того или иного метода
        • 0
          чтоб работали особые настройки, нужны особые нагрузки… а без нагрузок смысл говорить об эффективности? что выиигрываем, что проигрываем — один черт, все равно работает ;) и пофиг как работает…
          а вот нагрузочку даем, сразу видно что, где и как
  • +1
    Третий метод должен был быть первым.
  • +1
    В целом статья хорошая, все правильно расписано про заголовки, но файлы отдавать напрямую РНР — это Зло, нет — это даже очень Большое Зло! Так что первые два пункта, можно рассматривать только как пример того, как не надо делать.

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

    Как в принципе и закачивать надо средствами WEB сервера а не РНР.
    Разработано куча специальных модулей. Я с Апачем не работаю лет уже как пять, и кроме XSendFile — я не знаю, но для nginx есть модули: X-Accel-Redirect,
    Для закачки надо использовать ngx_upload_module — на эту тему статьи были на Хабре
    • +1
      для создания защищенной зоны от скачивания есть модули accesskey wiki.nginx.org/HttpAccessKeyModule
      отдача приватных файлов организуется с помощью этих двух модулей
  • 0
    а что если мне нужно отдать юзеру файл не со своего сервера а с внешнего? + учесть вариант «докачки» файла?

    и, важный момент, редирект на сам файл не считается =)
    • 0
      Проксировать файлы на php это еще хуже первых двух вариантов.
      Можно за nginx'ом запустить хоть на python сервер прокачки, отдающий ответ nginx'у. Так его сложнее будет «положить».
      Если докачка не поддерживается источником, то тогда в любом случае придется предварительно выкачивать весь файл.
  • +2
    Аналогичный модуль есть и для lighttpd: blog.lighttpd.net/articles/2006/07/22/mod_proxy_core-got-x-sendfile-support
  • +1
    Еще, полезно было бы упомянуть про ETag и набор http-функций для избежания велосипедов, в частности эти:
    http_cache_etag();
    http_send_file();

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