* @author Morris Jobke * @author Robin Appelman * @author William Pain * * @license AGPL-3.0 * * This code is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License, version 3, * as published by the Free Software Foundation. * * 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, version 3, * along with this program. If not, see * */ namespace OC\Files\ObjectStore; use Guzzle\Http\Exception\ClientErrorResponseException; 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 */ private $container; private $memcache; 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->params = $params; } protected function init() { if ($this->container) { return; } $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 (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; } } } 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 string the container name where objects are stored */ public function getStorageId() { return $this->params['container']; } /** * @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 */ public function writeObject($urn, $stream) { $this->init(); $this->container->uploadObject($urn, $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 */ public function readObject($urn) { $this->init(); $object = $this->container->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->rewind(); $stream = $objectContent->getStream(); // 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); RetryWrapper::wrap($stream); } /** * @param string $urn Unified Resource Name * @return void * @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(); } public function deleteContainer($recursive = false) { $this->init(); $this->container->delete($recursive); } }