az_blob_fs-8.x-1.x-dev/src/StreamWrapper/AzBlobFsStream.php

src/StreamWrapper/AzBlobFsStream.php
<?php

namespace Drupal\az_blob_fs\StreamWrapper;

use Drupal\az_blob_fs\Constants\AzBlobFsConstants;
use Drupal\Component\Utility\UrlHelper;
use Drupal\Core\StreamWrapper\StreamWrapperInterface;
use Drupal\Core\StreamWrapper\StreamWrapperManager;
use Drupal\Core\StringTranslation\StringTranslationTrait;
use GuzzleHttp\Psr7\Stream;
use GuzzleHttp\Psr7\StreamDecoratorTrait;
use MicrosoftAzure\Storage\Blob\Models\Block;
use MicrosoftAzure\Storage\Blob\Models\BlockList;
use MicrosoftAzure\Storage\Blob\Models\CommitBlobBlocksOptions;
use MicrosoftAzure\Storage\Blob\Models\ListBlobsOptions;
use MicrosoftAzure\Storage\Common\Exceptions\ServiceException;
use MicrosoftAzure\Storage\Common\Internal\Resources;
use Psr\Http\Message\StreamInterface;

/**
 * Azure Blob Filesystem Stream.
 */
class AzBlobFsStream extends AzBlobFsStreamWrapper implements StreamWrapperInterface, StreamInterface {

  use StreamDecoratorTrait;
  use StringTranslationTrait;

  // @codingStandardsIgnoreStart
  /**
   * Constructs a new AzBlobFsStream object.
   *
   * Dependency injection will not work here, since stream wrappers
   * are not loaded the normal way: PHP creates them automatically
   * when certain file functions are called.  This prevents us from
   * passing arguments to the constructor, which we'd need to do in
   * order to use standard dependency injection as is typically done
   * in Drupal.
   *
   * @throws \Drupal\az_blob_fs\AzBlobFsException
   */
  public function __construct() {
    parent::__construct();
  }
  // @codingStandardsIgnoreEnd

  /**
   * Returns the type of stream wrapper.
   *
   * @return int
   *   Type of stream wrapper.
   */
  public static function getType(): int {
    return StreamWrapperInterface::NORMAL;
  }

  /**
   * Returns the name of the stream wrapper for use in the UI.
   *
   * @return string
   *   The stream wrapper name.
   */
  public function getName(): string {
    return $this->t('Azure Blob Storage');
  }

  /**
   * Returns the description of the stream wrapper for use in the UI.
   *
   * @return string
   *   The stream wrapper description.
   */
  public function getDescription(): string {
    return $this->t('Files served from Azure Blob Storage.');
  }

  /**
   * Gets the path that the wrapper is responsible for.
   *
   * This function isn't part of DrupalStreamWrapperInterface, but the rest
   * of Drupal calls it as if it were, so we need to define it.
   *
   * @return string
   *   The empty string. Since this is a remote stream wrapper,
   *   it has no directory path.
   *
   * @see \Drupal\Core\File\LocalStream::getDirectoryPath()
   */
  public function getDirectoryPath(): string {
    return '';
  }

  /**
   * Sets the absolute stream resource URI.
   *
   * This allows you to set the URI. Generally is only called by the factory
   * method.
   *
   * @param string $uri
   *   A string containing the URI that should be used for this instance.
   */
  public function setUri($uri) {
    $this->uri = $uri;
  }

  /**
   * Returns the stream resource URI.
   *
   * @return string
   *   Returns the current URI of the instance.
   */
  public function getUri(): string {
    return $this->uri;
  }

  /**
   * Returns a web accessible URL for the resource.
   *
   * @return string
   *   Returns a string containing a web accessible URL for the resource.
   */
  public function getExternalUrl(): string {
    // Get the target destination without the scheme.
    $target = $this->streamWrapperManager->getTarget($this->uri);

    // Handle image styles.
    if (strpos($target, 'styles/') === 0) {
      // If the style derivative does not exist yet, we return to our custom
      // image style path handler.
      if (!file_exists(AzBlobFsConstants::SCHEME . '://' . $target)) {
        return $GLOBALS['base_url'] . '/' . AzBlobFsConstants::SCHEME . '/files/' . UrlHelper::encodePath($target);
      }
    }

    // If there is no target we won't return path to the bucket,
    // instead we'll return empty string.
    if (empty($target)) {
      return '';
    }

    // Return external url.
    return $this->client->getBlobUrl($this->container, $target);
  }

