Skip to content

Commit

Permalink
Merge pull request #20 from City-of-Helsinki/UHF-9113-drupal-10-support
Browse files Browse the repository at this point in the history
UHF-9113: Drupal 10 support
  • Loading branch information
hyrsky authored Oct 30, 2023
2 parents 0ce00bc + b9286ad commit c54a5f6
Show file tree
Hide file tree
Showing 7 changed files with 431 additions and 24 deletions.
10 changes: 5 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

![CI](https://github.com/City-of-Helsinki/drupal-module-helfi-azure-fs/workflows/CI/badge.svg) [![codecov](https://codecov.io/gh/City-of-Helsinki/drupal-module-helfi-azure-fs/branch/main/graph/badge.svg?token=46YWS8J8NN)](https://codecov.io/gh/City-of-Helsinki/drupal-module-helfi-azure-fs)

Provides various fixes to deal with Azure's NFS mount and an integration to Azure's Blob storage service using [flysystem](https://www.drupal.org/project/flysystem) and [flysystem_azure](https://www.drupal.org/project/flysystem_azure) modules.
Provides various fixes to deal with Azure's NFS mount and an integration to Azure's Blob storage service using [flysystem](https://www.drupal.org/project/flysystem).

Azure's NFS file mount does not support certain file operations (such as chmod), causing any request that performs them to give a 5xx error, like when trying to generate an image style.

Expand All @@ -18,18 +18,17 @@ Enable the module.

### Using Azure Blob storage to host all files (optional)

- Enable `flysystem_azure` module: `drush en flysystem_azure`
- Populate required environment variables:
```
AZURE_BLOB_STORAGE_CONTAINER: The container name
AZURE_BLOB_STORAGE_KEY: The blob storage secret
AZURE_BLOB_STORAGE_NAME: The blob storage name
BLOBSTORAGE_ACCOUNT_KEY: The blob storage secret
```

or if you're using SAS token authentication:

```
AZURE_BLOB_STORAGE_SAS_TOKEN: The SAS token
BLOBSTORAGE_SAS_TOKEN: The SAS token
AZURE_BLOB_STORAGE_NAME: The blob storage name
```

Expand All @@ -53,9 +52,10 @@ $schemes = [
];
$config['helfi_azure_fs.settings']['use_blob_storage'] = TRUE;
$settings['flysystem'] = $schemes;
$settings['is_azure'] = TRUE;
```

The correct values can be found by running `printenv | grep AZURE_BLOB_STORAGE` inside a OpenShift Drupal pod.
The correct values can be found by running `printenv | grep BLOB` inside a OpenShift Drupal pod.

## Contact

Expand Down
10 changes: 2 additions & 8 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,17 +5,11 @@
"license": "GPL-2.0-or-later",
"minimum-stability": "dev",
"require": {
"drupal/flysystem_azure": "1.0.x-dev"
"drupal/flysystem": "^2.1@RC",
"microsoft/azure-storage-blob": "^1.1"
},
"require-dev": {
"dealerdirect/phpcodesniffer-composer-installer": "^0.7.0",
"drupal/coder": "^8.3"
},
"extra": {
"patches": {
"drupal/flysystem_azure": {
"D10 patch": "https://www.drupal.org/files/issues/2022-06-15/flysystem_azure.1.0.x-dev.rector.patch"
}
}
}
}
7 changes: 7 additions & 0 deletions modules/flysystem_azure/flysystem_azure.info.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
name: Flysystem Azure
description: 'Provides drupal/flysystem_azure so composer package can be removed.'
type: module
core_version_requirement: ^9 || ^10
package: Flysystem
dependencies:
- flysystem:flysystem
323 changes: 323 additions & 0 deletions src/AzureBlobStorageAdapter.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,323 @@
<?php

/**
* @file
* AzureBlobStorageAdapter.php from league/flysystem-azure-blob-storage:1.0.0.
* Modified to make this play nicely with Drupal 10 dependencies.
*
* @license MIT
* @license https://github.com/thephpleague/flysystem-azure-blob-storage/blob/1.0.0/LICENSE
*/

// phpcs:ignoreFile

namespace Drupal\helfi_azure_fs;

use GuzzleHttp\Psr7\Utils as GuzzleUtil;
use League\Flysystem\Adapter\AbstractAdapter;
use League\Flysystem\Adapter\Polyfill\NotSupportingVisibilityTrait;
use League\Flysystem\Config;
use League\Flysystem\Util;
use MicrosoftAzure\Storage\Blob\BlobRestProxy;
use MicrosoftAzure\Storage\Blob\Models\BlobPrefix;
use MicrosoftAzure\Storage\Blob\Models\BlobProperties;
use MicrosoftAzure\Storage\Blob\Models\CreateBlockBlobOptions;
use MicrosoftAzure\Storage\Blob\Models\ListBlobsOptions;
use MicrosoftAzure\Storage\Common\Exceptions\ServiceException;
use MicrosoftAzure\Storage\Common\Models\ContinuationToken;

use function array_merge;
use function compact;
use function stream_get_contents;
use function strpos;

class AzureBlobStorageAdapter extends AbstractAdapter
{
use NotSupportingVisibilityTrait;

/**
* @var string[]
*/
protected static $metaOptions = [
'CacheControl',
'ContentType',
'Metadata',
'ContentLanguage',
'ContentEncoding',
];

/**
* @var BlobRestProxy
*/
private $client;

private $container;

private $maxResultsForContentsListing = 5000;

public function __construct(BlobRestProxy $client, $container, $prefix = null)
{
$this->client = $client;
$this->container = $container;
$this->setPathPrefix($prefix);
}

public function write($path, $contents, Config $config)
{
return $this->upload($path, $contents, $config) + compact('contents');
}

public function writeStream($path, $resource, Config $config)
{
return $this->upload($path, $resource, $config);
}

protected function upload($path, $contents, Config $config)
{
$destination = $this->applyPathPrefix($path);

$options = $this->getOptionsFromConfig($config);

if (empty($options->getContentType())) {
$options->setContentType(Util::guessMimeType($path, $contents));
}

/**
* We manually create the stream to prevent it from closing the resource
* in its destructor.
*/
$stream = GuzzleUtil::streamFor($contents);
$response = $this->client->createBlockBlob(
$this->container,
$destination,
$contents,
$options
);

$stream->detach();

return [
'path' => $path,
'timestamp' => (int) $response->getLastModified()->getTimestamp(),
'dirname' => Util::dirname($path),
'type' => 'file',
];
}

public function update($path, $contents, Config $config)
{
return $this->upload($path, $contents, $config) + compact('contents');
}

public function updateStream($path, $resource, Config $config)
{
return $this->upload($path, $resource, $config);
}

public function rename($path, $newpath)
{
return $this->copy($path, $newpath) && $this->delete($path);
}

public function copy($path, $newpath)
{
$source = $this->applyPathPrefix($path);
$destination = $this->applyPathPrefix($newpath);
$this->client->copyBlob($this->container, $destination, $this->container, $source);

return true;
}

public function delete($path)
{
try {
$this->client->deleteBlob($this->container, $this->applyPathPrefix($path));
} catch (ServiceException $exception) {
if ($exception->getCode() !== 404) {
throw $exception;
}
}

return true;
}

public function deleteDir($dirname)
{
$prefix = $this->applyPathPrefix($dirname);
$options = new ListBlobsOptions();
$options->setPrefix($prefix . '/');
$listResults = $this->client->listBlobs($this->container, $options);
foreach ($listResults->getBlobs() as $blob) {
$this->client->deleteBlob($this->container, $blob->getName());
}

return true;
}

public function createDir($dirname, Config $config)
{
return ['path' => $dirname, 'type' => 'dir'];
}

public function has($path)
{
return $this->getMetadata($path);
}

public function read($path)
{
$response = $this->readStream($path);

if ( ! isset($response['stream']) || ! is_resource($response['stream'])) {
return $response;
}

$response['contents'] = stream_get_contents($response['stream']);
unset($response['stream']);

return $response;
}

public function readStream($path)
{
$location = $this->applyPathPrefix($path);

try {
$response = $this->client->getBlob(
$this->container,
$location
);

return $this->normalizeBlobProperties(
$path,
$response->getProperties()
) + ['stream' => $response->getContentStream()];
} catch (ServiceException $exception) {
if ($exception->getCode() !== 404) {
throw $exception;
}

return false;
}
}

public function listContents($directory = '', $recursive = false)
{
$result = [];
$location = $this->applyPathPrefix($directory);

if (strlen($location) > 0) {
$location = rtrim($location, '/') . '/';
}

$options = new ListBlobsOptions();
$options->setPrefix($location);
$options->setMaxResults($this->maxResultsForContentsListing);

if ( ! $recursive) {
$options->setDelimiter('/');
}

list_contents:
$response = $this->client->listBlobs($this->container, $options);
$continuationToken = $response->getContinuationToken();
foreach ($response->getBlobs() as $blob) {
$name = $blob->getName();

if ($location === '' || strpos($name, $location) === 0) {
$result[] = $this->normalizeBlobProperties($name, $blob->getProperties());
}
}

if ( ! $recursive) {
$result = array_merge($result, array_map([$this, 'normalizeBlobPrefix'], $response->getBlobPrefixes()));
}

if ($continuationToken instanceof ContinuationToken) {
$options->setContinuationToken($continuationToken);
goto list_contents;
}

return Util::emulateDirectories($result);
}

public function getMetadata($path)
{
$path = $this->applyPathPrefix($path);

try {
return $this->normalizeBlobProperties(
$path,
$this->client->getBlobProperties($this->container, $path)->getProperties()
);
} catch (ServiceException $exception) {
if ($exception->getCode() !== 404) {
throw $exception;
}

return false;
}
}

public function getSize($path)
{
return $this->getMetadata($path);
}

public function getMimetype($path)
{
return $this->getMetadata($path);
}

public function getTimestamp($path)
{
return $this->getMetadata($path);
}

protected function getOptionsFromConfig(Config $config)
{
$options = $config->get('blobOptions', new CreateBlockBlobOptions());
foreach (static::$metaOptions as $option) {
if ( ! $config->has($option)) {
continue;
}
call_user_func([$options, "set$option"], $config->get($option));
}
if ($mimetype = $config->get('mimetype')) {
$options->setContentType($mimetype);
}

return $options;
}

protected function normalizeBlobProperties($path, BlobProperties $properties)
{
$path = $this->removePathPrefix($path);

if (substr($path, -1) === '/') {
return ['type' => 'dir', 'path' => rtrim($path, '/')];
}

return [
'path' => $path,
'timestamp' => (int) $properties->getLastModified()->format('U'),
'dirname' => Util::dirname($path),
'mimetype' => $properties->getContentType(),
'size' => $properties->getContentLength(),
'type' => 'file',
];
}

/**
* @param int $numberOfResults
*/
public function setMaxResultsForContentsListing($numberOfResults)
{
$this->maxResultsForContentsListing = $numberOfResults;
}

protected function normalizeBlobPrefix(BlobPrefix $blobPrefix)
{
return ['type' => 'dir', 'path' => $this->removePathPrefix(rtrim($blobPrefix->getName(), '/'))];
}
}
Loading

0 comments on commit c54a5f6

Please sign in to comment.