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

И снова, всем, привет! В предыдущем посте я рассказывал о том, как оседлать Dropbox API и OAuth. Однако самое интересное я припас для этого поста: как организовать работу файловой системы Drupal с удаленными файлами через собственный Stream Wrapper.
Предыдущий пост решал проблему взаимодействия с Dropbox API, в частности – авторизацию. Однако как организовать процесс загрузки файлов на Dropbox? Как хранить данные в Drupal об уже загруженных файлах на Dropbox? И какой интерфейс предоставлять пользователю для загрузки?
Первыми моими мыслями было:
- завести отдельную таблицу, наподобие "file_managed";
- создать свой элемент формы и поле, написать виджет;
- отлавливать событие сохранения файла через какой-нибудь hook_field_presave()
Ребята, все это костыли и говнокод, запомните! Drupal в очередной раз покорил меня своей гибкостью и изяществом, когда в недрах ядра я нашел ответы на поставленные вопросы. Для реализации задач данного типа достаточно определить свой PHP Stream Wrapper, который скажет Drupal-системе куда что положить и как потом забрать. Решается все одним хуком и одним классом. Звучит круто, не правда ли?
Реализация Stream Wrappers в ядре
Думаю, что все в курсе того, что в Drupal из коробки есть Public file system и Private file system. И выбирать систему хранения вы можете отдельно для каждого созданного поля. Так вот, эти самые два варианта хранения и раздачи файлов описываются в includes/stream_wrappers.incDrupalPrivateStreamWrapperDrupalPublicStreamWrapper
Если открыть таблицу базы данных "file_managed", то можно увидеть, что пути к файлам хранятся не в абсолютном виде, а в формате public://path/to/fileprivate://path/to/file
Как же это все работает? Каждый зарегистрированный Stream Wrapper в Drupal имеет набор обязательных методов, определенных интерфейсом DrupalStreamWrapperInterface
Рассмотрим в качестве небольшого примера загрузку файла с использованием DrupalPublicStreamWrapper
- После нажатия на «Submit» выбранный файл загружается во временную папку вашего сайта в виде «php8B98.tmp»;
- Опустим всяческие валидации файла и проверки директорий на право записи. Лучше обратим внимание на функцию drupal_move_uploaded_file()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’ и вводилось свойство $bufferDrupalPublicStreamWrapperfwrite()
-   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?
ну вот - опять облом. нет в жизни щастья.
Будет ли данный функционал выложен в виде отдельного модуля?
Добавить комментарий