  /**
   * Returns canonical, absolute path of the resource.
   *
   * Implementation placeholder. PHP's realpath() does not support stream
   * wrappers. We provide this as a default so that individual wrappers may
   * implement their own solutions.
   *
   * * This wrapper does not support realpath().
   *
   * @return bool
   *   Always returns FALSE.
   */
  public function realpath(): bool {
    return FALSE;
  }

  /**
   * Extract container name.
   *
   * @param string $path
   *   The path to get the container name.
   *
   * @return string
   *   The container name.
   */
  protected function getContainerName(string $path): string {
    $url = parse_url($path);
    if ($url['host']) {
      return $url['host'];
    }
    return '';
  }

  /**
   * Extract file name.
   *
   * @param string $path
   *   The path to get the file name.
   *
   * @return string
   *   The file name.
   */
  protected function getFileName(string $path): string {
    $url = parse_url($path);
    if ($url['host']) {
      $fileName = $url['path'] ?? $url['host'];
      if (strpos($fileName, '/') === 0) {
        $fileName = substr($fileName, 1);
      }
      return $fileName;
    }
    return '';
  }

  // @codingStandardsIgnoreStart
  /**
   * Close the directory listing handles.
   *
   * @return bool
   *   Returns TRUE if the operation was successful, FALSE otherwise.
   */
  // @codingStandardsIgnoreStart
  public function dir_closedir(): bool {
    // @codingStandardsIgnoreEnd
    $this->iterator = NULL;
    return TRUE;
  }

  /**
   * {@inheritdoc}
   *
   * Support for opendir().
   *
   * @param string $path
   *   The URI to the directory to open.
   * @param int $options
   *   A flag used to enable safe_mode.
   *   This wrapper doesn't support safe_mode, so this parameter is ignored.
   *
   * @return bool
   *   TRUE on success. Otherwise, FALSE.
   *
   * @see http://php.net/manual/en/streamwrapper.dir-opendir.php
   */
  // @codingStandardsIgnoreStart
  public function dir_opendir($path, $options): bool {
    $uri = $path;

    // @codingStandardsIgnoreEnd
    if ($this->client->uriIsFile($uri)) {
      // Path is a file but return TRUE without creating the iterator.
      return TRUE;
    }

    $prefix = $uri;
    if ($uri == '/' || $uri == '') {
      $prefix = '';
    }
    else {
      // Add trailing slash.
      if (substr($uri, -1) != '/') {
        $prefix = $uri . '/';
      }
    }

    $options = new ListBlobsOptions();
    $options->setPrefix($prefix);
    $options->setDelimiter('/');
    $blobs_result = $this->client->listBlobs($this->container, $options);

    $blobs = new \ArrayObject($blobs_result->getBlobs());
    if (empty($blobs)) {
      $this->dir_closedir();
      return FALSE;
    }
    $this->iterator = $blobs->getIterator();

    return TRUE;
  }

  /**
   * This method is called in response to readdir()
   *
   * @return string
   *   Should return a string representing the next filename, or false if there
   *   is no next file.
   *
   * @link http://www.php.net/manual/en/function.readdir.php
   */
  // @codingStandardsIgnoreStart
  public function dir_readdir() {
    // @codingStandardsIgnoreEnd
    // Skip empty result keys.
    if (!$this->iterator->valid()) {
      return FALSE;
    }

    $file_name = $this->iterator->valid() ? $this->iterator->current()
      ->getName() : FALSE;
    $this->iterator->next();

    // The blobs hold their names as the full path of the namespace
    // we want the actual file name.
    if ($file_name) {
      $file_name = explode('/', $file_name);
      return end($file_name);
    }

    return FALSE;
  }

  /**
   * This method is called in response to rewinddir().
   *
   * @return bool
   *   Returns TRUE if the operation was successful, FALSE otherwise.
   */
  // @codingStandardsIgnoreStart
  public function dir_rewinddir(): bool {
    // @codingStandardsIgnoreEnd
    // If our iterator is empty, there is nothing to rewind.
    // This also means there is likely no directory.
    if ($this->iterator === NULL) {
      return FALSE;
    }

    // Try to rewind our iterator.
    try {
      $this->iterator->rewind();
      return TRUE;
    }
    catch (ServiceException $e) {
      $this->logger->error('Azure Blob Stream Exception (dir_rewinddir)');
      return FALSE;
    }
  }

  /**
   * Azure Blob Storage doesn't support physical directories.
   *
   * So always return TRUE.
   *
   * @param string $path
   *   The path.
   * @param int $mode
   *   The mode.
   * @param int $options
   *   The options.
   *
   * @return bool
   *   Returns TRUE if the operation was successful, FALSE otherwise.
   */
  public function mkdir($path, $mode, $options): bool {
    return TRUE;
  }

  /**
   * Called in response to rename() to rename a file or directory.
   *
   * Currently, only supports renaming objects.
   *
   * In Azure Blob Storage, we have to copy the existing blob to a new one with
   * the new name, then delete the original.
   *
   * @param string $path_from
   *   The path to the file to rename.
   * @param string $path_to
   *   The new path to the file.
   *
   * @return bool
   *   True if file was successfully renamed
   *
   * @link http://www.php.net/manual/en/function.rename.php
   */
  public function rename($path_from, $path_to): bool {
    return $this->renameBlob($path_from, $path_to);
  }

  /**
   * Called in response to rmdir(). To remove a directory.
   *
   * {@inheritdoc}
   */
  public function rmdir($path, $options): bool {
    // Delete what is found at the provided URI.
    return $this->deleteRemotePath($path);
  }

  /**
   * Retrieve the underlying stream resource.
   *
   * This method is called in response to stream_select().
   *
   * @param int $cast_as
   *   Can be STREAM_CAST_FOR_SELECT when stream_select() is calling
   *   stream_cast() or STREAM_CAST_AS_STREAM when stream_cast() is called for
   *   other uses.
   *
   * @return resource|false
   *   The underlying stream resource or FALSE if stream_select() is not
   *   supported.
   *
   * @see stream_select()
   * @see http://php.net/manual/streamwrapper.stream-cast.php
   */
  // @codingStandardsIgnoreStart
  public function stream_cast($cast_as): bool {
    // @codingStandardsIgnoreEnd
    return FALSE;
  }

  /**
   * {@inheritdoc}
   */
  // @codingStandardsIgnoreStart
  public function stream_close() {
    // @codingStandardsIgnoreEnd
    $this->stream = $this->cache = NULL;
  }

  /**
   * {@inheritdoc}
   */
  // @codingStandardsIgnoreStart
  public function stream_eof(): bool {
    // @codingStandardsIgnoreEnd
    return feof($this->temporaryFileHandle);
  }

