Файловая система Drupal: что такое Stream Wrappers и как их использовать?
И снова, всем, привет! В предыдущем посте я рассказывал о том, как оседлать Dropbox API и OAuth. Однако самое интересное я припас для этого поста: как организовать работу файловой системы Drupal с удаленными файлами через собственный Stream Wrapper.
Предыдущий пост решал проблему взаимодействия с Dropbox API, в частности – авторизацию. Однако как организовать процесс загрузки файлов на Dropbox? Как хранить данные в Drupal об уже загруженных файлах на Dropbox? И какой интерфейс предоставлять пользователю для загрузки?
Первыми моими мыслями было:
- завести отдельную таблицу, наподобие "file_managed";
- создать свой элемент формы и поле, написать виджет;
- отлавливать событие сохранения файла через какой-нибудь
и дублировать загрузку на Dropbox.hook_field_presave()
Ребята, все это костыли и говнокод, запомните! Drupal в очередной раз покорил меня своей гибкостью и изяществом, когда в недрах ядра я нашел ответы на поставленные вопросы. Для реализации задач данного типа достаточно определить свой PHP Stream Wrapper, который скажет Drupal-системе куда что положить и как потом забрать. Решается все одним хуком и одним классом. Звучит круто, не правда ли?
Реализация Stream Wrappers в ядре
Думаю, что все в курсе того, что в Drupal из коробки есть Public file system и Private file system. И выбирать систему хранения вы можете отдельно для каждого созданного поля. Так вот, эти самые два варианта хранения и раздачи файлов описываются в
классами includes/stream_wrappers.inc
и DrupalPrivateStreamWrapper
соответственно.DrupalPublicStreamWrapper
Если открыть таблицу базы данных "file_managed", то можно увидеть, что пути к файлам хранятся не в абсолютном виде, а в формате
или public://path/to/file
. Собственно эти префиксы путей (назовем их так) ‘public’ и ‘private’ позволяют определять какой Stream Wrapper использовать для обработки файла.private://path/to/file
Как же это все работает? Каждый зарегистрированный Stream Wrapper в Drupal имеет набор обязательных методов, определенных интерфейсом
, который включает в себя набор стандартных методов для PHP stream wrapper, а также несколько дополнительных, обусловленных системой Drupal. Таким образом, мы наблюдаем отличный пример полиморфизма: мы говорим системе, например, «сохранить», а она уже сама решает, как ей обрабатывать файл.DrupalStreamWrapperInterface
Рассмотрим в качестве небольшого примера загрузку файла с использованием
:DrupalPublicStreamWrapper
- После нажатия на «Submit» выбранный файл загружается во временную папку вашего сайта в виде «php8B98.tmp»;
- Опустим всяческие валидации файла и проверки директорий на право записи. Лучше обратим внимание на функцию
, где вызывается уже чистокровная PHPdrupal_move_uploaded_file()
, после которой управление переходит в руки Stream Wrapper;move_uploaded_file($filename, $uri)
- Вызывается метод ‘stream_open’, в котором через
в указанной папке сервера создается файл и открывается для записи;fopen()
- В цикле идет срабатывание метода ‘stream_write’, который через
записывает данные в файл;fwrite()
- Вызывается ‘stream_flush’, где отрабатывает
для очистки буфера;fflush()
- Вызывается ‘stream_close’ для закрытия записываемого файла через
;fclose()
- Временный файл удаляется, данные записываются в базу данных.
Вот в таком ключе, если не вдаваться во все детали, работает Public file system при записи файла. Таким образом, нам остается лишь грамотно расписать методы своего Stream Wrapper, чтобы загружать и хранить файлы, где угодно: на стороннем сервере, на Dropbox или даже на серверах VK (скоро и до них мои руки доберутся ^^). Зачем я так извращаюсь спросите вы? На это есть свои причины – рассажу в посте о SEO как-нибудь.
Написание собственного класса Stream Wrapper
Не скажу, что написание собственного Stream Wrapper’a занятие безумно сложное, но разобраться в тонкостях все же придется. В зависимости от конкретной схемы управления файлами вам могут не потребоваться какие-то методы, другие же методы придется интерпретировать. Например, я не заморачивался с методами наподобие ‘stream_eof’, ‘stream_read’, ‘stream_lock’, так как побитового чтения у меня не подразумевалось в задаче. Просто заглушил их
в зависимости от логики метода.return TRUE / FALSE / NULL;
Ну-с, пробежимся немного по самым интересным методам класса, который я написал для работы с Dropbox. Помимо стандартного свойства
я еще использовал $uri
– далее объясню для чего оно.$buffer
- class DropboxStreamWrapper implements DrupalStreamWrapperInterface {
-
- protected $uri;
- private $buffer = '';
-
- function setUri($uri) {
- $this->uri = $uri;
- }
-
- function getUri() {
- return $this->uri;
- }
-
- // 100500 строчек кода..
- }
Остановимся более подробно на методах записи файла. В ‘stream_write’ мы очищаем свойство буфера данных.
- public function stream_open($uri, $mode, $options, &$opened_path) {
- $this->uri = $uri;
- // Clears buffer.
- $this->buffer = '';
- return TRUE;
- }
Собственно для метода ‘stream_write’ и вводилось свойство
. В качестве аргумента в метод приходят данные загружаемого файла. Дело в том, что информация файла считывается не вся сразу, а порциями, объем которых скорее всего определяется настройкам сервера. В случае $buffer
данные сразу же записывались через DrupalPublicStreamWrapper
, однако Dropbox API при загрузке файла требует сразу всей информации по файлу. Поэтому на этом этапе мы ничего никуда не пишем, а просто собираем в буфер данные.fwrite()
- public function stream_write($data) {
- $this->buffer .= $data;
- $data_length = strlen($data);
-
- return $data_length;
- }
А вот в методе ‘stream_flush’ будем отправлять файл на Dropbox через POST-запрос:
- public function stream_flush() {
- $path = $this->getLocalPath();
- $Dropbox = new DropboxFileService();
- $result = $Dropbox->fileUpload($path, $this->buffer);
-
- if ($result['code'] != 200) {
- $this->uri = '';
- watchdog('dropbox_file', 'Error in stream_flush(). Code: @code. Error: @error.',
- array('@code' => $result['code'], '@error' => $result['error']), WATCHDOG_ERROR);
- return FALSE;
- }
-
- cache_clear_all('dropbox_file:', 'cache', TRUE);
-
- return TRUE;
- }
Метод ‘stream_close’ мне оказался совсем ненужным:
- public function stream_close() {
- return FALSE;
- }
Для удаления файлов используется метод ‘unlink’.
- public function unlink($uri) {
- $this->uri = $uri;
- $path = $this->getLocalPath();
-
- $Dropbox = new DropboxFileService();
- $result = $Dropbox->pathDelete($path);
-
- if ($result['code'] != 200 || empty($result['is_deleted'])) {
- $this->uri = '';
- watchdog('dropbox_file', 'Error in unlink(). Code: @code. Error: @error.',
- array('@code' => $result['code'], '@error' => !empty($result['error']) ? $result['error'] : 'Empty.'), WATCHDOG_ERROR);
- return FALSE;
- }
-
- cache_clear_all('dropbox_file:', 'cache', TRUE);
-
- return TRUE;
- }
Интересным методом у меня получился ‘getExternalUrl’, который возвращает абсолютную ссылку на файл. Каждый запрос к Dropbox API сказывается на времени загрузки страницы, поэтому я решил использовать кеширование. Сразу хочу сказать, что это кеширование было сделано на скорую руку и возможно требует доработки – модуль интеграции с Dropbox я пока еще не успел обкатать на боевых сайтах.
- public function getExternalUrl() {
- $path = str_replace('\\', '/', $this->getTarget());
-
- $url = '';
- if ($cache = cache_get('dropbox_file:' . $path)) {
- $url = $cache->data;
- }
- else {
- $Dropbox = new DropboxFileService();
- $result = $Dropbox->fileShareLink($path);
-
- if ($result['code'] == 200) {
- // Добавляем параметр, чтобы получить прямую ссылку на файл Dropbox.
- $url = $result['url'] . '?dl=1';
- cache_set('dropbox_file:' . $path, $url, 'cache', REQUEST_TIME + 60 * 60 * 24);
- }
- else {
- watchdog('dropbox_file', 'Error in getExternalUrl(). Code: @code. Error: @error.',
- array('@code' => $result['code'], '@error' => $result['error']), WATCHDOG_ERROR);
- }
- }
-
- return $url;
- }
Небольшие трудности возникли и с ‘url_stat’, который я поначалу также пытался заглушить. Но, как оказалось, данный метод отрабатывает с функцией
, а также необходим для операции удаления. Через данный метод к тому же указывается размер файла, время обновления. Основную нагрузку несет строка:is_file()
– не оставляйте ее пустой, иначе всегда будете получать ответ, что «файл не существует».$stat[2] = $stat['mode'] = 33206;
- public function url_stat($uri, $flags) {
- $this->uri = $uri;
- $stat = $this->_stat($uri);
- return $stat;
- }
-
- protected function _stat($uri = NULL) {
- $path = $this->getLocalPath($uri);
-
- // Prevent file request for image styles.
- if (preg_match('/^styles\/(.*)$/', $path)) {
- return FALSE;
- }
-
- $Dropbox = new DropboxFileService();
- $metadata = $Dropbox->metadata($path);
-
- if ($metadata['code'] == 200) {
- $stat = array();
- $stat[0] = $stat['dev'] = 0;
- $stat[1] = $stat['ino'] = 0;
- // Without this $stat['mode'] value is_file() will be empty.
- $stat[2] = $stat['mode'] = 33206;
- $stat[3] = $stat['nlink'] = 0;
- $stat[4] = $stat['uid'] = 0;
- $stat[5] = $stat['gid'] = 0;
- $stat[6] = $stat['rdev'] = 0;
- $stat[7] = $stat['size'] = 0;
- $stat[8] = $stat['atime'] = 0;
- $stat[9] = $stat['mtime'] = 0;
- $stat[10] = $stat['ctime'] = 0;
- $stat[11] = $stat['blksize'] = 0;
- $stat[12] = $stat['blocks'] = 0;
-
- if (!$metadata['is_dir']) {
- if (!isset($metadata['bytes']) || !isset($metadata['modified'])) {
- return FALSE;
- }
- else {
- $stat[7] = $stat['size'] = $metadata['bytes'];
- $stat[8] = $stat['atime'] = strtotime($metadata['modified']);
- $stat[9] = $stat['mtime'] = strtotime($metadata['modified']);
- $stat[10] = $stat['ctime'] = strtotime($metadata['modified']);
- }
- }
- return $stat;
- }
- return FALSE;
- }
Подключение Stream Wrapper
Как я и говорил, подключается кастомный Stream Wrapper через хук:
- /**
- * Implements hook_stream_wrappers().
- */
- function MYMODULE_stream_wrappers() {
- return array(
- 'dropbox' => array(
- 'name' => t('Dropbox files'),
- 'class' => 'DropboxStreamWrapper',
- 'description' => t('Remote files served by Dropbox.'),
- 'type' => STREAM_WRAPPERS_WRITE_VISIBLE,
- ),
- );
- }
Если все сделано правильно, то теперь наряду с Public file system, Private file system вы сможете выбирать и Dropbox files при создании файловых полей для той же ноды или таксономии.
Вот таким заложенным в самых недрах Drupal’a методом можно играться с файловой системой, не допиливая и не ломая архитектуру самого движка. Данная возможность для меня была, наверное, самым большим открытием за последнее время в Drupal. Все же хорошо, когда ты только подумаешь «вот было бы круто, если бы…», а оно уже в движке и заложено.
Если есть вопросы – задавайте, буду помогать по возможности. Код пока не выкладываю, так как он еще сыроват и не оттестирован в боевых условиях.
Комментарии
Спасибо, в закладки.
это отдельный модуль, или это пихать в dropbox_file?
ну вот - опять облом. нет в жизни щастья.
Будет ли данный функционал выложен в виде отдельного модуля?
Добавить комментарий