Php куда сохранить временный файл

Обновлено: 04.07.2024

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

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

3 ответа

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

Итак, чтобы гарантировать , что PHP может предоставить все данные $_POST , PHP должен обработать весь запрос. Так что он может завершить суперглобальный $_FILES , пока он там.

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

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

Теперь вы можете сохранить данные этого файла на RAM-диске, если хотите, на скорость. Это хороший подход, если вы не возражаете против затрат на инфраструктуру (например, поддержание настройки RAM-диска). Но обратите внимание, что это не то же самое, что PHP, удерживающий его в самой ОЗУ: в этом случае процесс контейнера PHP (обычно Apache или какой-либо другой веб-сервер) должен иметь кучу для хранения файла (чего может и не быть). В этом случае RAM-диск управляется ядром.

Две дополнительные причины:

Если вы по какой-либо причине решите не принимать файл, он будет храниться во временном местоположении и, вероятно, будет автоматически удален в какой-то момент.

Безопасность. Допустим, PHP был настроен на загрузку в доступный через Интернет каталог, например / images. Кто-то может загрузить какой-нибудь хакерский файл и затем выполнить его. Поместив сначала файлы во временный каталог (который обычно не будет доступен через Интернет), PHP позволяет сначала изучить файл. Например, при обработке изображений удаляются любые комментарии, которые могут содержать код PHP.

Когда PHP-программисту необходимо создать временный файл, он в мануале находит функцию tmpfile() и после изучения примеров начинает думать, как её лучше применить. Так было и со мной, когда мне потребовалось выгрузить данные сразу во временный файл, а не работать с ними через переменную. Но с файлом, созданным таким образом, в дальнейшем неудобно работать в силу того, что tmpfile() возвращает дескриптор, а не ссылку на локальный файл. Давайте немного углубимся в анатомию временного файла и рассмотрим подводные камни, с которыми мне пришлось столкнуться.

Временные файлы в PHP нужны, например, для загрузки большого количества данных из сети или выгрузки данных из БД. Сохранять мега- или гигабайты дынных в переменной не самая лучшая идея, поскольку потребление памяти интерпретатором и сервером ограничено. Лучшим вариантом является сохранение данных на диске и удаление их по результату обработки. Функция tmpfile() именно так и работает, но в PHP существуют и другие нативные способы, с помощью которых можно организовать работу с временными данными:

Временные данные, созданные через fopen() или tmpfile() , будут уничтожены самим PHP после завершения скрипта или когда вы принудительно закроете ресурс с помощью функции fclose() . Если будет брошено исключение, PHP всё равно уничтожит временные данные, но если вылетит фатальная ошибка или вы просто вызовете exit() , нет гарантий, что временные данные будут удалены.

Получаем URI временного файла

У вас может появиться необходимость «придержать» временные данные и поработать с ними на уровне физических файлов. Этого можно достичь, если открыть поток php://temp/maxmemory:0 или вызвать tmpfile() , а затем, до закрытия ресурса, воспользоваться stream_get_meta_data() , чтобы извлечь метаданные и узнать абсолютный путь к файлу для дальнейших манипуляций:

Какие значения возвращает stream_get_meta_data() хорошо описано в документации, но нас больше интересует абсолютный путь к файлу, связанный с потоком. Его можно извлечь по ключу uri из полученного массива:

В случае с php://temp/maxmemory:0 мы не сможем получить URI файла из метаданных потока, но фактически файл будет создан во временной папке. Функция stream_get_meta_data() по ключу uri будет возвращать название потока. Поэтому для получения URI временного файла на диске, нужно использовать функцию tmpfile() . Другого способа узнать, где физически хранится временный файл, не существует.

Существует класс SplTempFileObject, который является всего лишь ООП-обёрткой на потоком php://temp и не более. Данный класс наследуется от SplFileObject, а тот в свою очередь от SplFileInfo. Это значит, что у SplTempFileObject должны быть доступны такие методы, как getFilename() , getPathInfo() , getSize() , но они не отработают так, как ожидается. Начиная с версии 5.3 закрался баг, который возвращает false для вышеперечисленных методов. В версии 7.4 ничего не изменилось.

Проблемы функции tmpfile()

Когда вы получили URI временного файла и записали в него данные, может потребоваться сохранить этот файл в нужной вам папке на диске. Проще всего и правильнее сделать это через функцию rename() , т. е. просто переименовать файл вместо физического перемещения данных.