  /**
   * {@inheritdoc}
   */
  // @codingStandardsIgnoreStart
  public function stream_flush(): bool {
    // @codingStandardsIgnoreEnd
    if ($this->mode == 'r') {
      return FALSE;
    }

    if ($this->isSeekable()) {
      $this->seek(0);
    }

    try {
      $blob_name = $this->streamWrapperManager->getTarget($this->uri);
      $blob_parts = pathinfo($blob_name);
      $blob_extension = $blob_parts['extension'] ?? NULL;
      $blob_extension = strtolower($blob_extension);

      $block_counter = 1;
      $blocks = [];
      while (!$this->stream->eof()) {
        // Make sure not to exceed max length for block id.
        $block_id = base64_encode(md5(basename($this->uri) . $block_counter));

        // Create block object.
        $block = new Block($block_id);
        $block->setType('Uncommitted');

        // Add to blocks array.
        $blocks[] = $block;

        // Get block chunk size.
        $block_size = $this->client->getBlockSize();
        if (empty($block_size)) {
          $block_size = Resources::MB_IN_BYTES_100;
        }

        // Get chunked data to store in block.
        $block_data = $this->stream->read($block_size);
        $this->client->createBlobBlock($this->container, $blob_name, $block_id, $block_data);

        // Increment block counter.
        $block_counter++;
      }

      // Set block options.
      $options = new CommitBlobBlocksOptions();

      // Set the content type.
      $content_types = $this->getContentTypes();
      if (!empty($content_types[$blob_extension])) {
        $options->setContentType($content_types[$blob_extension]);
      }

      // Commit blocks.
      $blocksList = BlockList::create($blocks);
      $this->client->commitBlobBlocks($this->container, $blob_name, $blocksList, $options);

      $this->stream = '';
      $this->iterator = FALSE;

      return TRUE;
    }
    catch (ServiceException $e) {
      $this->logger->error('Azure Blob Stream Exception (stream_flush)');
      return FALSE;
    }
  }

  /**
   * {@inheritdoc}
   */
  // @codingStandardsIgnoreStart
  public function stream_lock($operation): bool {
    // @codingStandardsIgnoreEnd
    return FALSE;
  }

  /**
   * Sets metadata on the stream.
   *
   * @param string $path
   *   A string containing the URI to the file to set metadata on.
   * @param int $option
   *   One of:
   *   - STREAM_META_TOUCH: The method was called in response to touch().
   *   - STREAM_META_OWNER_NAME: The method was called in response to chown()
   *     with string parameter.
   *   - STREAM_META_OWNER: The method was called in response to chown().
   *   - STREAM_META_GROUP_NAME: The method was called in response to chgrp().
   *   - STREAM_META_GROUP: The method was called in response to chgrp().
   *   - STREAM_META_ACCESS: The method was called in response to chmod().
   * @param mixed $value
   *   If option is:
   *   - STREAM_META_TOUCH: Array consisting of two arguments of the touch()
   *     function.
   *   - STREAM_META_OWNER_NAME or STREAM_META_GROUP_NAME: The name of the owner
   *     user/group as string.
   *   - STREAM_META_OWNER or STREAM_META_GROUP: The value of the owner
   *     user/group as integer.
   *   - STREAM_META_ACCESS: The argument of the chmod() as integer.
   *
   *    * This wrapper does not support touch(), chmod(), chown(), or chgrp().
   *
   * @return bool
   *   Manual recommends return FALSE for not implemented options, but Drupal
   *   require TRUE in some cases like chmod for avoid watchdog erros.
   *
   * @see \Drupal\Core\File\FileSystem::chmod()
   *
   * Returns FALSE if the option is not included in bypassed_options array
   * otherwise, TRUE.
   *
   * @see http://php.net/manual/en/streamwrapper.stream-metadata.php
   * @see http://php.net/manual/streamwrapper.stream-metadata.php
   */
  // @codingStandardsIgnoreStart
  public function stream_metadata($path, $option, $value): bool {
    // @codingStandardsIgnoreEnd
    $bypassed_options = [STREAM_META_ACCESS];
    return in_array($option, $bypassed_options);
  }

  /**
   * Opens a stream, as for fopen(), file_get_contents(), file_put_contents().
   *
   * @param string $path
   *   A string containing the URI to the file to open.
   * @param string $mode
   *   The file mode ("r", "wb" etc.).
   * @param int $options
   *   A bit mask of STREAM_USE_PATH and STREAM_REPORT_ERRORS.
   * @param string &$opened_path
   *   A string containing the path actually opened.
   *
   * @return bool
   *   Returns TRUE if file was opened successfully. (Always returns TRUE).
   *
   * @see http://php.net/manual/en/streamwrapper.stream-open.php
   */
  // @codingStandardsIgnoreStart
  public function stream_open($path, $mode, $options, &$opened_path): bool {
    $uri = $path;

    // @codingStandardsIgnoreEnd
    if (!$this->isClientReady()) {
      return FALSE;
    }

    $this->setUri($uri);
    $this->stream = new Stream(fopen('php://temp', $mode));

    if (in_array($mode, [
      'r',
      'rb',
      'rt',
    ])) {
      // Get the target destination without the scheme.
      $target = $this->streamWrapperManager->getTarget($this->uri);
      try {
        $blob = $this->client->getBlob($this->container, $target);
        $this->temporaryFileHandle = $blob->getContentStream();
      }
      catch (ServiceException $e) {
        $this->logger->error('Azure Blob Stream Exception (stream_open)');
        return FALSE;
      }
    }

    return TRUE;
  }

  /**
   * {@inheritdoc}
   */
  // @codingStandardsIgnoreStart
  public function stream_read($count) {
    // @codingStandardsIgnoreEnd
    return fread($this->temporaryFileHandle, $count);
  }

  /**
   * Seeks to specific location in a stream.
   *
   * This method is called in response to fseek().
   *
   * The read/write position of the stream should be updated according to the
   * offset and whence.
   *
   * @param int $offset
   *   The byte offset to seek to.
   * @param int $whence
   *   Possible values:
   *   - SEEK_SET: Set position equal to offset bytes.
   *   - SEEK_CUR: Set position to current location plus offset.
   *   - SEEK_END: Set position to end-of-file plus offset.
   *   Defaults to SEEK_SET.
   *
   * @return bool
   *   TRUE if the position was updated, FALSE otherwise.
   *
   * @see http://php.net/manual/streamwrapper.stream-seek.php
   */
  // @codingStandardsIgnoreStart
  public function stream_seek($offset, $whence = SEEK_SET): bool {
    // @codingStandardsIgnoreEnd
    return !fseek($this->temporaryFileHandle, $offset, $whence);
  }

  /**
   * Change stream options.
   *
   * This method is called to set options on the stream.
   *
   * @param int $option
   *   One of:
   *   - STREAM_OPTION_BLOCKING: The method was called in response to
   *     stream_set_blocking().
   *   - STREAM_OPTION_READ_TIMEOUT: The method was called in response to
   *     stream_set_timeout().
   *   - STREAM_OPTION_WRITE_BUFFER: The method was called in response to
   *     stream_set_write_buffer().
   * @param int $arg1
   *   If option is:
   *   - STREAM_OPTION_BLOCKING: The requested blocking mode:
   *     - 1 means blocking.
   *     - 0 means not blocking.
   *   - STREAM_OPTION_READ_TIMEOUT: The timeout in seconds.
   *   - STREAM_OPTION_WRITE_BUFFER: The buffer mode, STREAM_BUFFER_NONE or
   *     STREAM_BUFFER_FULL.
   * @param int $arg2
   *   If option is:
   *   - STREAM_OPTION_BLOCKING: This option is not set.
   *   - STREAM_OPTION_READ_TIMEOUT: The timeout in microseconds.
   *   - STREAM_OPTION_WRITE_BUFFER: The requested buffer size.
   *
   * @return bool
   *   TRUE on success, FALSE otherwise. If $option is not implemented, FALSE
   *   should be returned.
   */
  // @codingStandardsIgnoreStart
  public function stream_set_option($option, $arg1, $arg2): bool {
    // @codingStandardsIgnoreEnd
    return FALSE;
  }

  /**
   * {@inheritdoc}
   */
  // @codingStandardsIgnoreStart
  public function stream_stat() {
    // @codingStandardsIgnoreEnd
    return $this->url_stat($this->uri, 0);
  }

  /**
   * {@inheritdoc}
   */
  // @codingStandardsIgnoreStart
  public function stream_tell(): int {
    // @codingStandardsIgnoreEnd
    return $this->tell();
  }

  /**
   * {@inheritdoc}
   */
  // @codingStandardsIgnoreStart
  public function stream_truncate($new_size): bool {
    // @codingStandardsIgnoreEnd
    return FALSE;
  }

