Файловая система Drupal: что такое Stream Wrappers и как их использовать?

PHP Stream Wrappers в Drupal

И снова, всем, привет! В предыдущем посте я рассказывал о том, как оседлать Dropbox API и OAuth. Однако самое интересное я припас для этого поста: как организовать работу файловой системы Drupal с удаленными файлами через собственный Stream Wrapper.

Предыдущий пост решал проблему взаимодействия с Dropbox API, в частности – авторизацию. Однако как организовать процесс загрузки файлов на Dropbox? Как хранить данные в Drupal об уже загруженных файлах на Dropbox? И какой интерфейс предоставлять пользователю для загрузки?

Первыми моими мыслями было:

  • завести отдельную таблицу, наподобие "file_managed";
  • создать свой элемент формы и поле, написать виджет;
  • отлавливать событие сохранения файла через какой-нибудь hook_field_presave() и дублировать загрузку на Dropbox.

Ребята, все это костыли и говнокод, запомните! 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 или private://path/to/file . Собственно эти префиксы путей (назовем их так) ‘public’ и ‘private’ позволяют определять какой Stream Wrapper использовать для обработки файла.

Как же это все работает? Каждый зарегистрированный Stream Wrapper в Drupal имеет набор обязательных методов, определенных интерфейсом DrupalStreamWrapperInterface, который включает в себя набор стандартных методов для PHP stream wrapper, а также несколько дополнительных, обусловленных системой Drupal. Таким образом, мы наблюдаем отличный пример полиморфизма: мы говорим системе, например, «сохранить», а она уже сама решает, как ей обрабатывать файл.

Рассмотрим в качестве небольшого примера загрузку файла с использованием DrupalPublicStreamWrapper:

  1. После нажатия на «Submit» выбранный файл загружается во временную папку вашего сайта в виде «php8B98.tmp»;
  2. Опустим всяческие валидации файла и проверки директорий на право записи. Лучше обратим внимание на функцию drupal_move_uploaded_file(), где вызывается уже чистокровная PHP move_uploaded_file($filename, $uri), после которой управление переходит в руки Stream Wrapper;
  3. Вызывается метод ‘stream_open’, в котором через fopen() в указанной папке сервера создается файл и открывается для записи;
  4. В цикле идет срабатывание метода ‘stream_write’, который через fwrite() записывает данные в файл;
  5. Вызывается ‘stream_flush’, где отрабатывает fflush() для очистки буфера;
  6. Вызывается ‘stream_close’ для закрытия записываемого файла через fclose();
  7. Временный файл удаляется, данные записываются в базу данных.

Вот в таком ключе, если не вдаваться во все детали, работает 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 – далее объясню для чего оно.

  1. class DropboxStreamWrapper implements DrupalStreamWrapperInterface {
  2.  
  3. protected $uri;
  4. private $buffer = '';
  5.  
  6. function setUri($uri) {
  7. $this->uri = $uri;
  8. }
  9.  
  10. function getUri() {
  11. return $this->uri;
  12. }
  13.  
  14. // 100500 строчек кода..
  15. }

Остановимся более подробно на методах записи файла. В ‘stream_write’ мы очищаем свойство буфера данных.

  1. public function stream_open($uri, $mode, $options, &$opened_path) {
  2. $this->uri = $uri;
  3. // Clears buffer.
  4. $this->buffer = '';
  5. return TRUE;
  6. }

Собственно для метода ‘stream_write’ и вводилось свойство $buffer. В качестве аргумента в метод приходят данные загружаемого файла. Дело в том, что информация файла считывается не вся сразу, а порциями, объем которых скорее всего определяется настройкам сервера. В случае DrupalPublicStreamWrapper данные сразу же записывались через fwrite(), однако Dropbox API при загрузке файла требует сразу всей информации по файлу. Поэтому на этом этапе мы ничего никуда не пишем, а просто собираем в буфер данные.

  1. public function stream_write($data) {
  2. $this->buffer .= $data;
  3. $data_length = strlen($data);
  4.  
  5. return $data_length;
  6. }

А вот в методе ‘stream_flush’ будем отправлять файл на Dropbox через POST-запрос:

  1. public function stream_flush() {
  2. $path = $this->getLocalPath();
  3. $Dropbox = new DropboxFileService();
  4. $result = $Dropbox->fileUpload($path, $this->buffer);
  5.  
  6. if ($result['code'] != 200) {
  7. $this->uri = '';
  8. watchdog('dropbox_file', 'Error in stream_flush(). Code: @code. Error: @error.',
  9. array('@code' => $result['code'], '@error' => $result['error']), WATCHDOG_ERROR);
  10. return FALSE;
  11. }
  12.  
  13. cache_clear_all('dropbox_file:', 'cache', TRUE);
  14.  
  15. return TRUE;
  16. }

Метод ‘stream_close’ мне оказался совсем ненужным:

  1. public function stream_close() {
  2. return FALSE;
  3. }

Для удаления файлов используется метод ‘unlink’.

  1. public function unlink($uri) {
  2. $this->uri = $uri;
  3. $path = $this->getLocalPath();
  4.  
  5. $Dropbox = new DropboxFileService();
  6. $result = $Dropbox->pathDelete($path);
  7.  
  8. if ($result['code'] != 200 || empty($result['is_deleted'])) {
  9. $this->uri = '';
  10. watchdog('dropbox_file', 'Error in unlink(). Code: @code. Error: @error.',
  11. array('@code' => $result['code'], '@error' => !empty($result['error']) ? $result['error'] : 'Empty.'), WATCHDOG_ERROR);
  12. return FALSE;
  13. }
  14.  
  15. cache_clear_all('dropbox_file:', 'cache', TRUE);
  16.  
  17. return TRUE;
  18. }

Интересным методом у меня получился ‘getExternalUrl’, который возвращает абсолютную ссылку на файл. Каждый запрос к Dropbox API сказывается на времени загрузки страницы, поэтому я решил использовать кеширование. Сразу хочу сказать, что это кеширование было сделано на скорую руку и возможно требует доработки – модуль интеграции с Dropbox я пока еще не успел обкатать на боевых сайтах.

  1. public function getExternalUrl() {
  2. $path = str_replace('\\', '/', $this->getTarget());
  3.  
  4. $url = '';
  5. if ($cache = cache_get('dropbox_file:' . $path)) {
  6. $url = $cache->data;
  7. }
  8. else {
  9. $Dropbox = new DropboxFileService();
  10. $result = $Dropbox->fileShareLink($path);
  11.  
  12. if ($result['code'] == 200) {
  13. // Добавляем параметр, чтобы получить прямую ссылку на файл Dropbox.
  14. $url = $result['url'] . '?dl=1';
  15. cache_set('dropbox_file:' . $path, $url, 'cache', REQUEST_TIME + 60 * 60 * 24);
  16. }
  17. else {
  18. watchdog('dropbox_file', 'Error in getExternalUrl(). Code: @code. Error: @error.',
  19. array('@code' => $result['code'], '@error' => $result['error']), WATCHDOG_ERROR);
  20. }
  21. }
  22.  
  23. return $url;
  24. }