К сожалению, сохранить временный файл через rename() не получится, т. к. tmpfile() для Windows накладывает блокирующий режим и файл будет всё ещё занят другим процессом. В UNIX-системах такой проблемы нет, здесь временный файл можно удалять ещё до закрытия ресурса. Другими словами, для Windows вызов rename() до fclose() приведёт к ошибке:

Использовать rename() после fclose() не целесообразно, т. к. после закрытия ресурса PHP сразу же удалит временный файл. Тем не менее, мы можем сохранить временный файл через копирование, воспользовавшись функцией copy() . Этот способ рабочий, но при копировании временного файла на сервере будет храниться две копии файла до завершения скрипта.

Создание временного файла

Для альтернативного решения создадим свой временный файл, который будет базироваться на функции tempnam() . Функция только создаёт файл с уникальным именем на диске, но автоматическое удаление файла не предусмотрено. Поэтому для его уничтожения нам понадобится написать свой обработчик. Жизненный цикл временного файла будет следующий: 1) создание файла во временной папке; 2) манипуляции с файлом; 3) автоматическое удаление.

Первым аргументом указывается расположение временной папки через sys_get_temp_dir() , а вторым — префикс в имени файла. Такой файл доступен для чтения и записи только владельцу, т. к. создаётся с правами 0600 (rw-). Для реализации автоматического удаления файла можно перенести дальнейшую логику в класс, где с помощью __destruct() попробуем удалить файл:

Объект вернёт ссылку на файл, который создала функция tempnam() , т. к. в классе прописан __toString() . Таким образом, мы избавились от работы с ресурсом. Временный файл будет удалён при освобождении всех ссылок на объект или по завершению скрипта, но до того случая, пока не будет вызвана фатальная ошибка, брошено исключение или вызвана функция exit() .

Полагаться на __destruct() плохая идея, т. к. в языках с автоматическим управлением памятью, нельзя быть уверенным на 100% в том, когда он выполнится. Деструктор не должен оставлять объект в нестабильном состоянии, поэтому в PHP обработчики уничтожения и освобождения объекта отделены друг от друга. Обработчик освобождения вызывается, когда движок полностью уверен, что объект больше нигде не применяется.

Для гарантированного удаления временного файла мы можем зарегистрировать свою функцию, которая выполнится в любом случае после завершения скрипта. Делается это с помощью register_shutdown_function() в конструкторе нашего класса:

Мы вынесли обработчик удаления временного файла в статическое замыкание, чтобы отвязать все ссылки от объекта и сохранить вызов __destruct до register_shutdown_function :

Класс TmpFile избегает ситуации, когда на него по умолчанию открыт дескриптор. Теперь можно использовать rename() вместо copy() и не бояться, когда при сохранении временных данных в другой файл на диске хранится две копии до завершения скрипта. Главное не держать открытый дескриптор, чтобы rename() наверняка отработала в Windows.

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

Пример с загрузкой файлов

Бизнес-логика предполагала валидацию файла на другом уровне и здесь важно было позаботиться об удалении файла в самом начале его пути, если проверка будет провалена. С помощью функции register_shutdown_function() мы можем гарантировать, что временный файл будет удалён, когда скрипт завершится. В сниппете ниже приведён пример того, как был использован класс TmpFile вместо tmpfile() :

Временный файл в CLI и try-finally

В вебе запросы пользователей живут относительно недолго, но в CLI скрипты могут выполняться бесконечно и гарантировать выполнение функции register_shutdown_function() мы не можем. Скрипт может быть убит на системном уровне или выполнятся так долго, что все временные файлы останутся лежать без их финальной обработки. В консоле лучшим способом удаления временных файлов является использование конструкции try-finally :

Здесь процесс удаления временного файла мы помещаем в блок finally , который выполнится сразу после выхода из коллбека. В долгоживущих консольных приложениях это самый оптимальный способ обработать удаление временного файла, но при фатальных ошибках код до finally не дойдёт, а использование register_shutdown_function() не желательно, поэтому не остаётся ничего другого, как писать валидный код в секции try .

Чем класс TmpFile отличается от tmpfile()

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

image

На основе идей из этой статьи, я написал менеджер для управления временными файлами, который доступен в репозитории denisyukphp/tmpfile-manager. Менеджер умеет много полезного: 1) настраивать путь к папке с временными файлами; 2) задавать префикс временным файлам; 3) закрывать отрытые ресурсы на временные файлы; 4) автоматически или вручную очищать временные файлы; 5) запускать свой сборщик мусора.

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

Читайте также: