d2d73f1ce8
Signed-off-by: Morris Jobke <hey@morrisjobke.de>
287 lines
8.4 KiB
PHP
287 lines
8.4 KiB
PHP
<?php
|
|
/**
|
|
* @copyright Copyright (c) 2016, ownCloud, Inc.
|
|
*
|
|
* @author Jörn Friedrich Dreyer <jfd@butonic.de>
|
|
* @author Morris Jobke <hey@morrisjobke.de>
|
|
* @author Robin Appelman <robin@icewind.nl>
|
|
* @author William Pain <pain.william@gmail.com>
|
|
*
|
|
* @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 <http://www.gnu.org/licenses/>
|
|
*
|
|
*/
|
|
|
|
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);
|
|
}
|
|
|
|
}
|