Небольшие трудности возникли и с ‘url_stat’, который я поначалу также пытался заглушить. Но, как оказалось, данный метод отрабатывает с функцией is_file(), а также необходим для операции удаления. Через данный метод к тому же указывается размер файла, время обновления. Основную нагрузку несет строка:$stat[2] = $stat['mode'] = 33206; – не оставляйте ее пустой, иначе всегда будете получать ответ, что «файл не существует».

  1. public function url_stat($uri, $flags) {
  2. $this->uri = $uri;
  3. $stat = $this->_stat($uri);
  4. return $stat;
  5. }
  6.  
  7. protected function _stat($uri = NULL) {
  8. $path = $this->getLocalPath($uri);
  9.  
  10. // Prevent file request for image styles.
  11. if (preg_match('/^styles\/(.*)$/', $path)) {
  12. return FALSE;
  13. }
  14.  
  15. $Dropbox = new DropboxFileService();
  16. $metadata = $Dropbox->metadata($path);
  17.  
  18. if ($metadata['code'] == 200) {
  19. $stat = array();
  20. $stat[0] = $stat['dev'] = 0;
  21. $stat[1] = $stat['ino'] = 0;
  22. // Without this $stat['mode'] value is_file() will be empty.
  23. $stat[2] = $stat['mode'] = 33206;
  24. $stat[3] = $stat['nlink'] = 0;
  25. $stat[4] = $stat['uid'] = 0;
  26. $stat[5] = $stat['gid'] = 0;
  27. $stat[6] = $stat['rdev'] = 0;
  28. $stat[7] = $stat['size'] = 0;
  29. $stat[8] = $stat['atime'] = 0;
  30. $stat[9] = $stat['mtime'] = 0;
  31. $stat[10] = $stat['ctime'] = 0;
  32. $stat[11] = $stat['blksize'] = 0;
  33. $stat[12] = $stat['blocks'] = 0;
  34.  
  35. if (!$metadata['is_dir']) {
  36. if (!isset($metadata['bytes']) || !isset($metadata['modified'])) {
  37. return FALSE;
  38. }
  39. else {
  40. $stat[7] = $stat['size'] = $metadata['bytes'];
  41. $stat[8] = $stat['atime'] = strtotime($metadata['modified']);
  42. $stat[9] = $stat['mtime'] = strtotime($metadata['modified']);
  43. $stat[10] = $stat['ctime'] = strtotime($metadata['modified']);
  44. }
  45. }
  46. return $stat;
  47. }
  48. return FALSE;
  49. }

Подключение Stream Wrapper

Как я и говорил, подключается кастомный Stream Wrapper через хук:

  1. /**
  2.  * Implements hook_stream_wrappers().
  3.  */
  4. function MYMODULE_stream_wrappers() {
  5. return array(
  6. 'dropbox' => array(
  7. 'name' => t('Dropbox files'),
  8. 'class' => 'DropboxStreamWrapper',
  9. 'description' => t('Remote files served by Dropbox.'),
  10. 'type' => STREAM_WRAPPERS_WRITE_VISIBLE,
  11. ),
  12. );
  13. }

Если все сделано правильно, то теперь наряду с Public file system, Private file system вы сможете выбирать и Dropbox files при создании файловых полей для той же ноды или таксономии.

Выбрать файловое хранилище

Вот таким заложенным в самых недрах Drupal’a методом можно играться с файловой системой, не допиливая и не ломая архитектуру самого движка. Данная возможность для меня была, наверное, самым большим открытием за последнее время в Drupal. Все же хорошо, когда ты только подумаешь «вот было бы круто, если бы…», а оно уже в движке и заложено.

Если есть вопросы – задавайте, буду помогать по возможности. Код пока не выкладываю, так как он еще сыроват и не оттестирован в боевых условиях.

Комментарии

Аватар пользователя Rodman
Rodman

Спасибо, в закладки.

Аватар пользователя Прохожий
Прохожий

это отдельный модуль, или это пихать в dropbox_file?

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

ну вот - опять облом. нет в жизни щастья.

Аватар пользователя test
test

Будет ли данный функционал выложен в виде отдельного модуля?

Аватар пользователя angarsky
angarsky
Пока еще модуль обкатывается, есть огрехи. Попозже может и выложу. Это же еще оформлять все надо :)

Добавить комментарий

 8888888888            888    888   .d8888b.  
888 888 888 d88P Y88b
888 888 888 888
8888888 888 888 8888888888 888d888b.
888 `Y8bd8P' 888 888 888P "Y88b
888 X88K 888 888 888 888
888 .d8""8b. 888 888 Y88b d88P
888 888 888 888 888 "Y8888P"


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