  /**
   * {@inheritdoc}
   */
  // @codingStandardsIgnoreStart
  public function stream_write($data): int {
    // @codingStandardsIgnoreEnd
    return $this->write($data);
  }

  /**
   * Support for unlink().
   *
   * @param string $path
   *   A string containing the uri to the resource to delete.
   *
   * @return bool
   *   Returns the deleted remote path.
   *
   * @see http://php.net/manual/en/streamwrapper.unlink.php
   */
  // @codingStandardsIgnoreStart
  public function unlink($path): bool {
    // @codingStandardsIgnoreEnd
    // Delete what is found at the provided URI.
    return $this->deleteRemotePath($path);
  }

  /**
   * {@inheritdoc}
   */
  // @codingStandardsIgnoreStart
  public function url_stat($path, $flags) {
    $uri = $path;

    // @codingStandardsIgnoreEnd
    if (!$this->isClientReady()) {
      return FALSE;
    }

    // @see http://be2.php.net/manual/en/function.stat.php
    $stat = array_fill_keys([
      'dev',
      'ino',
      'mode',
      'nlink',
      'uid',
      'gid',
      'rdev',
      'size',
      'atime',
      'mtime',
      'ctime',
      'blksize',
      'blocks',
    ], 0);

    // If $blob_prefixes is not empty and $blobs is, it means it's a directory.
    // If $blobs is not empty and $blob_prefixes is, it's a file.
    // If both are empty, the blob does not exist.
    // Get the target destination without the scheme.
    $target = $this->streamWrapperManager->getTarget($uri);
    try {
      $blob = $this->client->getBlobProperties($this->container, $target);
    }
    catch (ServiceException $e) {
      // If it is a 404 code, continue on, otherwise, throw the exception..
      if ($e->getCode() !== 404) {
        $this->logger->error('Azure Blob Stream Exception (url_stat)');
        return FALSE;
      }
    }

    $blob_prefixes = [];
    $pathArray = explode("/", $target);
    $count = 0;
    foreach ($pathArray as $slice) {
      $pos = strpos($slice, '.');
      $count++;
      if ($pos !== FALSE) {
        $blob_prefixes[] = $slice;
      }
    }

    // Blob exists.
    // Blob is a file.
    if (!empty($blob) && !empty($blob_prefixes)) {
      $blob_properties = $blob->getProperties();

      // Use the S_IFREG posix flag for files.
      // All files are considered writable, so OR in 0777.
      $stat['mode'] = 0100000 | 0777;
      $stat['size'] = $blob_properties->getContentLength();
      $stat['mtime'] = date_timestamp_get($blob_properties->getLastModified());
      $stat['blksize'] = -1;
      $stat['blocks'] = -1;
    }

    if (empty($blob)) {
      // Blob is directory.
      if (empty($blob_prefixes)) {
        // Use the S_IFDIR posix flag for directories
        // All directories are considered writable, so OR in 0777.
        $stat['mode'] = 0040000 | 0777;
      }
      else {
        $stat = FALSE;
      }
    }

    return $stat;
  }

  /**
   * Gets the name of the directory from a given path.
   *
   * This method is usually accessed through drupal_dirname(), which wraps
   * around the normal PHP dirname() function, which does not support stream
   * wrappers.
   *
   * @param string $uri
   *   An optional URI.
   *
   * @return string
   *   A string containing the directory name, or FALSE if not applicable.
   *
   * @see \Drupal::service('file_system')->dirname()
   */
  public function dirname($uri = NULL): string {
    if (!isset($uri)) {
      $uri = $this->uri;
    }

    // Get scheme.
    $scheme = StreamWrapperManager::getScheme($uri);

    // Get directory name.
    $dirname = $this->fileSystem->dirname(($this->streamWrapperManager->getTarget($uri)));

    // When the dirname() call above is given '$scheme://', it returns '.'.
    // But '$scheme://.' is an invalid uri, so we return "$scheme://" instead.
    if ($dirname == '.') {
      $dirname = '';
    }

    return "$scheme://$dirname";
  }

}

Главная | Обратная связь

drupal hosting | друпал хостинг | it patrol .inc