summaryrefslogtreecommitdiffstats
path: root/lib/private/Files
diff options
context:
space:
mode:
authorRobin Appelman <robin@icewind.nl>2018-02-15 10:50:23 +0100
committerRoeland Jago Douma <roeland@famdouma.nl>2018-02-19 19:41:16 +0100
commit3192efc415ac6d71683994afdd8f510e3480b42f (patch)
tree000b031c8ac3fc2424e8160673963232e4340706 /lib/private/Files
parent2abfe3347eded42381a19b17703fe99d58488d40 (diff)
downloadnextcloud-server-3192efc415ac6d71683994afdd8f510e3480b42f.tar.gz
nextcloud-server-3192efc415ac6d71683994afdd8f510e3480b42f.zip
adjust swift object storage to new openstack sdk
Signed-off-by: Robin Appelman <robin@icewind.nl>
Diffstat (limited to 'lib/private/Files')
-rw-r--r--lib/private/Files/ObjectStore/Swift.php242
-rw-r--r--lib/private/Files/ObjectStore/SwiftConnectionFactory.php154
2 files changed, 184 insertions, 212 deletions
diff --git a/lib/private/Files/ObjectStore/Swift.php b/lib/private/Files/ObjectStore/Swift.php
index a3cba488f5f..1f93954c774 100644
--- a/lib/private/Files/ObjectStore/Swift.php
+++ b/lib/private/Files/ObjectStore/Swift.php
@@ -25,223 +25,40 @@
namespace OC\Files\ObjectStore;
-use Guzzle\Http\Exception\ClientErrorResponseException;
-use Guzzle\Http\Exception\CurlException;
+use function GuzzleHttp\Psr7\stream_for;
use Icewind\Streams\RetryWrapper;
use OCP\Files\ObjectStore\IObjectStore;
use OCP\Files\StorageAuthException;
-use OCP\Files\StorageNotAvailableException;
-use OpenCloud\Common\Service\Catalog;
-use OpenCloud\Common\Service\CatalogItem;
-use OpenCloud\Identity\Resource\Token;
-use OpenCloud\ObjectStore\Service;
-use OpenCloud\OpenStack;
-use OpenCloud\Rackspace;
class Swift implements IObjectStore {
-
- /**
- * @var \OpenCloud\OpenStack
- */
- private $client;
-
/**
* @var array
*/
private $params;
/**
- * @var \OpenCloud\ObjectStore\Service
- */
- private $objectStoreService;
-
- /**
- * @var \OpenCloud\ObjectStore\Resource\Container
+ * @var \OpenStack\ObjectStore\v1\Models\Container|null
*/
- private $container;
+ private $container = null;
- private $memcache;
+ /** @var SwiftConnectionFactory */
+ private $swiftFactory;
public function __construct($params) {
- if (isset($params['bucket'])) {
- $params['container'] = $params['bucket'];
- }
- if (!isset($params['container'])) {
- $params['container'] = 'owncloud';
- }
- if (!isset($params['autocreate'])) {
- // should only be true for tests
- $params['autocreate'] = false;
- }
-
- if (isset($params['apiKey'])) {
- $this->client = new Rackspace($params['url'], $params);
- $cacheKey = $params['username'] . '@' . $params['url'] . '/' . $params['bucket'];
- } else {
- $this->client = new OpenStack($params['url'], $params);
- $cacheKey = $params['username'] . '@' . $params['url'] . '/' . $params['bucket'];
- }
-
- $cacheFactory = \OC::$server->getMemCacheFactory();
- $this->memcache = $cacheFactory->createDistributed('swift::' . $cacheKey);
-
+ $this->swiftFactory = new SwiftConnectionFactory(\OC::$server->getMemCacheFactory()->createDistributed('swift::'));
$this->params = $params;
}
/**
- * @suppress PhanNonClassMethodCall
+ * @return \OpenStack\ObjectStore\v1\Models\Container
+ * @throws StorageAuthException
+ * @throws \Exception
*/
- protected function init() {
- if ($this->container) {
- return;
+ private function getContainer() {
+ if (is_null($this->container)) {
+ $this->container = $this->swiftFactory->getContainer($this->params);
}
-
- $this->importToken();
-
- /** @var Token $token */
- $token = $this->client->getTokenObject();
-
- if (!$token || $token->hasExpired()) {
- try {
- $this->client->authenticate();
- $this->exportToken();
- } catch (ClientErrorResponseException $e) {
- $statusCode = $e->getResponse()->getStatusCode();
- if ($statusCode == 412) {
- throw new StorageAuthException('Precondition failed, verify the keystone url', $e);
- } else if ($statusCode === 401) {
- throw new StorageAuthException('Authentication failed, verify the username, password and possibly tenant', $e);
- } else {
- throw new StorageAuthException('Unknown error', $e);
- }
- }
- }
-
-
- /** @var Catalog $catalog */
- $catalog = $this->client->getCatalog();
-
- if (count($catalog->getItems()) === 0) {
- throw new StorageAuthException('Keystone did not provide a valid catalog, verify the credentials');
- }
-
- if (isset($this->params['serviceName'])) {
- $serviceName = $this->params['serviceName'];
- } else {
- $serviceName = Service::DEFAULT_NAME;
- }
-
- if (isset($this->params['urlType'])) {
- $urlType = $this->params['urlType'];
- if ($urlType !== 'internalURL' && $urlType !== 'publicURL') {
- throw new StorageNotAvailableException('Invalid url type');
- }
- } else {
- $urlType = Service::DEFAULT_URL_TYPE;
- }
-
- $catalogItem = $this->getCatalogForService($catalog, $serviceName);
- if (!$catalogItem) {
- $available = implode(', ', $this->getAvailableServiceNames($catalog));
- throw new StorageNotAvailableException(
- "Service $serviceName not found in service catalog, available services: $available"
- );
- } else if (isset($this->params['region'])) {
- $this->validateRegion($catalogItem, $this->params['region']);
- }
-
- $this->objectStoreService = $this->client->objectStoreService($serviceName, $this->params['region'], $urlType);
-
- try {
- $this->container = $this->objectStoreService->getContainer($this->params['container']);
- } catch (ClientErrorResponseException $ex) {
- // if the container does not exist and autocreate is true try to create the container on the fly
- if (isset($this->params['autocreate']) && $this->params['autocreate'] === true) {
- $this->container = $this->objectStoreService->createContainer($this->params['container']);
- } else {
- throw $ex;
- }
- } catch (CurlException $e) {
- if ($e->getErrorNo() === 7) {
- $host = $e->getCurlHandle()->getUrl()->getHost() . ':' . $e->getCurlHandle()->getUrl()->getPort();
- \OC::$server->getLogger()->error("Can't connect to object storage server at $host");
- throw new StorageNotAvailableException("Can't connect to object storage server at $host", StorageNotAvailableException::STATUS_ERROR, $e);
- }
- throw $e;
- }
- }
-
- private function exportToken() {
- $export = $this->client->exportCredentials();
- $export['catalog'] = array_map(function (CatalogItem $item) {
- return [
- 'name' => $item->getName(),
- 'endpoints' => $item->getEndpoints(),
- 'type' => $item->getType()
- ];
- }, $export['catalog']->getItems());
- $this->memcache->set('token', json_encode($export));
- }
-
- private function importToken() {
- $cachedTokenString = $this->memcache->get('token');
- if ($cachedTokenString) {
- $cachedToken = json_decode($cachedTokenString, true);
- $cachedToken['catalog'] = array_map(function (array $item) {
- $itemClass = new \stdClass();
- $itemClass->name = $item['name'];
- $itemClass->endpoints = array_map(function (array $endpoint) {
- return (object)$endpoint;
- }, $item['endpoints']);
- $itemClass->type = $item['type'];
-
- return $itemClass;
- }, $cachedToken['catalog']);
- try {
- $this->client->importCredentials($cachedToken);
- } catch (\Exception $e) {
- $this->client->setTokenObject(new Token());
- }
- }
- }
-
- /**
- * @param Catalog $catalog
- * @param $name
- * @return null|CatalogItem
- */
- private function getCatalogForService(Catalog $catalog, $name) {
- foreach ($catalog->getItems() as $item) {
- /** @var CatalogItem $item */
- if ($item->hasType(Service::DEFAULT_TYPE) && $item->hasName($name)) {
- return $item;
- }
- }
-
- return null;
- }
-
- private function validateRegion(CatalogItem $item, $region) {
- $endPoints = $item->getEndpoints();
- foreach ($endPoints as $endPoint) {
- if ($endPoint->region === $region) {
- return;
- }
- }
-
- $availableRegions = implode(', ', array_map(function ($endpoint) {
- return $endpoint->region;
- }, $endPoints));
-
- throw new StorageNotAvailableException("Invalid region '$region', available regions: $availableRegions");
- }
-
- private function getAvailableServiceNames(Catalog $catalog) {
- return array_map(function (CatalogItem $item) {
- return $item->getName();
- }, array_filter($catalog->getItems(), function (CatalogItem $item) {
- return $item->hasType(Service::DEFAULT_TYPE);
- }));
+ return $this->container;
}
/**
@@ -254,29 +71,29 @@ class Swift implements IObjectStore {
/**
* @param string $urn the unified resource name used to identify the object
* @param resource $stream stream with the data to write
- * @throws Exception from openstack lib when something goes wrong
+ * @throws \Exception from openstack lib when something goes wrong
*/
public function writeObject($urn, $stream) {
- $this->init();
- $this->container->uploadObject($urn, $stream);
+ $this->getContainer()->createObject([
+ 'name' => $urn,
+ 'stream' => stream_for($stream)
+ ]);
}
/**
* @param string $urn the unified resource name used to identify the object
* @return resource stream with the read data
- * @throws Exception from openstack lib when something goes wrong
+ * @throws \Exception from openstack lib when something goes wrong
*/
public function readObject($urn) {
- $this->init();
- $object = $this->container->getObject($urn);
+ $object = $this->getContainer()->getObject($urn);
// we need to keep a reference to objectContent or
// the stream will be closed before we can do anything with it
- /** @var $objectContent \Guzzle\Http\EntityBody * */
- $objectContent = $object->getContent();
+ $objectContent = $object->download();
$objectContent->rewind();
- $stream = $objectContent->getStream();
+ $stream = $objectContent->detach();
// save the object content in the context of the stream to prevent it being gc'd until the stream is closed
stream_context_set_option($stream, 'swift', 'content', $objectContent);
@@ -286,17 +103,18 @@ class Swift implements IObjectStore {
/**
* @param string $urn Unified Resource Name
* @return void
- * @throws Exception from openstack lib when something goes wrong
+ * @throws \Exception from openstack lib when something goes wrong
*/
public function deleteObject($urn) {
- $this->init();
- // see https://github.com/rackspace/php-opencloud/issues/243#issuecomment-30032242
- $this->container->dataObject()->setName($urn)->delete();
+ $this->getContainer()->getObject($urn)->delete();
}
- public function deleteContainer($recursive = false) {
- $this->init();
- $this->container->delete($recursive);
+ /**
+ * @return void
+ * @throws \Exception from openstack lib when something goes wrong
+ */
+ public function deleteContainer() {
+ $this->getContainer()->delete();
}
}
diff --git a/lib/private/Files/ObjectStore/SwiftConnectionFactory.php b/lib/private/Files/ObjectStore/SwiftConnectionFactory.php
new file mode 100644
index 00000000000..5401cb2c840
--- /dev/null
+++ b/lib/private/Files/ObjectStore/SwiftConnectionFactory.php
@@ -0,0 +1,154 @@
+<?php
+declare(strict_types=1);
+/**
+ * @copyright Copyright (c) 2018 Robin Appelman <robin@icewind.nl>
+ *
+ * @license GNU AGPL version 3 or any later version
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+namespace OC\Files\ObjectStore;
+
+use GuzzleHttp\Client;
+use GuzzleHttp\Exception\ClientException;
+use GuzzleHttp\Exception\ConnectException;
+use GuzzleHttp\Exception\RequestException;
+use GuzzleHttp\HandlerStack;
+use OCP\Files\StorageAuthException;
+use OCP\Files\StorageNotAvailableException;
+use OCP\ICache;
+use OpenStack\Common\Error\BadResponseError;
+use OpenStack\Identity\v2\Models\Token;
+use OpenStack\Identity\v2\Service;
+use OpenStack\OpenStack;
+use OpenStack\Common\Transport\Utils as TransportUtils;
+use Psr\Http\Message\RequestInterface;
+
+class SwiftConnectionFactory {
+ private $cache;
+
+ public function __construct(ICache $cache) {
+ $this->cache = $cache;
+ }
+
+ private function getCachedToken(string $cacheKey) {
+ $cachedTokenString = $this->cache->get($cacheKey . '/token');
+ if ($cachedTokenString) {
+ return json_decode($cachedTokenString);
+ } else {
+ return null;
+ }
+ }
+
+ private function cacheToken(Token $token, string $cacheKey) {
+ $this->cache->set($cacheKey . '/token', json_encode($token));
+ }
+
+ /**
+ * @param array $params
+ * @return OpenStack
+ * @throws StorageAuthException
+ * @throws \Exception
+ */
+ private function getClient(array &$params) {
+ if (isset($params['bucket'])) {
+ $params['container'] = $params['bucket'];
+ }
+ if (!isset($params['container'])) {
+ $params['container'] = 'owncloud';
+ }
+ if (!isset($params['autocreate'])) {
+ // should only be true for tests
+ $params['autocreate'] = false;
+ }
+
+ $cacheKey = $params['username'] . '@' . $params['url'] . '/' . $params['bucket'];
+ $token = $this->getCachedToken($cacheKey);
+ $hasToken = is_array($token) && (new \DateTimeImmutable($token['expires_at'])) > (new \DateTimeImmutable('now'));
+ if ($hasToken) {
+ $params['cachedToken'] = $token;
+ }
+ $httpClient = new Client([
+ 'base_uri' => TransportUtils::normalizeUrl($params['url']),
+ 'handler' => HandlerStack::create()
+ ]);
+
+ $authService = Service::factory($httpClient);
+ $params['identityService'] = $authService;
+ $params['authUrl'] = $params['url'];
+ $client = new OpenStack($params);
+
+ if (!$hasToken) {
+ try {
+ $token = $authService->generateToken($params);
+ $this->cacheToken($token, $cacheKey);
+ } catch (ConnectException $e) {
+ throw new StorageAuthException('Failed to connect to keystone, verify the keystone url', $e);
+ } catch (ClientException $e) {
+ $statusCode = $e->getResponse()->getStatusCode();
+ if ($statusCode === 404) {
+ throw new StorageAuthException('Keystone not found, verify the keystone url', $e);
+ } else if ($statusCode === 412) {
+ throw new StorageAuthException('Precondition failed, verify the keystone url', $e);
+ } else if ($statusCode === 401) {
+ throw new StorageAuthException('Authentication failed, verify the username, password and possibly tenant', $e);
+ } else {
+ throw new StorageAuthException('Unknown error', $e);
+ }
+ } catch (RequestException $e) {
+ throw new StorageAuthException('Connection reset while connecting to keystone, verify the keystone url', $e);
+ }
+ }
+
+ return $client;
+ }
+
+ /**
+ * @param array $params
+ * @return \OpenStack\ObjectStore\v1\Models\Container
+ * @throws StorageAuthException
+ * @throws \Exception
+ */
+ public function getContainer(array $params) {
+
+ $client = $this->getClient($params);
+ $objectStoreService = $client->objectStoreV1();
+
+ $autoCreate = isset($params['autocreate']) && $params['autocreate'] === true;
+ try {
+ $container = $objectStoreService->getContainer($params['container']);
+ if ($autoCreate) {
+ $container->getMetadata();
+ }
+ return $container;
+ } catch (BadResponseError $ex) {
+ // if the container does not exist and autocreate is true try to create the container on the fly
+ if ($ex->getResponse()->getStatusCode() === 404 && $autoCreate) {
+ return $objectStoreService->createContainer([
+ 'name' => $params['container']
+ ]);
+ } else {
+ throw $ex;
+ }
+ } catch (ConnectException $e) {
+ /** @var RequestInterface $request */
+ $request = $e->getRequest();
+ $host = $request->getUri()->getHost() . ':' . $request->getUri()->getPort();
+ \OC::$server->getLogger()->error("Can't connect to object storage server at $host");
+ throw new StorageNotAvailableException("Can't connect to object storage server at $host", StorageNotAvailableException::STATUS_ERROR, $e);
+ }
+ }
+}