From e5c695f72ac815aa81f25e1beabc5c70bafc0327 Mon Sep 17 00:00:00 2001 From: Sam Choi Date: Wed, 16 Apr 2014 22:58:32 -0700 Subject: [PATCH] Partially implements blueprint psr-2 Change-Id: Ifd8b7237f650f0f18fa58ab0e870eb2774c776fc --- doc/oo-tutorial-code.php | 4 - doc/streams-tutorial-example.php | 23 +- src/OpenStack/Autoloader.php | 155 +- src/OpenStack/Bootstrap.php | 390 +-- src/OpenStack/Exception.php | 18 +- src/OpenStack/Services/IdentityService.php | 1237 +++++---- src/OpenStack/Storage/ObjectStorage.php | 980 +++---- src/OpenStack/Storage/ObjectStorage/ACL.php | 878 +++--- .../Storage/ObjectStorage/Container.php | 1894 ++++++------- .../ContainerNotEmptyException.php | 18 +- .../ContentVerificationException.php | 18 +- .../Storage/ObjectStorage/Object.php | 925 ++++--- .../ObjectStorage/ReadOnlyObjectException.php | 18 +- .../Storage/ObjectStorage/RemoteObject.php | 1145 ++++---- .../Storage/ObjectStorage/StreamWrapper.php | 2442 +++++++++-------- .../Storage/ObjectStorage/StreamWrapperFS.php | 271 +- .../Storage/ObjectStorage/Subdir.php | 104 +- .../Transport/AuthorizationException.php | 18 +- src/OpenStack/Transport/ClientInterface.php | 191 +- src/OpenStack/Transport/ConflictException.php | 18 +- .../Transport/FileNotFoundException.php | 18 +- .../Transport/ForbiddenException.php | 18 +- src/OpenStack/Transport/GuzzleClient.php | 377 +-- .../Transport/LengthRequiredException.php | 18 +- .../Transport/MethodNotAllowedException.php | 18 +- src/OpenStack/Transport/ResponseInterface.php | 305 +- src/OpenStack/Transport/ServerException.php | 18 +- .../Transport/UnauthorizedException.php | 18 +- .../UnprocessableEntityException.php | 18 +- test/AuthTest.php | 49 +- test/TestCase.php | 407 ++- test/Tests/ACLTest.php | 281 +- test/Tests/AutoloaderTest.php | 56 +- test/Tests/BootstrapTest.php | 35 +- test/Tests/ContainerTest.php | 803 +++--- test/Tests/GuzzleClientTest.php | 99 +- test/Tests/IdentityServicesTest.php | 813 +++--- test/Tests/ObjectStorageTest.php | 547 ++-- test/Tests/ObjectTest.php | 205 +- test/Tests/RemoteObjectTest.php | 507 ++-- test/Tests/StreamWrapperFSTest.php | 1162 ++++---- test/Tests/StreamWrapperTest.php | 1098 ++++---- test/bootstrap_curl.php | 20 +- test/bootstrap_phpstream.php | 20 +- 44 files changed, 8957 insertions(+), 8700 deletions(-) diff --git a/doc/oo-tutorial-code.php b/doc/oo-tutorial-code.php index f3ee9a7..360737c 100644 --- a/doc/oo-tutorial-code.php +++ b/doc/oo-tutorial-code.php @@ -1,7 +1,6 @@ name()); printf("Size: %d \n", $object->contentLength()); printf("Type: %s \n", $object->contentType()); print $object->content() . PHP_EOL; - - - diff --git a/doc/streams-tutorial-example.php b/doc/streams-tutorial-example.php index 75ad963..20947f5 100644 --- a/doc/streams-tutorial-example.php +++ b/doc/streams-tutorial-example.php @@ -8,10 +8,10 @@ Bootstrap::useStreamWrappers(); $ini = parse_ini_file(getenv('HOME') . '/.OpenStack.ini'); $settings = array( - 'account' => $ini['account'], - 'key' => $ini['secret'], - 'tenantid' => $ini['tenantId'], - 'endpoint' => $ini['url'], + 'account' => $ini['account'], + 'key' => $ini['secret'], + 'tenantid' => $ini['tenantId'], + 'endpoint' => $ini['url'], ); Bootstrap::setConfiguration($settings); @@ -22,7 +22,7 @@ fclose($newfile); // Check for an object: if (file_exists('swift://Example/my_file.txt')) { - print "Found my_file.txt." . PHP_EOL; + print "Found my_file.txt." . PHP_EOL; } // Get an entire object at once: @@ -30,13 +30,12 @@ $file = file_get_contents('swift://Example/my_file.txt'); print 'File: ' . $file . PHP_EOL; $cxt = stream_context_create(array( - 'swift' => array( - 'account' => $ini['account'], - 'key' => $ini['secret'], - 'tenantid' => $ini['tenantId'], - 'endpoint' => $ini['url'], - ), + 'swift' => array( + 'account' => $ini['account'], + 'key' => $ini['secret'], + 'tenantid' => $ini['tenantId'], + 'endpoint' => $ini['url'], + ), )); print file_get_contents('swift://Example/my_file.txt', FALSE, $cxt); - diff --git a/src/OpenStack/Autoloader.php b/src/OpenStack/Autoloader.php index 7bc7755..06c6a9e 100644 --- a/src/OpenStack/Autoloader.php +++ b/src/OpenStack/Autoloader.php @@ -2,17 +2,17 @@ /* ============================================================================ (c) Copyright 2014 Hewlett-Packard Development Company, L.P. - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at - http://www.apache.org/licenses/LICENSE-2.0 + http://www.apache.org/licenses/LICENSE-2.0 - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. ============================================================================ */ /** * An Autoloader to use for the case Composer isn't available. @@ -44,74 +44,77 @@ namespace OpenStack; */ Class Autoloader { - /** - * @var string The directory where OpenStack is located. - */ - public static $basedir = __DIR__; + /** + * @var string The directory where OpenStack is located. + */ + public static $basedir = __DIR__; - /** - * Add the autoloader to PHP's autoloader list. - * - * This will add the internal special-purpose - * autoloader to the list of autoloaders that PHP will - * leverage to resolve class paths. - * - * Because OpenStack is PSR-4 compliant, any - * full PSR-4 classloader should be capable of loading - * these classes witout issue. You may prefer to use - * a standard PSR-4 loader instead of this one. - * - * @see https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-4.md - */ - public static function useAutoloader() { - spl_autoload_register(__NAMESPACE__ . '\Autoloader::autoload'); - } - - /** - * OpenStack autoloader. - * - * An implementation of a PHP autoload function. Use - * OpenStack::useAutoloader() if you want PHP to automatically - * load classes using this autoloader. - * - * // Enable the autoloader. - * Autoloader::useAutoloader(); - * - * This is a special-purpose autoloader for loading - * only the OpenStack classes. It will not attempt to - * autoload anything outside of the OpenStack namespace. - * - * Because this is a special-purpose autoloader, it - * should be safe to use with other special-purpose - * autoloaders (and also projects that don't - * rely upon autoloaders). - * - * @param string $klass The fully qualified name of the class to be autoloaded. - */ - public static function autoload($klass) { - $components = explode('\\', $klass); - if (empty($components[0])) { - array_shift($components); + /** + * Add the autoloader to PHP's autoloader list. + * + * This will add the internal special-purpose + * autoloader to the list of autoloaders that PHP will + * leverage to resolve class paths. + * + * Because OpenStack is PSR-4 compliant, any + * full PSR-4 classloader should be capable of loading + * these classes witout issue. You may prefer to use + * a standard PSR-4 loader instead of this one. + * + * @see https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-4.md + */ + public static function useAutoloader() + { + spl_autoload_register(__NAMESPACE__ . '\Autoloader::autoload'); } - // This class loader ONLY loads - // our classes. A general purpose - // classloader should be used for - // more sophisticated needs. - if ($components[0] != 'OpenStack') { - return; + /** + * OpenStack autoloader. + * + * An implementation of a PHP autoload function. Use + * OpenStack::useAutoloader() if you want PHP to automatically + * load classes using this autoloader. + * + * // Enable the autoloader. + * Autoloader::useAutoloader(); + * + * This is a special-purpose autoloader for loading + * only the OpenStack classes. It will not attempt to + * autoload anything outside of the OpenStack namespace. + * + * Because this is a special-purpose autoloader, it + * should be safe to use with other special-purpose + * autoloaders (and also projects that don't + * rely upon autoloaders). + * + * @param string $klass The fully qualified name of the class to be autoloaded. + */ + public static function autoload($klass) + { + $components = explode('\\', $klass); + if (empty($components[0])) { + array_shift($components); + } + + // This class loader ONLY loads + // our classes. A general purpose + // classloader should be used for + // more sophisticated needs. + if ($components[0] != 'OpenStack') { + return; + } + + // We need the path up to, but not including, the root OpenStack dir: + $loc = DIRECTORY_SEPARATOR . 'OpenStack'; + $local_path = substr(self::$basedir, 0, strrpos(self::$basedir, $loc)); + + array_unshift($components, $local_path); + $path = implode(DIRECTORY_SEPARATOR, $components) . '.php'; + + if (file_exists($path)) { + require $path; + + return; + } } - - // We need the path up to, but not including, the root OpenStack dir: - $loc = DIRECTORY_SEPARATOR . 'OpenStack'; - $local_path = substr(self::$basedir, 0, strrpos(self::$basedir, $loc)); - - array_unshift($components, $local_path); - $path = implode(DIRECTORY_SEPARATOR, $components) . '.php'; - - if (file_exists($path)) { - require $path; - return; - } - } -} \ No newline at end of file +} diff --git a/src/OpenStack/Bootstrap.php b/src/OpenStack/Bootstrap.php index ef686a4..abf2ec8 100644 --- a/src/OpenStack/Bootstrap.php +++ b/src/OpenStack/Bootstrap.php @@ -2,17 +2,17 @@ /* ============================================================================ (c) Copyright 2012-2014 Hewlett-Packard Development Company, L.P. - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at - http://www.apache.org/licenses/LICENSE-2.0 + http://www.apache.org/licenses/LICENSE-2.0 - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. ============================================================================ */ /** * OpenStack SDK for PHP configuration. @@ -23,7 +23,6 @@ namespace OpenStack; use OpenStack\Services\IdentityService; -use OpenStack\Exception; /** * Bootstrapping services. @@ -80,204 +79,207 @@ use OpenStack\Exception; * * It's awesome. Trust me. */ -class Bootstrap { - - public static $config = array( - // The transport implementation. By default, we use the Guzzle Client - 'transport' => '\OpenStack\Transport\GuzzleClient', - ); - - /** - * @var \OpenStack\Services\IdentityService An identity services object - * created from the global settings. - */ - public static $identity = NULL; - - /** - * @var \OpenStack\Transport\ClientInterface A transport client for requests. - */ - public static $transport = NULL; - - /** - * Register stream wrappers for OpenStack. - * - * This registers the ObjectStorage stream wrappers, which allow you to access - * ObjectStorage through standard file access mechanisms. - * - * // Enable stream wrapper. - * Bootstrap::useStreamWrappers(); - * - * // Create a context resource. - * $cxt = stream_context_create(array( - * 'tenantid' => '12de21', - * 'username' => 'foobar', - * 'password' => 'f78saf7hhlll', - * 'endpoint' => 'https://identity.hpcloud.com' // <-- not real URL! - * )); - * - * // Get the contents of a Swift object. - * $content = file_get_contents('swift://public/notes.txt', 'r', FALSE, $cxt); - */ - public static function useStreamWrappers() { - $swift = stream_wrapper_register( - \OpenStack\Storage\ObjectStorage\StreamWrapper::DEFAULT_SCHEME, - '\OpenStack\Storage\ObjectStorage\StreamWrapper' +class Bootstrap +{ + public static $config = array( + // The transport implementation. By default, we use the Guzzle Client + 'transport' => '\OpenStack\Transport\GuzzleClient', ); - $swiftfs = stream_wrapper_register( - \OpenStack\Storage\ObjectStorage\StreamWrapperFS::DEFAULT_SCHEME, - '\OpenStack\Storage\ObjectStorage\StreamWrapperFS' - ); + /** + * @var \OpenStack\Services\IdentityService An identity services object + * created from the global settings. + */ + public static $identity = NULL; - return ($swift && $swiftfs); - } + /** + * @var \OpenStack\Transport\ClientInterface A transport client for requests. + */ + public static $transport = NULL; - /** - * Set configuration directives for OpenStack. - * - * This merges the provided associative array into the existing - * configuration parameters (Bootstrap::$config). - * - * All of the OpenStack classes share the same configuration. This - * ensures that a stable runtime environment is maintained. - * - * Common configuration directives: - * - * - 'transport': The namespaced classname for the transport that - * should be used. Example: @code \OpenStack\Transport\CURLTransport @endcode - * - 'transport.debug': The integer 1 for enabling debug, 0 for - * disabling. Enabling will turn on verbose debugging output - * for any transport that supports it. - * - 'transport.timeout': An integer value indicating how long - * the transport layer should wait for an HTTP request. A - * transport MAY ignore this parameter, but the ones included - * with the library honor it. - * - 'transport.ssl_verify': Set this to FALSE to turn off SSL certificate - * verification. This is NOT recommended, but is sometimes necessary for - * certain proxy configurations. - * - 'transport.proxy': Set the proxy as a string. - * - 'username' and 'password' - * - 'tenantid' - * - 'endpoint': The full URL to identity services. This is used by stream - * wrappers. - * - * @param array $array An associative array of configuration directives. - */ - public static function setConfiguration($array) { - self::$config = $array + self::$config; - } + /** + * Register stream wrappers for OpenStack. + * + * This registers the ObjectStorage stream wrappers, which allow you to access + * ObjectStorage through standard file access mechanisms. + * + * // Enable stream wrapper. + * Bootstrap::useStreamWrappers(); + * + * // Create a context resource. + * $cxt = stream_context_create(array( + * 'tenantid' => '12de21', + * 'username' => 'foobar', + * 'password' => 'f78saf7hhlll', + * 'endpoint' => 'https://identity.hpcloud.com' // <-- not real URL! + * )); + * + * // Get the contents of a Swift object. + * $content = file_get_contents('swift://public/notes.txt', 'r', FALSE, $cxt); + */ + public static function useStreamWrappers() + { + $swift = stream_wrapper_register( + \OpenStack\Storage\ObjectStorage\StreamWrapper::DEFAULT_SCHEME, + '\OpenStack\Storage\ObjectStorage\StreamWrapper' + ); - /** - * Get a configuration option. - * - * Get a configuration option by name, with an optional default. - * - * @param string $name The name of the configuration option to get. - * @param mixed $default The default value to return if the name is not found. - * - * @return mixed The value, if found; or the default, if set; or NULL. - */ - public static function config($name = NULL, $default = NULL) { + $swiftfs = stream_wrapper_register( + \OpenStack\Storage\ObjectStorage\StreamWrapperFS::DEFAULT_SCHEME, + '\OpenStack\Storage\ObjectStorage\StreamWrapperFS' + ); - // If no name is specified, return the entire config array. - if (empty($name)) { - return self::$config; + return ($swift && $swiftfs); } - // If the config value exists, return that. - if (isset(self::$config[$name])) { - return self::$config[$name]; + /** + * Set configuration directives for OpenStack. + * + * This merges the provided associative array into the existing + * configuration parameters (Bootstrap::$config). + * + * All of the OpenStack classes share the same configuration. This + * ensures that a stable runtime environment is maintained. + * + * Common configuration directives: + * + * - 'transport': The namespaced classname for the transport that + * should be used. Example: @code \OpenStack\Transport\CURLTransport @endcode + * - 'transport.debug': The integer 1 for enabling debug, 0 for + * disabling. Enabling will turn on verbose debugging output + * for any transport that supports it. + * - 'transport.timeout': An integer value indicating how long + * the transport layer should wait for an HTTP request. A + * transport MAY ignore this parameter, but the ones included + * with the library honor it. + * - 'transport.ssl_verify': Set this to FALSE to turn off SSL certificate + * verification. This is NOT recommended, but is sometimes necessary for + * certain proxy configurations. + * - 'transport.proxy': Set the proxy as a string. + * - 'username' and 'password' + * - 'tenantid' + * - 'endpoint': The full URL to identity services. This is used by stream + * wrappers. + * + * @param array $array An associative array of configuration directives. + */ + public static function setConfiguration($array) + { + self::$config = $array + self::$config; } - // Otherwise, just return the default value. - return $default; - } + /** + * Get a configuration option. + * + * Get a configuration option by name, with an optional default. + * + * @param string $name The name of the configuration option to get. + * @param mixed $default The default value to return if the name is not found. + * + * @return mixed The value, if found; or the default, if set; or NULL. + */ + public static function config($name = NULL, $default = NULL) + { + // If no name is specified, return the entire config array. + if (empty($name)) { + return self::$config; + } - /** - * Check whether the given configuration option is set. - * - * if (Bootstrap::hasConfig('transport')) { - * syslog(LOG_INFO, 'An alternate transport is supplied.'); - * } - * - * @param string $name The name of the item to check for. - * - * @return boolean TRUE if the named option is set, FALSE otherwise. Note that - * the value may be falsey (FALSE, 0, etc.), but if the value is NULL, this - * will return false. - */ - public static function hasConfig($name) { - return isset(self::$config[$name]); - } + // If the config value exists, return that. + if (isset(self::$config[$name])) { + return self::$config[$name]; + } - /** - * Get a \OpenStack\Services\IdentityService object from the bootstrap config. - * - * A factory helper function that uses the bootstrap configuration to create - * a ready to use \OpenStack\Services\IdentityService object. - * - * @param bool $force Whether to force the generation of a new object even if - * one is already cached. - * - * @return \OpenStack\Services\IdentityService An authenticated ready to use - * \OpenStack\Services\IdentityService object. - * @throws \OpenStack\Exception When the needed configuration to authenticate - * is not available. - */ - public static function identity($force = FALSE) { - - $transport = self::transport(); - - // If we already have an identity make sure the token is not expired. - if ($force || is_null(self::$identity) || self::$identity->isExpired()) { - - // Make sure we have an endpoint to use - if (!self::hasConfig('endpoint')) { - throw new Exception('Unable to authenticate. No endpoint supplied.'); - } - - // User cannot be an empty string, so we need - // to do more checking than self::hasConfig(), which returns TRUE - // if an item exists and is an empty string. - $user = self::config('username', NULL); - - // Check if we have a username/password - if (!empty($user) && self::hasConfig('password')) { - $is = new IdentityService(self::config('endpoint'), $transport); - $is->authenticateAsUser($user, self::config('password'), self::config('tenantid', NULL), self::config('tenantname', NULL)); - self::$identity = $is; - } - else { - throw new Exception('Unable to authenticate. No user credentials supplied.'); - } + // Otherwise, just return the default value. + return $default; } - return self::$identity; - } - - /** - * Get a transport client. - * - * @param boolean $reset Whether to recreate the transport client if one already exists. - * @return \OpenStack\Transport\ClientInterface A transport client. - */ - public static function transport($reset = FALSE) { - - if (is_null(self::$transport) || $reset == TRUE) { - $options = [ - 'ssl_verify' => self::config('ssl_verify', TRUE), - 'timeout' => self::config('timeout', 0), // 0 is no timeout. - 'debug' => self::config('debug', 0), - ]; - $proxy = self::config('proxy', FALSE); - if ($proxy) { - $options['proxy'] = $proxy; - } - - $klass = self::config('transport'); - self::$transport = new $klass($options); + /** + * Check whether the given configuration option is set. + * + * if (Bootstrap::hasConfig('transport')) { + * syslog(LOG_INFO, 'An alternate transport is supplied.'); + * } + * + * @param string $name The name of the item to check for. + * + * @return boolean TRUE if the named option is set, FALSE otherwise. Note that + * the value may be falsey (FALSE, 0, etc.), but if the value is NULL, this + * will return false. + */ + public static function hasConfig($name) + { + return isset(self::$config[$name]); } - return self::$transport; - } + /** + * Get a \OpenStack\Services\IdentityService object from the bootstrap config. + * + * A factory helper function that uses the bootstrap configuration to create + * a ready to use \OpenStack\Services\IdentityService object. + * + * @param bool $force Whether to force the generation of a new object even if + * one is already cached. + * + * @return \OpenStack\Services\IdentityService An authenticated ready to use + * \OpenStack\Services\IdentityService object. + * @throws \OpenStack\Exception When the needed configuration to authenticate + * is not available. + */ + public static function identity($force = FALSE) + { + $transport = self::transport(); + + // If we already have an identity make sure the token is not expired. + if ($force || is_null(self::$identity) || self::$identity->isExpired()) { + + // Make sure we have an endpoint to use + if (!self::hasConfig('endpoint')) { + throw new Exception('Unable to authenticate. No endpoint supplied.'); + } + + // User cannot be an empty string, so we need + // to do more checking than self::hasConfig(), which returns TRUE + // if an item exists and is an empty string. + $user = self::config('username', NULL); + + // Check if we have a username/password + if (!empty($user) && self::hasConfig('password')) { + $is = new IdentityService(self::config('endpoint'), $transport); + $is->authenticateAsUser($user, self::config('password'), self::config('tenantid', NULL), self::config('tenantname', NULL)); + self::$identity = $is; + } else { + throw new Exception('Unable to authenticate. No user credentials supplied.'); + } + } + + return self::$identity; + } + + /** + * Get a transport client. + * + * @param boolean $reset Whether to recreate the transport client if one already exists. + * + * @return \OpenStack\Transport\ClientInterface A transport client. + */ + public static function transport($reset = FALSE) + { + if (is_null(self::$transport) || $reset == TRUE) { + $options = [ + 'ssl_verify' => self::config('ssl_verify', TRUE), + 'timeout' => self::config('timeout', 0), // 0 is no timeout. + 'debug' => self::config('debug', 0), + ]; + $proxy = self::config('proxy', FALSE); + if ($proxy) { + $options['proxy'] = $proxy; + } + + $klass = self::config('transport'); + self::$transport = new $klass($options); + } + + return self::$transport; + } } diff --git a/src/OpenStack/Exception.php b/src/OpenStack/Exception.php index 02c7ea8..a17613f 100644 --- a/src/OpenStack/Exception.php +++ b/src/OpenStack/Exception.php @@ -2,17 +2,17 @@ /* ============================================================================ (c) Copyright 2012-2014 Hewlett-Packard Development Company, L.P. - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at - http://www.apache.org/licenses/LICENSE-2.0 + http://www.apache.org/licenses/LICENSE-2.0 - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. ============================================================================ */ /** * The parent exception class for OpenStack. diff --git a/src/OpenStack/Services/IdentityService.php b/src/OpenStack/Services/IdentityService.php index 94563c7..a7781b1 100644 --- a/src/OpenStack/Services/IdentityService.php +++ b/src/OpenStack/Services/IdentityService.php @@ -2,17 +2,17 @@ /* ============================================================================ (c) Copyright 2012-2014 Hewlett-Packard Development Company, L.P. - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at - http://www.apache.org/licenses/LICENSE-2.0 + http://www.apache.org/licenses/LICENSE-2.0 - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. ============================================================================ */ /** * This file contains the main IdentityService class. @@ -121,617 +121,634 @@ use OpenStack\Transport\GuzzleClient; * than make repeated requests for identity information. * */ -class IdentityService { - /** - * The version of the API currently supported. - */ - const API_VERSION = '2.0'; - - /** - * The full OpenStack accept type. - */ - const ACCEPT_TYPE = 'application/json'; - - // This is no longer supported. - //const ACCEPT_TYPE = 'application/vnd.openstack.identity+json;version=2.0'; - - /** - * The URL to the CS endpoint. - */ - protected $endpoint; - - /** - * The details sent with the token. - * - * The exact details of this array will differ depending on what type of - * authentication is used. For example, authenticating by username and - * password will set tenant information. Authenticating by username and - * password, however, will leave the tenant section empty. - * - * This is an associative array looking like this: - * - * 'auth_123abc321defef99', - * // Only non-empty for username/password auth. - * 'tenant' => array( - * 'id' => '123456', - * 'name' => 'matt.butcher@hp.com', - * ), - * 'expires' => '2012-01-24T12:46:01.682Z' - * ); - */ - protected $tokenDetails; - - /** - * The service catalog. - */ - protected $catalog = array(); - - protected $userDetails; - - /** - * The HTTP Client - */ - protected $client; - - /** - * Build a new IdentityService object. - * - * Each object is bound to a particular identity services endpoint. - * - * For the URL, you are advised to use the version without a - * version number at the end, e.g. http://cs.example.com/ rather - * than http://cs.example.com/v2.0. The version number must be - * controlled by the library. - * - * If a version is included in the URI, the library will attempt to use - * that URI. - * - * authenticateAsUser($username, $password); - * ?> - * - * @param string $url An URL pointing to the Identity Services endpoint. Note - * that you do not need the version identifier in the URL, as version - * information is sent in the HTTP headers rather than in the URL. The URL - * should always be to an SSL/TLS encrypted endpoint. - * @param \OpenStack\Transport\ClientInterface $client An optional HTTP client - * to use when making the requests. - */ - public function __construct($url, \OpenStack\Transport\ClientInterface $client = NULL) { - $parts = parse_url($url); - - if (!empty($parts['path'])) { - $this->endpoint = rtrim($url, '/'); - } - else { - $this->endpoint = rtrim($url, '/') . '/v' . self::API_VERSION; - } - - // Guzzle is the default client to use. - if (is_null($client)) { - $this->client = new GuzzleClient(); - } - else { - $this->client = $client; - } - } - - /** - * Get the endpoint URL. - * - * This includes version number, so in that regard it is not an identical - * URL to the one passed into the constructor. - * - * @return string The complete URL to the identity services endpoint. - */ - public function url() { - return $this->endpoint; - } - - /** - * Send an authentication request. - * - * EXPERT: This allows authentication requests at a low level. For simple - * authentication requests using a username, see the - * authenticateAsUser() method. - * - * Here is an example of username/password-based authentication done with - * the authenticate() method: - * - * array( - * 'username' => $username, - * 'password' => $password, - * ), - * 'tenantId' => $tenantId, - * ); - * $token = $cs->authenticate($ops); - * ?> - * - * Note that the same authentication can be done by authenticateAsUser(). - * - * @param array $ops An associative array of authentication operations and - * their respective parameters. - * - * @return string The token. This is returned for simplicity. The full - * response is used to populate this object's service catalog, etc. The - * token is also retrievable with token(). - * @throws \OpenStack\Transport\AuthorizationException - * If authentication failed. - * @throws \OpenStack\Exception - * For abnormal network conditions. The message will give an indication as - * to the underlying problem. - */ - public function authenticate(array $ops) { - $url = $this->url() . '/tokens'; - $envelope = array( - 'auth' => $ops, - ); - - $body = json_encode($envelope); - - $headers = array( - 'Content-Type' => 'application/json', - 'Accept' => self::ACCEPT_TYPE, - 'Content-Length' => strlen($body), - ); - - $response = $this->client->doRequest($url, 'POST', $headers, $body); - - $this->handleResponse($response); - - return $this->token(); - } - - /** - * Authenticate to Identity Services with username, password, and either - * tenant ID or tenant Name. - * - * Given a OpenStack username and password, authenticate to Identity Services. - * Identity Services will then issue a token that can be used to access other - * OpenStack services. - * - * If a tenant ID is provided, this will also associate the user with the - * given tenant ID. If a tenant Name is provided, this will associate the user - * with the given tenant Name. Only the tenant ID or tenant Name needs to be - * given, not both. - * - * If no tenant ID or tenant Name is given, it will likely be necessary to - * rescope() the request (See also tenants()). - * - * Other authentication methods: - * - authenticate() - * - * @param string $username A valid username. - * @param string $password A password string. - * @param string $tenantId The tenant ID. This can be obtained through the - * OpenStack console. - * @param string $tenantName The tenant Name. This can be obtained through the - * OpenStack console. - * @throws \OpenStack\Transport\AuthorizationException If authentication failed. - * @throws \OpenStack\Exception For abnormal network conditions. The message - * will give an indication as to the underlying problem. - */ - public function authenticateAsUser($username, $password, $tenantId = NULL, $tenantName = NULL) { - $ops = array( - 'passwordCredentials' => array( - 'username' => $username, - 'password' => $password, - ), - ); - - // If a tenant ID is provided, added it to the auth array. - if (!empty($tenantId)) { - $ops['tenantId'] = $tenantId; - } - elseif (!empty($tenantName)) { - $ops['tenantName'] = $tenantName; - } - - - return $this->authenticate($ops); - } - - /** - * Get the token. - * - * This will not be populated until after one of the authentication - * methods has been run. - * - * @return string The token ID to be used in subsequent calls. - */ - public function token() { - return $this->tokenDetails['id']; - } - - /** - * Get the tenant ID associated with this token. - * - * If this token has a tenant ID, the ID will be returned. Otherwise, this - * will return NULL. - * - * This will not be populated until after an authentication method has been - * run. - * - * @return string The tenant ID if available, or NULL. - */ - public function tenantId() { - if (!empty($this->tokenDetails['tenant']['id'])) { - return $this->tokenDetails['tenant']['id']; - } - } - - /** - * Get the tenant name associated with this token. - * - * If this token has a tenant name, the name will be returned. Otherwise, this - * will return NULL. - * - * This will not be populated until after an authentication method has been - * run. - * - * @return string The tenant name if available, or NULL. - */ - public function tenantName() { - if (!empty($this->tokenDetails['tenant']['name'])) { - return $this->tokenDetails['tenant']['name']; - } - } - - /** - * Get the token details. - * - * This returns an associative array with several pieces of information - * about the token, including: - * - * - id: The token itself - * - expires: When the token expires - * - tenant_id: The tenant ID of the authenticated user. - * - tenant_name: The username of the authenticated user. - * - * 'auth_123abc321defef99', - * 'tenant' => array( - * 'id' => '123456', - * 'name' => 'matt.butcher@hp.com', - * ), - * 'expires' => '2012-01-24T12:46:01.682Z' - * ); - * - * This will not be populated until after authentication has been done. - * - * @return array An associative array of details. - */ - public function tokenDetails() { - return $this->tokenDetails; - } - - /** - * Check whether the current identity has an expired token. - * - * This does not perform a round-trip to the server. Instead, it compares the - * machine's local timestamp with the server's expiration time stamp. A - * mis-configured machine timestamp could give spurious results. - * - * @return boolean This will return FALSE if there is a current token and it - * has not yet expired (according to the date info). In all other cases it - * returns TRUE. - */ - public function isExpired() { - $details = $this->tokenDetails(); - - if (empty($details['expires'])) { - return TRUE; - } - - $currentDateTime = new \DateTime('now'); - $expireDateTime = new \DateTime($details['expires']); - - return $currentDateTime > $expireDateTime; - } - - /** - * Get the service catalog, optionaly filtering by type. - * - * This returns the service catalog (largely unprocessed) that - * is returned during an authentication request. If a type is passed in, - * only entries of that type are returned. If no type is passed in, the - * entire service catalog is returned. - * - * The service catalog contains information about what services (if any) are - * available for the present user. Object storage (Swift) Compute instances - * (Nova) and other services will each be listed here if they are enabled - * for your user in the current tenant. Only services that have been turned on - * for the user on the tenant will be available. (That is, even if you *can* - * create a compute instance, until you have actually created one, it will not - * show up in this list.) - * - * One of the authentication methods MUST be run before obtaining the service - * catalog. - * - * The return value is an indexed array of associative arrays, where each assoc - * array describes an individual service. - * - * 'object-store', - * 'endpoints' => array( - * 'tenantId' => '123456', - * 'adminURL' => 'https://example.hpcloud.net/1.0', - * 'publicURL' => 'https://example.hpcloud.net/1.0/123456', - * 'region' => 'region-a.geo-1', - * 'id' => '1.0', - * ), - * ), - * array( - * 'name' => 'Identity', - * 'type' => 'identity' - * 'endpoints' => array( - * 'publicURL' => 'https://example.hpcloud.net/1.0/123456', - * 'region' => 'region-a.geo-1', - * 'id' => '2.0', - * 'list' => 'http://example.hpcloud.net/extension', - * ), - * ) - * - * ); - * ?> - * - * This will not be populated until after authentication has been done. - * - * Types: - * - * While this is by no means an exhaustive list, here are a few types that - * might appear in a service catalog (and upon which you can filter): - * - * - identity: Identity Services (i.e. Keystone) - * - compute: Compute instance (Nova) - * - object-store: Object Storage (Swift) - * - * Other services will be added. - * - * @todo Paging on the service catalog is not yet implemented. - * - * @return array An associative array representing the service catalog. - */ - public function serviceCatalog($type = NULL) { - // If no type is specified, return the entire - // catalog. - if (empty($type)) { - return $this->serviceCatalog; - } - - $list = array(); - foreach ($this->serviceCatalog as $entry) { - if ($entry['type'] == $type) { - $list[] = $entry; - } - } - - return $list; - } - - /** - * Get information about the currently authenticated user. - * - * This returns an associative array of information about the authenticated - * user, including the user's username and roles. - * - * The returned data is structured like this: - * - * 'matthew.butcher@hp.com', - * 'id' => '1234567890' - * 'roles' => array( - * array( - * 'name' => 'domainuser', - * 'serviceId' => '100', - * 'id' => '000100400010011', - * ), - * // One array for each role... - * ), - * ) - * ?> - * - * This will not have data until after authentication has been done. - * - * @return array An associative array, as described above. - */ - public function user() { - return $this->userDetails; - } - - /** - * Get a list of all tenants associated with this account. - * - * If a valid token is passed into this object, the method can be invoked - * before authentication. However, if no token is supplied, this attempts - * to use the one returned by an authentication call. - * - * Returned data will follow this format: - * - * "395I91234514446", - * "name" => "Banking Tenant Services", - * "description" => "Banking Tenant Services for TimeWarner", - * "enabled" => TRUE, - * "created" => "2011-11-29T16:59:52.635Z", - * "updated" => "2011-11-29T16:59:52.635Z", - * ), - * ); - * ?> - * - * Note that this method invokes a new request against the remote server. - * - * @return array An indexed array of tenant info. Each entry will be an - * associative array containing tenant details. - * @throws \OpenStack\Transport\AuthorizationException If authentication failed. - * @throws \OpenStack\Exception For abnormal network conditions. The message - * will give an indication as to the underlying problem. - */ - public function tenants($token = NULL) { - $url = $this->url() . '/tenants'; - - if (empty($token)) { - $token = $this->token(); - } - - $headers = array( - 'X-Auth-Token' => $token, - 'Accept' => 'application/json', - //'Content-Type' => 'application/json', - ); - - $response = $this->client->doRequest($url, 'GET', $headers); - - $json = $response->json(); - - return $json['tenants']; - - } - - /** - * @see \OpenStack\Services\IdentityService::rescopeUsingTenantId() - * @deprecated - */ - public function rescope($tenantId) { - return $this->rescopeUsingTenantId($tenantId); - } +class IdentityService +{ + /** + * The version of the API currently supported. + */ + const API_VERSION = '2.0'; /** - * Rescope the authentication token to a different tenant. - * - * Note that this will rebuild the service catalog and user information for - * the current object, since this information is sensitive to tenant info. - * - * An authentication token can be in one of two states: - * - * - unscoped: It has no associated tenant ID. - * - scoped: It has a tenant ID, and can thus access that tenant's services. - * - * This method allows you to do any of the following: - * - * - Begin with an unscoped token, and assign it a tenant ID. - * - Change a token from one tenant ID to another (re-scoping). - * - Remove the tenant ID from a scoped token (unscoping). - * - * @param string $tenantId The tenant ID that this present token should be - * bound to. If this is the empty string (`''`), the present token will be - * "unscoped" and its tenant ID will be removed. - * - * @return string The authentication token. - * @throws \OpenStack\Transport\AuthorizationException If authentication - * failed. - * @throws \OpenStack\Exception For abnormal network conditions. The message - * will give an indication as to the underlying problem. - */ - public function rescopeUsingTenantId($tenantId) { - $url = $this->url() . '/tokens'; - $token = $this->token(); - $data = array( - 'auth' => array( - 'tenantId' => $tenantId, - 'token' => array( - 'id' => $token, - ), - ), - ); - $body = json_encode($data); + * The full OpenStack accept type. + */ + const ACCEPT_TYPE = 'application/json'; - $headers = array( - 'Accept' => self::ACCEPT_TYPE, - 'Content-Type' => 'application/json', - 'Content-Length' => strlen($body), - //'X-Auth-Token' => $token, - ); + // This is no longer supported. + //const ACCEPT_TYPE = 'application/vnd.openstack.identity+json;version=2.0'; - $response = $this->client->doRequest($url, 'POST', $headers, $body); - $this->handleResponse($response); + /** + * The URL to the CS endpoint. + */ + protected $endpoint; - return $this->token(); - } + /** + * The details sent with the token. + * + * The exact details of this array will differ depending on what type of + * authentication is used. For example, authenticating by username and + * password will set tenant information. Authenticating by username and + * password, however, will leave the tenant section empty. + * + * This is an associative array looking like this: + * + * 'auth_123abc321defef99', + * // Only non-empty for username/password auth. + * 'tenant' => array( + * 'id' => '123456', + * 'name' => 'matt.butcher@hp.com', + * ), + * 'expires' => '2012-01-24T12:46:01.682Z' + * ); + */ + protected $tokenDetails; - /** - * Rescope the authentication token to a different tenant. - * - * Note that this will rebuild the service catalog and user information for - * the current object, since this information is sensitive to tenant info. - * - * An authentication token can be in one of two states: - * - * - unscoped: It has no associated tenant ID. - * - scoped: It has a tenant ID, and can thus access that tenant's services. - * - * This method allows you to do any of the following: - * - * - Begin with an unscoped token, and assign it a tenant ID. - * - Change a token from one tenant ID to another (re-scoping). - * - Remove the tenant ID from a scoped token (unscoping). - * - * @param string $tenantName The tenant name that this present token should be - * bound to. If this is the empty string (`''`), the present token will be - * "unscoped" and its tenant name will be removed. - * - * @return string The authentication token. - * @throws \OpenStack\Transport\AuthorizationException If authentication failed. - * @throws \OpenStack\Exception For abnormal network conditions. The message - * will give an indication as to the underlying problem. - */ - public function rescopeUsingTenantName($tenantName) { - $url = $this->url() . '/tokens'; - $token = $this->token(); - $data = array( - 'auth' => array( - 'tenantName' => $tenantName, - 'token' => array( - 'id' => $token, - ), - ), - ); - $body = json_encode($data); + /** + * The service catalog. + */ + protected $catalog = array(); - $headers = array( - 'Accept' => self::ACCEPT_TYPE, - 'Content-Type' => 'application/json', - 'Content-Length' => strlen($body), - //'X-Auth-Token' => $token, - ); + protected $userDetails; - $response = $this->client->doRequest($url, 'POST', $headers, $body); - $this->handleResponse($response); + /** + * The HTTP Client + */ + protected $client; - return $this->token(); - } + /** + * Build a new IdentityService object. + * + * Each object is bound to a particular identity services endpoint. + * + * For the URL, you are advised to use the version without a + * version number at the end, e.g. http://cs.example.com/ rather + * than http://cs.example.com/v2.0. The version number must be + * controlled by the library. + * + * If a version is included in the URI, the library will attempt to use + * that URI. + * + * authenticateAsUser($username, $password); + * ?> + * + * @param string $url An URL pointing to the Identity Services endpoint. + * Note that you do not need the version identifier in the URL, as version + * information is sent in the HTTP headers rather than in the URL. The URL + * should always be to an SSL/TLS encrypted endpoint. + * + * @param \OpenStack\Transport\ClientInterface $client An optional HTTP client to use when making the requests. + */ + public function __construct($url, \OpenStack\Transport\ClientInterface $client = NULL) + { + $parts = parse_url($url); - /** - * Given a response object, populate this object. - * - * This parses the JSON data and parcels out the data to the appropriate - * fields. - * - * @param \OpenStack\Transport\ResponseInterface $response A response object. - * - * @return \OpenStack\Services\IdentityService $this for the current object so - * it can be used in chaining. - */ - protected function handleResponse($response) { - $json = $response->json(); + if (!empty($parts['path'])) { + $this->endpoint = rtrim($url, '/'); + } else { + $this->endpoint = rtrim($url, '/') . '/v' . self::API_VERSION; + } - $this->tokenDetails = $json['access']['token']; - $this->userDetails = $json['access']['user']; - $this->serviceCatalog = $json['access']['serviceCatalog']; + // Guzzle is the default client to use. + if (is_null($client)) { + $this->client = new GuzzleClient(); + } else { + $this->client = $client; + } + } - return $this; - } + /** + * Get the endpoint URL. + * + * This includes version number, so in that regard it is not an identical + * URL to the one passed into the constructor. + * + * @return string The complete URL to the identity services endpoint. + */ + public function url() + { + return $this->endpoint; + } + + /** + * Send an authentication request. + * + * EXPERT: This allows authentication requests at a low level. For simple + * authentication requests using a username, see the + * authenticateAsUser() method. + * + * Here is an example of username/password-based authentication done with + * the authenticate() method: + * + * array( + * 'username' => $username, + * 'password' => $password, + * ), + * 'tenantId' => $tenantId, + * ); + * $token = $cs->authenticate($ops); + * ?> + * + * Note that the same authentication can be done by authenticateAsUser(). + * + * @param array $ops An associative array of authentication operations and + * their respective parameters. + * + * @return string The token. This is returned for simplicity. The full + * response is used to populate this object's service catalog, etc. The + * token is also retrievable with token(). + * + * @throws \OpenStack\Transport\AuthorizationException If authentication failed. + * @throws \OpenStack\Exception For abnormal network conditions. The message + * will give an indication as to the underlying problem. + */ + public function authenticate(array $ops) + { + $url = $this->url() . '/tokens'; + $envelope = array( + 'auth' => $ops, + ); + + $body = json_encode($envelope); + + $headers = array( + 'Content-Type' => 'application/json', + 'Accept' => self::ACCEPT_TYPE, + 'Content-Length' => strlen($body), + ); + + $response = $this->client->doRequest($url, 'POST', $headers, $body); + + $this->handleResponse($response); + + return $this->token(); + } + + /** + * Authenticate to Identity Services with username, password, and either + * tenant ID or tenant Name. + * + * Given a OpenStack username and password, authenticate to Identity Services. + * Identity Services will then issue a token that can be used to access other + * OpenStack services. + * + * If a tenant ID is provided, this will also associate the user with the + * given tenant ID. If a tenant Name is provided, this will associate the user + * with the given tenant Name. Only the tenant ID or tenant Name needs to be + * given, not both. + * + * If no tenant ID or tenant Name is given, it will likely be necessary to + * rescope() the request (See also tenants()). + * + * Other authentication methods: + * - authenticate() + * + * @param string $username A valid username. + * @param string $password A password string. + * @param string $tenantId The tenant ID. This can be obtained through the + * OpenStack console. + * @param string $tenantName The tenant Name. This can be obtained through the + * OpenStack console. + * + * @throws \OpenStack\Transport\AuthorizationException If authentication failed. + * @throws \OpenStack\Exception For abnormal network conditions. The message + * will give an indication as to the underlying problem. + */ + public function authenticateAsUser($username, $password, $tenantId = NULL, $tenantName = NULL) + { + $ops = array( + 'passwordCredentials' => array( + 'username' => $username, + 'password' => $password, + ), + ); + + // If a tenant ID is provided, added it to the auth array. + if (!empty($tenantId)) { + $ops['tenantId'] = $tenantId; + } elseif (!empty($tenantName)) { + $ops['tenantName'] = $tenantName; + } + + return $this->authenticate($ops); + } + + /** + * Get the token. + * + * This will not be populated until after one of the authentication + * methods has been run. + * + * @return string The token ID to be used in subsequent calls. + */ + public function token() + { + return $this->tokenDetails['id']; + } + + /** + * Get the tenant ID associated with this token. + * + * If this token has a tenant ID, the ID will be returned. Otherwise, this + * will return NULL. + * + * This will not be populated until after an authentication method has been + * run. + * + * @return string The tenant ID if available, or NULL. + */ + public function tenantId() + { + if (!empty($this->tokenDetails['tenant']['id'])) { + return $this->tokenDetails['tenant']['id']; + } + } + + /** + * Get the tenant name associated with this token. + * + * If this token has a tenant name, the name will be returned. Otherwise, this + * will return NULL. + * + * This will not be populated until after an authentication method has been + * run. + * + * @return string The tenant name if available, or NULL. + */ + public function tenantName() + { + if (!empty($this->tokenDetails['tenant']['name'])) { + return $this->tokenDetails['tenant']['name']; + } + } + + /** + * Get the token details. + * + * This returns an associative array with several pieces of information + * about the token, including: + * + * - id: The token itself + * - expires: When the token expires + * - tenant_id: The tenant ID of the authenticated user. + * - tenant_name: The username of the authenticated user. + * + * 'auth_123abc321defef99', + * 'tenant' => array( + * 'id' => '123456', + * 'name' => 'matt.butcher@hp.com', + * ), + * 'expires' => '2012-01-24T12:46:01.682Z' + * ); + * + * This will not be populated until after authentication has been done. + * + * @return array An associative array of details. + */ + public function tokenDetails() + { + return $this->tokenDetails; + } + + /** + * Check whether the current identity has an expired token. + * + * This does not perform a round-trip to the server. Instead, it compares the + * machine's local timestamp with the server's expiration time stamp. A + * mis-configured machine timestamp could give spurious results. + * + * @return boolean This will return FALSE if there is a current token and it + * has not yet expired (according to the date info). In all + * other cases it returns TRUE. + */ + public function isExpired() + { + $details = $this->tokenDetails(); + + if (empty($details['expires'])) { + return TRUE; + } + + $currentDateTime = new \DateTime('now'); + $expireDateTime = new \DateTime($details['expires']); + + return $currentDateTime > $expireDateTime; + } + + /** + * Get the service catalog, optionaly filtering by type. + * + * This returns the service catalog (largely unprocessed) that + * is returned during an authentication request. If a type is passed in, + * only entries of that type are returned. If no type is passed in, the + * entire service catalog is returned. + * + * The service catalog contains information about what services (if any) are + * available for the present user. Object storage (Swift) Compute instances + * (Nova) and other services will each be listed here if they are enabled + * for your user in the current tenant. Only services that have been turned on + * for the user on the tenant will be available. (That is, even if you *can* + * create a compute instance, until you have actually created one, it will not + * show up in this list.) + * + * One of the authentication methods MUST be run before obtaining the service + * catalog. + * + * The return value is an indexed array of associative arrays, where each assoc + * array describes an individual service. + * + * 'object-store', + * 'endpoints' => array( + * 'tenantId' => '123456', + * 'adminURL' => 'https://example.hpcloud.net/1.0', + * 'publicURL' => 'https://example.hpcloud.net/1.0/123456', + * 'region' => 'region-a.geo-1', + * 'id' => '1.0', + * ), + * ), + * array( + * 'name' => 'Identity', + * 'type' => 'identity' + * 'endpoints' => array( + * 'publicURL' => 'https://example.hpcloud.net/1.0/123456', + * 'region' => 'region-a.geo-1', + * 'id' => '2.0', + * 'list' => 'http://example.hpcloud.net/extension', + * ), + * ) + * + * ); + * ?> + * + * This will not be populated until after authentication has been done. + * + * Types: + * + * While this is by no means an exhaustive list, here are a few types that + * might appear in a service catalog (and upon which you can filter): + * + * - identity: Identity Services (i.e. Keystone) + * - compute: Compute instance (Nova) + * - object-store: Object Storage (Swift) + * + * Other services will be added. + * + * @todo Paging on the service catalog is not yet implemented. + * + * @return array An associative array representing the service catalog. + */ + public function serviceCatalog($type = NULL) + { + // If no type is specified, return the entire + // catalog. + if (empty($type)) { + return $this->serviceCatalog; + } + + $list = array(); + foreach ($this->serviceCatalog as $entry) { + if ($entry['type'] == $type) { + $list[] = $entry; + } + } + + return $list; + } + + /** + * Get information about the currently authenticated user. + * + * This returns an associative array of information about the authenticated + * user, including the user's username and roles. + * + * The returned data is structured like this: + * + * 'matthew.butcher@hp.com', + * 'id' => '1234567890' + * 'roles' => array( + * array( + * 'name' => 'domainuser', + * 'serviceId' => '100', + * 'id' => '000100400010011', + * ), + * // One array for each role... + * ), + * ) + * ?> + * + * This will not have data until after authentication has been done. + * + * @return array An associative array, as described above. + */ + public function user() + { + return $this->userDetails; + } + + /** + * Get a list of all tenants associated with this account. + * + * If a valid token is passed into this object, the method can be invoked + * before authentication. However, if no token is supplied, this attempts + * to use the one returned by an authentication call. + * + * Returned data will follow this format: + * + * "395I91234514446", + * "name" => "Banking Tenant Services", + * "description" => "Banking Tenant Services for TimeWarner", + * "enabled" => TRUE, + * "created" => "2011-11-29T16:59:52.635Z", + * "updated" => "2011-11-29T16:59:52.635Z", + * ), + * ); + * ?> + * + * Note that this method invokes a new request against the remote server. + * + * @return array An indexed array of tenant info. Each entry will be an + * associative array containing tenant details. + * + * @throws \OpenStack\Transport\AuthorizationException If authentication failed. + * @throws \OpenStack\Exception For abnormal network conditions. The message + * will give an indication as to the underlying problem. + */ + public function tenants($token = NULL) + { + $url = $this->url() . '/tenants'; + + if (empty($token)) { + $token = $this->token(); + } + + $headers = array( + 'X-Auth-Token' => $token, + 'Accept' => 'application/json', + //'Content-Type' => 'application/json', + ); + + $response = $this->client->doRequest($url, 'GET', $headers); + + $json = $response->json(); + + return $json['tenants']; + + } + + /** + * @see \OpenStack\Services\IdentityService::rescopeUsingTenantId() + * @deprecated + */ + public function rescope($tenantId) + { + return $this->rescopeUsingTenantId($tenantId); + } + + /** + * Rescope the authentication token to a different tenant. + * + * Note that this will rebuild the service catalog and user information for + * the current object, since this information is sensitive to tenant info. + * + * An authentication token can be in one of two states: + * + * - unscoped: It has no associated tenant ID. + * - scoped: It has a tenant ID, and can thus access that tenant's services. + * + * This method allows you to do any of the following: + * + * - Begin with an unscoped token, and assign it a tenant ID. + * - Change a token from one tenant ID to another (re-scoping). + * - Remove the tenant ID from a scoped token (unscoping). + * + * @param string $tenantId The tenant ID that this present token should be + * bound to. If this is the empty string (`''`), the + * present token will be "unscoped" and its tenant + * ID will be removed. + * + * @return string The authentication token. + * + * @throws \OpenStack\Transport\AuthorizationException If authentication failed. + * @throws \OpenStack\Exception For abnormal network conditions. The message will give an + * indication as to the underlying problem. + */ + public function rescopeUsingTenantId($tenantId) + { + $url = $this->url() . '/tokens'; + $token = $this->token(); + $data = array( + 'auth' => array( + 'tenantId' => $tenantId, + 'token' => array( + 'id' => $token, + ), + ), + ); + $body = json_encode($data); + + $headers = array( + 'Accept' => self::ACCEPT_TYPE, + 'Content-Type' => 'application/json', + 'Content-Length' => strlen($body), + //'X-Auth-Token' => $token, + ); + + $response = $this->client->doRequest($url, 'POST', $headers, $body); + $this->handleResponse($response); + + return $this->token(); + } + + /** + * Rescope the authentication token to a different tenant. + * + * Note that this will rebuild the service catalog and user information for + * the current object, since this information is sensitive to tenant info. + * + * An authentication token can be in one of two states: + * + * - unscoped: It has no associated tenant ID. + * - scoped: It has a tenant ID, and can thus access that tenant's services. + * + * This method allows you to do any of the following: + * + * - Begin with an unscoped token, and assign it a tenant ID. + * - Change a token from one tenant ID to another (re-scoping). + * - Remove the tenant ID from a scoped token (unscoping). + * + * @param string $tenantName The tenant name that this present token should be + * bound to. If this is the empty string (`''`), the + * present token will be "unscoped" and its tenant + * name will be removed. + * + * @return string The authentication token. + * + * @throws \OpenStack\Transport\AuthorizationException If authentication failed. + * @throws \OpenStack\Exception For abnormal network conditions. The message will + * give an indication as to the underlying problem. + */ + public function rescopeUsingTenantName($tenantName) + { + $url = $this->url() . '/tokens'; + $token = $this->token(); + $data = array( + 'auth' => array( + 'tenantName' => $tenantName, + 'token' => array( + 'id' => $token, + ), + ), + ); + $body = json_encode($data); + + $headers = array( + 'Accept' => self::ACCEPT_TYPE, + 'Content-Type' => 'application/json', + 'Content-Length' => strlen($body), + //'X-Auth-Token' => $token, + ); + + $response = $this->client->doRequest($url, 'POST', $headers, $body); + $this->handleResponse($response); + + return $this->token(); + } + + /** + * Given a response object, populate this object. + * + * This parses the JSON data and parcels out the data to the appropriate + * fields. + * + * @param \OpenStack\Transport\ResponseInterface $response A response object. + * + * @return \OpenStack\Services\IdentityService $this for the current object so + * it can be used in chaining. + */ + protected function handleResponse($response) + { + $json = $response->json(); + + $this->tokenDetails = $json['access']['token']; + $this->userDetails = $json['access']['user']; + $this->serviceCatalog = $json['access']['serviceCatalog']; + + return $this; + } } diff --git a/src/OpenStack/Storage/ObjectStorage.php b/src/OpenStack/Storage/ObjectStorage.php index 3186088..3931455 100644 --- a/src/OpenStack/Storage/ObjectStorage.php +++ b/src/OpenStack/Storage/ObjectStorage.php @@ -2,17 +2,17 @@ /* ============================================================================ (c) Copyright 2012-2014 Hewlett-Packard Development Company, L.P. - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at - http://www.apache.org/licenses/LICENSE-2.0 + http://www.apache.org/licenses/LICENSE-2.0 - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. ============================================================================ */ /** * This file provides the ObjectStorage class, which is the primary @@ -60,513 +60,529 @@ use OpenStack\Transport\GuzzleClient; * of the API. It attempts to use whatever version is passed in to the * URL. This is different than IdentityServices, which used a fixed version. */ -class ObjectStorage { +class ObjectStorage +{ + /** + * The name of this service type in OpenStack. + * + * This is used with IdentityService::serviceCatalog(). + */ + const SERVICE_TYPE = 'object-store'; - /** - * The name of this service type in OpenStack. - * - * This is used with IdentityService::serviceCatalog(). - */ - const SERVICE_TYPE = 'object-store'; + const API_VERSION = '1'; - const API_VERSION = '1'; + const DEFAULT_REGION = 'region-a.geo-1'; - const DEFAULT_REGION = 'region-a.geo-1'; + /** + * The authorization token. + */ + protected $token = NULL; + /** + * The URL to the Swift endpoint. + */ + protected $url = NULL; - /** - * The authorization token. - */ - protected $token = NULL; - /** - * The URL to the Swift endpoint. - */ - protected $url = NULL; + /** + * The HTTP Client + */ + protected $client; - /** - * The HTTP Client - */ - protected $client; + /** + * Create a new instance after getting an authenitcation token. + * + * THIS METHOD IS DEPRECATED. OpenStack now uses Keyston to authenticate. + * You should use \OpenStack\Services\IdentityServices to authenticate. + * Then use this class's constructor to create an object. + * + * This uses the legacy Swift authentication facility to authenticate + * to swift, get a new token, and then create a new ObjectStorage + * instance with that token. + * + * To use the legacy Object Storage authentication mechanism, you will + * need the follwing pieces of information: + * + * - Account ID: This will typically be a combination of your tenantId and + * username. + * - Key: Typically this will be your password. + * - Endpoint URL: The URL given to you by your service provider. + * + * @param string $account Your account name. + * @param string $key Your secret key. + * @param string $url The URL to the object storage endpoint. + * + * @throws \OpenStack\Transport\AuthorizationException if the + * authentication failed. + * @throws \OpenStack\Transport\FileNotFoundException if the URL is + * wrong. + * @throws \OpenStack\Exception if some other exception occurs. + * + * @deprecated Newer versions of OpenStack use Keystone auth instead + * of Swift auth. + */ + public static function newFromSwiftAuth($account, $key, $url, \OpenStack\Transport\ClientInterface $client = NULL) + { + $headers = array( + 'X-Auth-User' => $account, + 'X-Auth-Key' => $key, + ); - /** - * Create a new instance after getting an authenitcation token. - * - * THIS METHOD IS DEPRECATED. OpenStack now uses Keyston to authenticate. - * You should use \OpenStack\Services\IdentityServices to authenticate. - * Then use this class's constructor to create an object. - * - * This uses the legacy Swift authentication facility to authenticate - * to swift, get a new token, and then create a new ObjectStorage - * instance with that token. - * - * To use the legacy Object Storage authentication mechanism, you will - * need the follwing pieces of information: - * - * - Account ID: This will typically be a combination of your tenantId and - * username. - * - Key: Typically this will be your password. - * - Endpoint URL: The URL given to you by your service provider. - * - * @param string $account Your account name. - * @param string $key Your secret key. - * @param string $url The URL to the object storage endpoint. - * - * @throws \OpenStack\Transport\AuthorizationException if the - * authentication failed. - * @throws \OpenStack\Transport\FileNotFoundException if the URL is - * wrong. - * @throws \OpenStack\Exception if some other exception occurs. - * - * @deprecated Newer versions of OpenStack use Keystone auth instead - * of Swift auth. - */ - public static function newFromSwiftAuth($account, $key, $url, \OpenStack\Transport\ClientInterface $client = NULL) { - $headers = array( - 'X-Auth-User' => $account, - 'X-Auth-Key' => $key, - ); - - // Guzzle is the default client to use. - if (is_null($client)) { - $client = new GuzzleClient(); - } - - // This will throw an exception if it cannot connect or - // authenticate. - $res = $client->doRequest($url, 'GET', $headers); - - - // Headers that come back: - // X-Storage-Url: https://region-a.geo-1.objects.hpcloudsvc.com:443/v1/AUTH_d8e28d35-3324-44d7-a625-4e6450dc1683 - // X-Storage-Token: AUTH_tkd2ffb4dac4534c43afbe532ca41bcdba - // X-Auth-Token: AUTH_tkd2ffb4dac4534c43afbe532ca41bcdba - // X-Trans-Id: tx33f1257e09f64bc58f28e66e0577268a - - - $token = $res->getHeader('X-Auth-Token'); - $newUrl = $res->getHeader('X-Storage-Url'); - - $store = new ObjectStorage($token, $newUrl, $client); - - return $store; - } - - /** - * Given an IdentityServices instance, create an ObjectStorage instance. - * - * This constructs a new ObjectStorage from an authenticated instance - * of an \OpenStack\Services\IdentityServices object. - * - * @param \OpenStack\Services\IdentityServices $identity An identity services - * object that already has a valid token and a service catalog. - * - * @return \OpenStack\Storage\ObjectStorage A new ObjectStorage instance. - */ - public static function newFromIdentity($identity, $region = ObjectStorage::DEFAULT_REGION, \OpenStack\Transport\ClientInterface $client = NULL) { - $cat = $identity->serviceCatalog(); - $tok = $identity->token(); - return self::newFromServiceCatalog($cat, $tok, $region, $client); - } - - /** - * Given a service catalog and an token, create an ObjectStorage instance. - * - * The IdentityServices object contains a service catalog listing all of the - * services to which the present user has access. - * - * This builder can scan the catalog and generate a new ObjectStorage - * instance pointed to the first object storage endpoint in the catalog. - * - * @param array $catalog The serice catalog from IdentityServices::serviceCatalog(). - * This can be either the entire catalog or a catalog filtered to just - * ObjectStorage::SERVICE_TYPE. - * @param string $authToken The auth token returned by IdentityServices. - * - * @return \OpenStack\Storage\ObjectStorage A new ObjectStorage instance. - */ - public static function newFromServiceCatalog($catalog, $authToken, $region = ObjectStorage::DEFAULT_REGION, \OpenStack\Transport\ClientInterface $client = NULL) { - $c = count($catalog); - for ($i = 0; $i < $c; ++$i) { - if ($catalog[$i]['type'] == self::SERVICE_TYPE) { - foreach ($catalog[$i]['endpoints'] as $endpoint) { - if (isset($endpoint['publicURL']) && $endpoint['region'] == $region) { - $os = new ObjectStorage($authToken, $endpoint['publicURL'], $client); - - return $os; - } + // Guzzle is the default client to use. + if (is_null($client)) { + $client = new GuzzleClient(); } - } - } - return FALSE; - } + // This will throw an exception if it cannot connect or + // authenticate. + $res = $client->doRequest($url, 'GET', $headers); - /** - * Construct a new ObjectStorage object. - * - * Use this if newFromServiceCatalog() does not meet your needs. - * - * @param string $authToken A token that will be included in subsequent - * requests to validate that this client has authenticated correctly. - * @param string $url The URL to the endpoint. This typically is returned - * after authentication. - */ - public function __construct($authToken, $url, \OpenStack\Transport\ClientInterface $client = NULL) { - $this->token = $authToken; - $this->url = $url; + // Headers that come back: + // X-Storage-Url: https://region-a.geo-1.objects.hpcloudsvc.com:443/v1/AUTH_d8e28d35-3324-44d7-a625-4e6450dc1683 + // X-Storage-Token: AUTH_tkd2ffb4dac4534c43afbe532ca41bcdba + // X-Auth-Token: AUTH_tkd2ffb4dac4534c43afbe532ca41bcdba + // X-Trans-Id: tx33f1257e09f64bc58f28e66e0577268a - // Guzzle is the default client to use. - if (is_null($client)) { - $this->client = new GuzzleClient(); - } - else { - $this->client = $client; - } - } + $token = $res->getHeader('X-Auth-Token'); + $newUrl = $res->getHeader('X-Storage-Url'); - /** - * Get the authentication token. - * - * @return string The authentication token. - */ - public function token() { - return $this->token; - } + $store = new ObjectStorage($token, $newUrl, $client); - /** - * Get the URL endpoint. - * - * @return string The URL that is the endpoint for this service. - */ - public function url() { - return $this->url; - } - - /** - * Fetch a list of containers for this user. - * - * By default, this fetches the entire list of containers for the - * given user. If you have more than 10,000 containers (who - * wouldn't?), you will need to use $marker for paging. - * - * If you want more controlled paging, you can use $limit to indicate - * the number of containers returned per page, and $marker to indicate - * the last container retrieved. - * - * Containers are ordered. That is, they will always come back in the - * same order. For that reason, the pager takes $marker (the name of - * the last container) as a paging parameter, rather than an offset - * number. - * - * @todo For some reason, ACL information does not seem to be returned - * in the JSON data. Need to determine how to get that. As a - * stop-gap, when a container object returned from here has its ACL - * requested, it makes an additional round-trip to the server to - * fetch that data. - * - * @param int $limit The maximum number to return at a time. The default is - * -- brace yourself -- 10,000 (as determined by OpenStack. Implementations - * may vary). - * @param string $marker The name of the last object seen. Used when paging. - * - * @return array An associative array of containers, where the key is the - * container's name and the value is an \OpenStack\Storage\ObjectStorage\Container - * object. Results are ordered in server order (the order that the remote - * host puts them in). - */ - public function containers($limit = 0, $marker = NULL) { - - $url = $this->url() . '?format=json'; - - if ($limit > 0) { - $url .= sprintf('&limit=%d', $limit); - } - if (!empty($marker)) { - $url .= sprintf('&marker=%d', $marker); + return $store; } - $containers = $this->get($url); + /** + * Given an IdentityServices instance, create an ObjectStorage instance. + * + * This constructs a new ObjectStorage from an authenticated instance + * of an \OpenStack\Services\IdentityServices object. + * + * @param \OpenStack\Services\IdentityServices $identity An identity services object that already has a valid token + * and a service catalog. + * + * @return \OpenStack\Storage\ObjectStorage A new ObjectStorage instance. + */ + public static function newFromIdentity($identity, $region = ObjectStorage::DEFAULT_REGION, \OpenStack\Transport\ClientInterface $client = NULL) + { + $cat = $identity->serviceCatalog(); + $tok = $identity->token(); - $containerList = array(); - foreach ($containers as $container) { - $cname = $container['name']; - $containerList[$cname] = Container::newFromJSON($container, $this->token(), $this->url(), $this->client); + return self::newFromServiceCatalog($cat, $tok, $region, $client); } - return $containerList; - } + /** + * Given a service catalog and an token, create an ObjectStorage instance. + * + * The IdentityServices object contains a service catalog listing all of the + * services to which the present user has access. + * + * This builder can scan the catalog and generate a new ObjectStorage + * instance pointed to the first object storage endpoint in the catalog. + * + * @param array $catalog The serice catalog from IdentityServices::serviceCatalog(). + * This can be either the entire catalog or a catalog + * filtered to just ObjectStorage::SERVICE_TYPE. + * @param string $authToken The auth token returned by IdentityServices. + * + * @return \OpenStack\Storage\ObjectStorage A new ObjectStorage instance. + */ + public static function newFromServiceCatalog($catalog, $authToken, $region = ObjectStorage::DEFAULT_REGION, \OpenStack\Transport\ClientInterface $client = NULL) + { + $c = count($catalog); + for ($i = 0; $i < $c; ++$i) { + if ($catalog[$i]['type'] == self::SERVICE_TYPE) { + foreach ($catalog[$i]['endpoints'] as $endpoint) { + if (isset($endpoint['publicURL']) && $endpoint['region'] == $region) { + $os = new ObjectStorage($authToken, $endpoint['publicURL'], $client); - /** - * Get a single specific container. - * - * This loads only the named container from the remote server. - * - * @param string $name The name of the container to load. - * - * @return \OpenStack\Storage\ObjectStorage\Container A container. - * @throws \OpenStack\Transport\FileNotFoundException if the named container - * is not found on the remote server. - */ - public function container($name) { + return $os; + } + } + } + } - $url = $this->url() . '/' . rawurlencode($name); - $data = $this->req($url, 'HEAD', FALSE); + return FALSE; - $status = $data->getStatusCode(); - if ($status == 204) { - $container = Container::newFromResponse($name, $data, $this->token(), $this->url()); - - return $container; } - // If we get here, it's not a 404 and it's not a 204. - throw new \OpenStack\Exception("Unknown status: $status"); - } + /** + * Construct a new ObjectStorage object. + * + * Use this if newFromServiceCatalog() does not meet your needs. + * + * @param string $authToken A token that will be included in subsequent + * requests to validate that this client has authenticated + * correctly. + * @param string $url The URL to the endpoint. This typically is returned + * after authentication. + */ + public function __construct($authToken, $url, \OpenStack\Transport\ClientInterface $client = NULL) + { + $this->token = $authToken; + $this->url = $url; - /** - * Check to see if this container name exists. - * - * This method directly checks the remote server. Calling container() - * or containers() might be more efficient if you plan to work with - * the resulting container. - * - * @param string $name The name of the container to test. - * - * @return boolean TRUE if the container exists, FALSE if it does not. - * @throws \OpenStack\Exception If an unexpected network error occurs. - */ - public function hasContainer($name) { - try { - $container = $this->container($name); - } - catch (\OpenStack\Transport\FileNotFoundException $fnfe) { - return FALSE; - } - return TRUE; - } - - /** - * Create a container with the given name. - * - * This creates a new container on the ObjectStorage - * server with the name provided in $name. - * - * A boolean is returned when the operation did not generate an error - * condition. - * - * - TRUE means that the container was created. - * - FALSE means that the container was not created because it already - * exists. - * - * Any actual error will cause an exception to be thrown. These will - * be the HTTP-level exceptions. - * - * ACLs - * - * Swift supports an ACL stream that allows for specifying (with - * certain caveats) various levels of read and write access. However, - * there are two standard settings that cover the vast majority of - * cases. - * - * - Make the resource private: This grants read and write access to - * ONLY the creating user tenant. This is the default; it can also be - * specified with ACL::makeNonPublic(). - * - Make the resource public: This grants READ permission to any - * requesting host, yet only allows the creator to WRITE to the - * object. This level can be granted by ACL::makePublic(). - * - * Note that ACLs operate at a container level. Thus, marking a - * container public will allow access to ALL objects inside of the - * container. - * - * To find out whether an existing container is public, you can - * write something like this: - * - * container('my_container'); - * - * //Check the permission on the ACL: - * $boolean = $container->acl()->isPublic(); - * ?> - * - * For details on ACLs, see \OpenStack\Storage\ObjectStorage\ACL. - * - * @param string $name The name of the container. - * @param object $acl \OpenStack\Storage\ObjectStorage\ACL An access control - * list object. By default, a container is non-public (private). To change - * this behavior, you can add a custom ACL. To make the container publically - * readable, you can use this: \OpenStack\Storage\ObjectStorage\ACL::makePublic(). - * @param array $metadata An associative array of metadata to attach to the - * container. - * - * @return boolean TRUE if the container was created, FALSE if the container - * was not created because it already exists. - */ - public function createContainer($name, ACL $acl = NULL, $metadata = array()) { - $url = $this->url() . '/' . rawurlencode($name); - $headers = array( - 'X-Auth-Token' => $this->token(), - ); - - if (!empty($metadata)) { - $prefix = Container::CONTAINER_METADATA_HEADER_PREFIX; - $headers += Container::generateMetadataHeaders($metadata, $prefix); + // Guzzle is the default client to use. + if (is_null($client)) { + $this->client = new GuzzleClient(); + } else { + $this->client = $client; + } } - // Add ACLs to header. - if (!empty($acl)) { - $headers += $acl->headers(); + /** + * Get the authentication token. + * + * @return string The authentication token. + */ + public function token() + { + return $this->token; } - $data = $this->client->doRequest($url, 'PUT', $headers); - //syslog(LOG_WARNING, print_r($data, TRUE)); - - $status = $data->getStatusCode(); - - if ($status == 201) { - return TRUE; - } elseif ($status == 202) { - return FALSE; - } - // According to the OpenStack docs, there are no other return codes. - else { - throw new \OpenStack\Exception('Server returned unexpected code: ' . $status); - } - } - - /** - * Alias of createContainer(). - * - * At present, there is no distinction in the Swift REST API between - * creating an updating a container. In the future this may change, so - * you are encouraged to use this alias in cases where you clearly intend - * to update an existing container. - */ - public function updateContainer($name, ACL $acl = NULL, $metadata = array()) { - return $this->createContainer($name, $acl, $metadata); - } - - /** - * Change the container's ACL. - * - * This will attempt to change the ACL on a container. If the - * container does not already exist, it will be created first, and - * then the ACL will be set. (This is a relic of the OpenStack Swift - * implementation, which uses the same HTTP verb to create a container - * and to set the ACL.) - * - * @param string $name The name of the container. - * @param object $acl \OpenStack\Storage\ObjectStorage\ACL An ACL. To make the - * container publically readable, use ACL::makePublic(). - * - * @return boolean TRUE if the cointainer was created, FALSE otherwise. - */ - public function changeContainerACL($name, ACL $acl) { - // Oddly, the way to change an ACL is to issue the - // same request as is used to create a container. - return $this->createContainer($name, $acl); - } - - /** - * Delete an empty container. - * - * Given a container name, this attempts to delete the container in - * the object storage. - * - * The container MUST be empty before it can be deleted. If it is not, - * an \OpenStack\Storage\ObjectStorage\ContainerNotEmptyException will - * be thrown. - * - * @param string $name The name of the container. - * - * @return boolean TRUE if the container was deleted, FALSE if the container - * was not found (and hence, was not deleted). - * @throws \OpenStack\Storage\ObjectStorage\ContainerNotEmptyException - * if the container is not empty. - * @throws \OpenStack\Exception if an unexpected response code is returned. - * While this should never happen on OpenStack servers, forks of - * OpenStack may choose to extend object storage in a way that - * results in a non-standard code. - */ - public function deleteContainer($name) { - $url = $this->url() . '/' . rawurlencode($name); - - try { - $data = $this->req($url, 'DELETE', FALSE); - } - catch (\OpenStack\Transport\FileNotFoundException $e) { - return FALSE; - } - // XXX: I'm not terribly sure about this. Why not just throw the - // ConflictException? - catch (\OpenStack\Transport\ConflictException $e) { - throw new ObjectStorage\ContainerNotEmptyException("Non-empty container cannot be deleted."); + /** + * Get the URL endpoint. + * + * @return string The URL that is the endpoint for this service. + */ + public function url() + { + return $this->url; } - $status = $data->getStatusCode(); + /** + * Fetch a list of containers for this user. + * + * By default, this fetches the entire list of containers for the + * given user. If you have more than 10,000 containers (who + * wouldn't?), you will need to use $marker for paging. + * + * If you want more controlled paging, you can use $limit to indicate + * the number of containers returned per page, and $marker to indicate + * the last container retrieved. + * + * Containers are ordered. That is, they will always come back in the + * same order. For that reason, the pager takes $marker (the name of + * the last container) as a paging parameter, rather than an offset + * number. + * + * @todo For some reason, ACL information does not seem to be returned + * in the JSON data. Need to determine how to get that. As a + * stop-gap, when a container object returned from here has its ACL + * requested, it makes an additional round-trip to the server to + * fetch that data. + * + * @param int $limit The maximum number to return at a time. The default is + * -- brace yourself -- 10,000 (as determined by OpenStack. Implementations + * may vary). + * @param string $marker The name of the last object seen. Used when paging. + * + * @return array An associative array of containers, where the key is the + * container's name and the value is an \OpenStack\Storage\ObjectStorage\Container + * object. Results are ordered in server order (the order that the remote + * host puts them in). + */ + public function containers($limit = 0, $marker = NULL) + { + $url = $this->url() . '?format=json'; - // 204 indicates that the container has been deleted. - if ($status == 204) { - return TRUE; + if ($limit > 0) { + $url .= sprintf('&limit=%d', $limit); + } + if (!empty($marker)) { + $url .= sprintf('&marker=%d', $marker); + } + + $containers = $this->get($url); + + $containerList = array(); + foreach ($containers as $container) { + $cname = $container['name']; + $containerList[$cname] = Container::newFromJSON($container, $this->token(), $this->url(), $this->client); + } + + return $containerList; } - // OpenStacks documentation doesn't suggest any other return - // codes. - else { - throw new \OpenStack\Exception('Server returned unexpected code: ' . $status); + + /** + * Get a single specific container. + * + * This loads only the named container from the remote server. + * + * @param string $name The name of the container to load. + * + * @return \OpenStack\Storage\ObjectStorage\Container A container. + * + * @throws \OpenStack\Transport\FileNotFoundException if the named container is not found on the remote server. + */ + public function container($name) + { + $url = $this->url() . '/' . rawurlencode($name); + $data = $this->req($url, 'HEAD', FALSE); + + $status = $data->getStatusCode(); + if ($status == 204) { + $container = Container::newFromResponse($name, $data, $this->token(), $this->url()); + + return $container; + } + + // If we get here, it's not a 404 and it's not a 204. + throw new \OpenStack\Exception("Unknown status: $status"); } - } - /** - * Retrieve account info. - * - * This returns information about: - * - * - The total bytes used by this Object Storage instance (`bytes`). - * - The number of containers (`count`). - * - * @return array An associative array of account info. Typical keys are: - * - bytes: Bytes consumed by existing content. - * - containers: Number of containers. - * - objects: Number of objects. - * @throws \OpenStack\Transport\AuthorizationException if the user credentials - * are invalid or have expired. - */ - public function accountInfo() { - $url = $this->url(); - $data = $this->req($url, 'HEAD', FALSE); + /** + * Check to see if this container name exists. + * + * This method directly checks the remote server. Calling container() + * or containers() might be more efficient if you plan to work with + * the resulting container. + * + * @param string $name The name of the container to test. + * + * @return boolean TRUE if the container exists, FALSE if it does not. + * + * @throws \OpenStack\Exception If an unexpected network error occurs. + */ + public function hasContainer($name) + { + try { + $container = $this->container($name); + } catch (\OpenStack\Transport\FileNotFoundException $fnfe) { + return FALSE; + } - $results = array( - 'bytes' => $data->getHeader('X-Account-Bytes-Used', 0), - 'containers' => $data->getHeader('X-Account-Container-Count', 0), - 'objects' => $data->getHeader('X-Account-Container-Count', 0), - ); - - return $results; - } - - /** - * Do a GET on Swift. - * - * This is a convenience method that handles the - * most common case of Swift requests. - */ - protected function get($url, $jsonDecode = TRUE) { - return $this->req($url, 'GET', $jsonDecode); - } - - /** - * Internal request issuing command. - */ - protected function req($url, $method = 'GET', $jsonDecode = TRUE, $body = '') { - $headers = array( - 'X-Auth-Token' => $this->token(), - ); - - $res = $this->client->doRequest($url, $method, $headers, $body); - if (!$jsonDecode) { - return $res; + return TRUE; } - return $res->json(); - } + /** + * Create a container with the given name. + * + * This creates a new container on the ObjectStorage + * server with the name provided in $name. + * + * A boolean is returned when the operation did not generate an error + * condition. + * + * - TRUE means that the container was created. + * - FALSE means that the container was not created because it already + * exists. + * + * Any actual error will cause an exception to be thrown. These will + * be the HTTP-level exceptions. + * + * ACLs + * + * Swift supports an ACL stream that allows for specifying (with + * certain caveats) various levels of read and write access. However, + * there are two standard settings that cover the vast majority of + * cases. + * + * - Make the resource private: This grants read and write access to + * ONLY the creating user tenant. This is the default; it can also be + * specified with ACL::makeNonPublic(). + * - Make the resource public: This grants READ permission to any + * requesting host, yet only allows the creator to WRITE to the + * object. This level can be granted by ACL::makePublic(). + * + * Note that ACLs operate at a container level. Thus, marking a + * container public will allow access to ALL objects inside of the + * container. + * + * To find out whether an existing container is public, you can + * write something like this: + * + * container('my_container'); + * + * //Check the permission on the ACL: + * $boolean = $container->acl()->isPublic(); + * ?> + * + * For details on ACLs, see \OpenStack\Storage\ObjectStorage\ACL. + * + * @param string $name The name of the container. + * @param object $acl \OpenStack\Storage\ObjectStorage\ACL An access control + * list object. By default, a container is non-public + * (private). To change this behavior, you can add a + * custom ACL. To make the container publically + * readable, you can use this: \OpenStack\Storage\ObjectStorage\ACL::makePublic(). + * @param array $metadata An associative array of metadata to attach to the + * container. + * + * @return boolean TRUE if the container was created, FALSE if the container + * was not created because it already exists. + */ + public function createContainer($name, ACL $acl = NULL, $metadata = array()) + { + $url = $this->url() . '/' . rawurlencode($name); + $headers = array( + 'X-Auth-Token' => $this->token(), + ); + + if (!empty($metadata)) { + $prefix = Container::CONTAINER_METADATA_HEADER_PREFIX; + $headers += Container::generateMetadataHeaders($metadata, $prefix); + } + + // Add ACLs to header. + if (!empty($acl)) { + $headers += $acl->headers(); + } + + $data = $this->client->doRequest($url, 'PUT', $headers); + //syslog(LOG_WARNING, print_r($data, TRUE)); + + $status = $data->getStatusCode(); + + if ($status == 201) { + return TRUE; + } elseif ($status == 202) { + return FALSE; + } + // According to the OpenStack docs, there are no other return codes. + else { + throw new \OpenStack\Exception('Server returned unexpected code: ' . $status); + } + } + + /** + * Alias of createContainer(). + * + * At present, there is no distinction in the Swift REST API between + * creating an updating a container. In the future this may change, so + * you are encouraged to use this alias in cases where you clearly intend + * to update an existing container. + */ + public function updateContainer($name, ACL $acl = NULL, $metadata = array()) + { + return $this->createContainer($name, $acl, $metadata); + } + + /** + * Change the container's ACL. + * + * This will attempt to change the ACL on a container. If the + * container does not already exist, it will be created first, and + * then the ACL will be set. (This is a relic of the OpenStack Swift + * implementation, which uses the same HTTP verb to create a container + * and to set the ACL.) + * + * @param string $name The name of the container. + * @param object $acl \OpenStack\Storage\ObjectStorage\ACL An ACL. To make the + * container publically readable, use ACL::makePublic(). + * + * @return boolean TRUE if the cointainer was created, FALSE otherwise. + */ + public function changeContainerACL($name, ACL $acl) + { + // Oddly, the way to change an ACL is to issue the + // same request as is used to create a container. + return $this->createContainer($name, $acl); + } + + /** + * Delete an empty container. + * + * Given a container name, this attempts to delete the container in + * the object storage. + * + * The container MUST be empty before it can be deleted. If it is not, + * an \OpenStack\Storage\ObjectStorage\ContainerNotEmptyException will + * be thrown. + * + * @param string $name The name of the container. + * + * @return boolean TRUE if the container was deleted, FALSE if the container + * was not found (and hence, was not deleted). + * + * @throws \OpenStack\Storage\ObjectStorage\ContainerNotEmptyException if the container is not empty. + * + * @throws \OpenStack\Exception if an unexpected response code is returned. While this should never happen on + * OpenStack servers, forks of OpenStack may choose to extend object storage in a way + * that results in a non-standard code. + */ + public function deleteContainer($name) + { + $url = $this->url() . '/' . rawurlencode($name); + + try { + $data = $this->req($url, 'DELETE', FALSE); + } catch (\OpenStack\Transport\FileNotFoundException $e) { + return FALSE; + } + // XXX: I'm not terribly sure about this. Why not just throw the + // ConflictException? + catch (\OpenStack\Transport\ConflictException $e) { + throw new ObjectStorage\ContainerNotEmptyException("Non-empty container cannot be deleted."); + } + + $status = $data->getStatusCode(); + + // 204 indicates that the container has been deleted. + if ($status == 204) { + return TRUE; + } + // OpenStacks documentation doesn't suggest any other return + // codes. + else { + throw new \OpenStack\Exception('Server returned unexpected code: ' . $status); + } + } + + /** + * Retrieve account info. + * + * This returns information about: + * + * - The total bytes used by this Object Storage instance (`bytes`). + * - The number of containers (`count`). + * + * @return array An associative array of account info. Typical keys are: + * - bytes: Bytes consumed by existing content. + * - containers: Number of containers. + * - objects: Number of objects. + * + * @throws \OpenStack\Transport\AuthorizationException if the user credentials are invalid or have expired. + */ + public function accountInfo() + { + $url = $this->url(); + $data = $this->req($url, 'HEAD', FALSE); + + $results = array( + 'bytes' => $data->getHeader('X-Account-Bytes-Used', 0), + 'containers' => $data->getHeader('X-Account-Container-Count', 0), + 'objects' => $data->getHeader('X-Account-Container-Count', 0), + ); + + return $results; + } + + /** + * Do a GET on Swift. + * + * This is a convenience method that handles the + * most common case of Swift requests. + */ + protected function get($url, $jsonDecode = TRUE) + { + return $this->req($url, 'GET', $jsonDecode); + } + + /** + * Internal request issuing command. + */ + protected function req($url, $method = 'GET', $jsonDecode = TRUE, $body = '') + { + $headers = array( + 'X-Auth-Token' => $this->token(), + ); + + $res = $this->client->doRequest($url, $method, $headers, $body); + if (!$jsonDecode) { + return $res; + } + + return $res->json(); + + } } diff --git a/src/OpenStack/Storage/ObjectStorage/ACL.php b/src/OpenStack/Storage/ObjectStorage/ACL.php index e60bd9c..2a714c4 100644 --- a/src/OpenStack/Storage/ObjectStorage/ACL.php +++ b/src/OpenStack/Storage/ObjectStorage/ACL.php @@ -2,17 +2,17 @@ /* ============================================================================ (c) Copyright 2012-2014 Hewlett-Packard Development Company, L.P. - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at - http://www.apache.org/licenses/LICENSE-2.0 + http://www.apache.org/licenses/LICENSE-2.0 - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. ============================================================================ */ /** * Contains the class for manipulating ObjectStorage ACL strings. @@ -98,459 +98,471 @@ namespace OpenStack\Storage\ObjectStorage; * For a detailed description of the rules for ACL creation, * @see http://swift.openstack.org/misc.html#acls */ -class ACL { +class ACL +{ + /** + * Read flag. + * + * This is for an ACL of the READ type. + */ + const READ = 1; + /** + * Write flag. + * + * This is for an ACL of the WRITE type. + */ + const WRITE = 2; + /** + * Flag for READ and WRITE. + * + * This is equivalent to `ACL::READ | ACL::WRITE` + */ + const READ_WRITE = 3; // self::READ | self::WRITE; - /** - * Read flag. - * - * This is for an ACL of the READ type. - */ - const READ = 1; - /** - * Write flag. - * - * This is for an ACL of the WRITE type. - */ - const WRITE = 2; - /** - * Flag for READ and WRITE. - * - * This is equivalent to `ACL::READ | ACL::WRITE` - */ - const READ_WRITE = 3; // self::READ | self::WRITE; + /** + * Header string for a read flag. + */ + const HEADER_READ = 'X-Container-Read'; + /** + * Header string for a write flag. + */ + const HEADER_WRITE = 'X-Container-Write'; - /** - * Header string for a read flag. - */ - const HEADER_READ = 'X-Container-Read'; - /** - * Header string for a write flag. - */ - const HEADER_WRITE = 'X-Container-Write'; + protected $rules = array(); - protected $rules = array(); + /** + * Allow READ access to the public. + * + * This grants the following: + * + * - READ to any host, with container listings. + * + * @return \OpenStack\Storage\ObjectStorage\ACL an ACL object with the + * appopriate permissions set. + */ + public static function makePublic() + { + $acl = new ACL(); + $acl->addReferrer(self::READ, '*'); + $acl->allowListings(); - /** - * Allow READ access to the public. - * - * This grants the following: - * - * - READ to any host, with container listings. - * - * @return \OpenStack\Storage\ObjectStorage\ACL an ACL object with the - * appopriate permissions set. - */ - public static function makePublic() { - $acl = new ACL(); - $acl->addReferrer(self::READ, '*'); - $acl->allowListings(); + return $acl; + } - return $acl; - } + /** + * Disallow all public access. + * + * Non-public is the same as private. Private, however, is a reserved + * word in PHP. + * + * This does not grant any permissions. OpenStack interprets an object + * with no permissions as a private object. + * + * @return \OpenStack\Storage\ObjectStorage\ACL an ACL object with the + * appopriate permissions set. + */ + public static function makeNonPublic() + { + // Default ACL is private. + return new ACL(); + } - /** - * Disallow all public access. - * - * Non-public is the same as private. Private, however, is a reserved - * word in PHP. - * - * This does not grant any permissions. OpenStack interprets an object - * with no permissions as a private object. - * - * @return \OpenStack\Storage\ObjectStorage\ACL an ACL object with the - * appopriate permissions set. - */ - public static function makeNonPublic() { - // Default ACL is private. - return new ACL(); - } + /** + * Alias of ACL::makeNonPublic(). + */ + public static function makePrivate() + { + return self::makeNonPublic(); + } - /** - * Alias of ACL::makeNonPublic(). - */ - public static function makePrivate() { - return self::makeNonPublic(); - } + /** + * Given a list of headers, get the ACL info. + * + * This is a utility for processing headers and discovering any ACLs embedded + * inside the headers. + * + * @param array $headers An associative array of headers. + * + * @return \OpenStack\Storage\ObjectStorage\ACL A new ACL. + */ + public static function newFromHeaders($headers) + { + $acl = new ACL(); - /** - * Given a list of headers, get the ACL info. - * - * This is a utility for processing headers and discovering any ACLs embedded - * inside the headers. - * - * @param array $headers An associative array of headers. - * - * @return \OpenStack\Storage\ObjectStorage\ACL A new ACL. - */ - public static function newFromHeaders($headers) { - $acl = new ACL(); - - // READ rules. - $rules = array(); - if (!empty($headers[self::HEADER_READ])) { - $read = $headers[self::HEADER_READ]; - $rules = explode(',', $read); - foreach ($rules as $rule) { - $ruleArray = self::parseRule(self::READ, $rule); - if (!empty($ruleArray)) { - $acl->rules[] = $ruleArray; + // READ rules. + $rules = array(); + if (!empty($headers[self::HEADER_READ])) { + $read = $headers[self::HEADER_READ]; + $rules = explode(',', $read); + foreach ($rules as $rule) { + $ruleArray = self::parseRule(self::READ, $rule); + if (!empty($ruleArray)) { + $acl->rules[] = $ruleArray; + } + } } - } - } - // WRITE rules. - $rules = array(); - if (!empty($headers[self::HEADER_WRITE])) { - $write = $headers[self::HEADER_WRITE]; - $rules = explode(',', $write); - foreach ($rules as $rule) { - $ruleArray = self::parseRule(self::WRITE, $rule); - if (!empty($ruleArray)) { - $acl->rules[] = $ruleArray; + // WRITE rules. + $rules = array(); + if (!empty($headers[self::HEADER_WRITE])) { + $write = $headers[self::HEADER_WRITE]; + $rules = explode(',', $write); + foreach ($rules as $rule) { + $ruleArray = self::parseRule(self::WRITE, $rule); + if (!empty($ruleArray)) { + $acl->rules[] = $ruleArray; + } + } } - } + + //throw new \Exception(print_r($acl->rules(), TRUE)); + return $acl; } - //throw new \Exception(print_r($acl->rules(), TRUE)); + /** + * Parse a rule. + * + * This attempts to parse an ACL rule. It is not particularly + * fault-tolerant. + * + * @param int $perm The permission (ACL::READ, ACL::WRITE). + * @param string $rule The string rule to parse. + * + * @return array The rule as an array. + */ + public static function parseRule($perm, $rule) + { + // This regular expression generates the following: + // + // array( + // 0 => ENTIRE RULE + // 1 => WHOLE EXPRESSION, no whitespace + // 2 => domain compontent + // 3 => 'rlistings', set if .rincludes is the directive + // 4 => account name + // 5 => :username + // 6 => username + // ); + $exp = '/^\s*(.r:([a-zA-Z0-9\*\-\.]+)|\.(rlistings)|([a-zA-Z0-9]+)(\:([a-zA-Z0-9]+))?)\s*$/'; - return $acl; - } + $matches = array(); + preg_match($exp, $rule, $matches); - /** - * Parse a rule. - * - * This attempts to parse an ACL rule. It is not particularly - * fault-tolerant. - * - * @param int $perm The permission (ACL::READ, ACL::WRITE). - * @param string $rule The string rule to parse. - * - * @return array The rule as an array. - */ - public static function parseRule($perm, $rule) { - // This regular expression generates the following: - // - // array( - // 0 => ENTIRE RULE - // 1 => WHOLE EXPRESSION, no whitespace - // 2 => domain compontent - // 3 => 'rlistings', set if .rincludes is the directive - // 4 => account name - // 5 => :username - // 6 => username - // ); - $exp = '/^\s*(.r:([a-zA-Z0-9\*\-\.]+)|\.(rlistings)|([a-zA-Z0-9]+)(\:([a-zA-Z0-9]+))?)\s*$/'; - - $matches = array(); - preg_match($exp, $rule, $matches); - - $entry = array('mask' => $perm); - if (!empty($matches[2])) { - $entry['host'] = $matches[2]; - } - elseif (!empty($matches[3])) { - $entry['rlistings'] = TRUE; - } - elseif (!empty($matches[4])) { - $entry['account'] = $matches[4]; - if (!empty($matches[6])) { - $entry['user'] = $matches[6]; - } - } - - return $entry; - } - - /** - * Create a new ACL. - * - * This creates an empty ACL with no permissions granted. When no - * permissions are granted, the file is effectively private - * (nonPublic()). - * - * Use add* methods to add permissions. - */ - public function __construct() {} - - /** - * Grant ACL access to an account. - * - * Optionally, a user may be given to further limit access. - * - * This is used to restrict access to a particular account and, if so - * specified, a specific user on that account. - * - * If just an account is given, any user on that account will be - * automatically granted access. - * - * If an account and a user is given, only that user of the account is - * granted access. - * - * If $user is an array, every user in the array will be granted - * access under the provided account. That is, for each user in the - * array, an entry of the form `account:user` will be generated in the - * final ACL. - * - * At this time there does not seem to be a way to grant global write - * access to an object. - * - * @param int $perm ACL::READ, ACL::WRITE or ACL::READ_WRITE (which is the - * same as ACL::READ|ACL::WRITE). - * @param string $account The name of the account. - * @param mixed $user The name of the user, or optionally an indexed array of - * user names. - * - * @return \OpenStack\Storage\ObjectStorage\ACL $this for current object so - * the method can be used in chaining. - */ - public function addAccount($perm, $account, $user = NULL) { - $rule = array('account' => $account); - - if (!empty($user)) { - $rule['user'] = $user; - } - - $this->addRule($perm, $rule); - - return $this; - } - - /** - * Allow (or deny) a hostname or host pattern. - * - * In current Swift implementations, only READ rules can have host - * patterns. WRITE permissions cannot be granted to hostnames. - * - * Formats: - * - Allow any host: '*' - * - Allow exact host: 'www.example.com' - * - Allow hosts in domain: '.example.com' - * - Disallow exact host: '-www.example.com' - * - Disallow hosts in domain: '-.example.com' - * - * Note that a simple minus sign ('-') is illegal, though it seems it - * should be "disallow all hosts." - * - * @param string $perm The permission being granted. One of ACL:READ, - * ACL::WRITE, or ACL::READ_WRITE. - * @param string $host A host specification string as described above. - * - * @return \OpenStack\Storage\ObjectStorage\ACL $this for current object so - * the method can be used in chaining. - */ - public function addReferrer($perm, $host = '*') { - $this->addRule($perm, array('host' => $host)); - - return $this; - } - - /** - * Add a rule to the appropriate stack of rules. - * - * @param int $perm One of the predefined permission constants. - * @param array $rule A rule array. - * - * @return \OpenStack\Storage\ObjectStorage\ACL $this for current object so - * the method can be used in chaining. - */ - protected function addRule($perm, $rule) { - $rule['mask'] = $perm; - - $this->rules[] = $rule; - - return $this; - } - - /** - * Allow hosts with READ permissions to list a container's content. - * - * By default, granting READ permission on a container does not grant - * permission to list the contents of a container. Setting the - * ACL::allowListings() permission will allow matching hosts to also list - * the contents of a container. - * - * In the current Swift implementation, there is no mechanism for - * allowing some hosts to get listings, while denying others. - * - * @return \OpenStack\Storage\ObjectStorage\ACL $this for current object so - * the method can be used in chaining. - */ - public function allowListings() { - - $this->rules[] = array( - 'mask' => self::READ, - 'rlistings' => TRUE, - ); - - return $this; - } - - /** - * Get the rules array for this ACL. - * - * @return array An array of associative arrays of rules. - */ - public function rules() { - return $this->rules; - } - - /** - * Generate HTTP headers for this ACL. - * - * If this is called on an empty object, an empty set of headers is - * returned. - * - * @return array Array of headers - */ - public function headers() { - $headers = array(); - $readers = array(); - $writers = array(); - - // Create the rule strings. We need two copies, one for READ and - // one for WRITE. - foreach ($this->rules as $rule) { - // We generate read and write rules separately so that the - // generation logic has a chance to respond to the differences - // allowances for READ and WRITE ACLs. - if (self::READ & $rule['mask']) { - $ruleStr = $this->ruleToString(self::READ, $rule); - if (!empty($ruleStr)) { - $readers[] = $ruleStr; + $entry = array('mask' => $perm); + if (!empty($matches[2])) { + $entry['host'] = $matches[2]; + } elseif (!empty($matches[3])) { + $entry['rlistings'] = TRUE; + } elseif (!empty($matches[4])) { + $entry['account'] = $matches[4]; + if (!empty($matches[6])) { + $entry['user'] = $matches[6]; + } } - } - if (self::WRITE & $rule['mask']) { - $ruleStr = $this->ruleToString(self::WRITE, $rule); - if (!empty($ruleStr)) { - $writers[] = $ruleStr; + + return $entry; + } + + /** + * Create a new ACL. + * + * This creates an empty ACL with no permissions granted. When no + * permissions are granted, the file is effectively private + * (nonPublic()). + * + * Use add* methods to add permissions. + */ + public function __construct() {} + + /** + * Grant ACL access to an account. + * + * Optionally, a user may be given to further limit access. + * + * This is used to restrict access to a particular account and, if so + * specified, a specific user on that account. + * + * If just an account is given, any user on that account will be + * automatically granted access. + * + * If an account and a user is given, only that user of the account is + * granted access. + * + * If $user is an array, every user in the array will be granted + * access under the provided account. That is, for each user in the + * array, an entry of the form `account:user` will be generated in the + * final ACL. + * + * At this time there does not seem to be a way to grant global write + * access to an object. + * + * @param int $perm ACL::READ, ACL::WRITE or ACL::READ_WRITE (which is the + * same as ACL::READ|ACL::WRITE). + * @param string $account The name of the account. + * @param mixed $user The name of the user, or optionally an indexed array of + * user names. + * + * @return \OpenStack\Storage\ObjectStorage\ACL $this for current object so + * the method can be used in chaining. + */ + public function addAccount($perm, $account, $user = NULL) + { + $rule = array('account' => $account); + + if (!empty($user)) { + $rule['user'] = $user; } - } + + $this->addRule($perm, $rule); + + return $this; } - // Create the HTTP headers. - if (!empty($readers)) { - $headers[self::HEADER_READ] = implode(',', $readers); - } - if (!empty($writers)) { - $headers[self::HEADER_WRITE] = implode(',', $writers); + /** + * Allow (or deny) a hostname or host pattern. + * + * In current Swift implementations, only READ rules can have host + * patterns. WRITE permissions cannot be granted to hostnames. + * + * Formats: + * - Allow any host: '*' + * - Allow exact host: 'www.example.com' + * - Allow hosts in domain: '.example.com' + * - Disallow exact host: '-www.example.com' + * - Disallow hosts in domain: '-.example.com' + * + * Note that a simple minus sign ('-') is illegal, though it seems it + * should be "disallow all hosts." + * + * @param string $perm The permission being granted. One of ACL:READ, + * ACL::WRITE, or ACL::READ_WRITE. + * @param string $host A host specification string as described above. + * + * @return \OpenStack\Storage\ObjectStorage\ACL $this for current object so + * the method can be used in chaining. + */ + public function addReferrer($perm, $host = '*') + { + $this->addRule($perm, array('host' => $host)); + + return $this; } - return $headers; - } + /** + * Add a rule to the appropriate stack of rules. + * + * @param int $perm One of the predefined permission constants. + * @param array $rule A rule array. + * + * @return \OpenStack\Storage\ObjectStorage\ACL $this for current object so + * the method can be used in chaining. + */ + protected function addRule($perm, $rule) + { + $rule['mask'] = $perm; - /** - * Convert a rule to a string. - * - * @param int $perm The permission for which to generate the rule. - * @param array $rule A rule array. - */ - protected function ruleToString($perm, $rule) { + $this->rules[] = $rule; - // Some rules only apply to READ. - if (self::READ & $perm) { - - // Host rule. - if (!empty($rule['host'])) { - return '.r:' . $rule['host']; - } - - // Listing rule. - if (!empty($rule['rlistings'])) { - return '.rlistings'; - } + return $this; } - // READ and WRITE both allow account/user rules. - if (!empty($rule['account'])) { + /** + * Allow hosts with READ permissions to list a container's content. + * + * By default, granting READ permission on a container does not grant + * permission to list the contents of a container. Setting the + * ACL::allowListings() permission will allow matching hosts to also list + * the contents of a container. + * + * In the current Swift implementation, there is no mechanism for + * allowing some hosts to get listings, while denying others. + * + * @return \OpenStack\Storage\ObjectStorage\ACL $this for current object so + * the method can be used in chaining. + */ + public function allowListings() + { + $this->rules[] = array( + 'mask' => self::READ, + 'rlistings' => TRUE, + ); - // Just an account name. - if (empty($rule['user'])) { - return $rule['account']; - } + return $this; + } + + /** + * Get the rules array for this ACL. + * + * @return array An array of associative arrays of rules. + */ + public function rules() + { + return $this->rules; + } + + /** + * Generate HTTP headers for this ACL. + * + * If this is called on an empty object, an empty set of headers is + * returned. + * + * @return array Array of headers + */ + public function headers() + { + $headers = array(); + $readers = array(); + $writers = array(); + + // Create the rule strings. We need two copies, one for READ and + // one for WRITE. + foreach ($this->rules as $rule) { + // We generate read and write rules separately so that the + // generation logic has a chance to respond to the differences + // allowances for READ and WRITE ACLs. + if (self::READ & $rule['mask']) { + $ruleStr = $this->ruleToString(self::READ, $rule); + if (!empty($ruleStr)) { + $readers[] = $ruleStr; + } + } + if (self::WRITE & $rule['mask']) { + $ruleStr = $this->ruleToString(self::WRITE, $rule); + if (!empty($ruleStr)) { + $writers[] = $ruleStr; + } + } + } + + // Create the HTTP headers. + if (!empty($readers)) { + $headers[self::HEADER_READ] = implode(',', $readers); + } + if (!empty($writers)) { + $headers[self::HEADER_WRITE] = implode(',', $writers); + } + + return $headers; + } + + /** + * Convert a rule to a string. + * + * @param int $perm The permission for which to generate the rule. + * @param array $rule A rule array. + */ + protected function ruleToString($perm, $rule) + { + // Some rules only apply to READ. + if (self::READ & $perm) { + + // Host rule. + if (!empty($rule['host'])) { + return '.r:' . $rule['host']; + } + + // Listing rule. + if (!empty($rule['rlistings'])) { + return '.rlistings'; + } + } + + // READ and WRITE both allow account/user rules. + if (!empty($rule['account'])) { + + // Just an account name. + if (empty($rule['user'])) { + return $rule['account']; + } + + // Account + multiple users. + elseif (is_array($rule['user'])) { + $buffer = array(); + foreach ($rule['user'] as $user) { + $buffer[] = $rule['account'] . ':' . $user; + } + + return implode(',', $buffer); + + } + + // Account + one user. + else { + return $rule['account'] . ':' . $rule['user']; + } + } + } + + /** + * Check if the ACL marks this private. + * + * This returns TRUE only if this ACL does not grant any permissions + * at all. + * + * @return boolean TRUE if this is private (non-public), FALSE if any + * permissions are granted via this ACL. + */ + public function isNonPublic() + { + return empty($this->rules); + } + + /** + * Alias of isNonPublic(). + */ + public function isPrivate() + { + return $this->isNonPublic(); + } + + /** + * Check whether this object allows public reading. + * + * This will return TRUE the ACL allows (a) any host to access + * the item, and (b) it allows container listings. + * + * This checks whether the object allows public reading, + * not whether it is ONLY allowing public reads. + * + * @see ACL::makePublic(). + * + * @return boolean Whether or not the object allows public reading. + */ + public function isPublic() + { + $allowsAllHosts = FALSE; + $allowsRListings = FALSE; + foreach ($this->rules as $rule) { + if (self::READ & $rule['mask']) { + if (!empty($rule['rlistings'])) { + $allowsRListings = TRUE; + } elseif (!empty($rule['host']) && trim($rule['host']) == '*') { + $allowsAllHosts = TRUE; + } + } + } + + return $allowsAllHosts && $allowsRListings; + } + + /** + * Implements the magic `__toString()` PHP function. + * + * This allows you to `print $acl` and get back + * a pretty string. + * + * @return string The ACL represented as a string. + */ + public function __toString() + { + $headers = $this->headers(); - // Account + multiple users. - elseif (is_array($rule['user'])) { $buffer = array(); - foreach ($rule['user'] as $user) { - $buffer[] = $rule['account'] . ':' . $user; + foreach ($headers as $k => $v) { + $buffer[] = $k . ': ' . $v; } - return implode(',', $buffer); - } - - // Account + one user. - else { - return $rule['account'] . ':' . $rule['user']; - } - } - } - - /** - * Check if the ACL marks this private. - * - * This returns TRUE only if this ACL does not grant any permissions - * at all. - * - * @return boolean TRUE if this is private (non-public), FALSE if any - * permissions are granted via this ACL. - */ - public function isNonPublic() { - return empty($this->rules); - } - - /** - * Alias of isNonPublic(). - */ - public function isPrivate() { - return $this->isNonPublic(); - } - - /** - * Check whether this object allows public reading. - * - * This will return TRUE the ACL allows (a) any host to access - * the item, and (b) it allows container listings. - * - * This checks whether the object allows public reading, - * not whether it is ONLY allowing public reads. - * - * @see ACL::makePublic(). - * - * @return boolean Whether or not the object allows public reading. - */ - public function isPublic() { - $allowsAllHosts = FALSE; - $allowsRListings = FALSE; - foreach ($this->rules as $rule) { - if (self::READ & $rule['mask']) { - if (!empty($rule['rlistings'])) { - $allowsRListings = TRUE; - } - elseif(!empty($rule['host']) && trim($rule['host']) == '*') { - $allowsAllHosts = TRUE; - } - } - } - return $allowsAllHosts && $allowsRListings; - } - - /** - * Implements the magic `__toString()` PHP function. - * - * This allows you to `print $acl` and get back - * a pretty string. - * - * @return string The ACL represented as a string. - */ - public function __toString() { - $headers = $this->headers(); - - $buffer = array(); - foreach ($headers as $k => $v) { - $buffer[] = $k . ': ' . $v; + return implode("\t", $buffer); } - return implode("\t", $buffer); - } - -} \ No newline at end of file +} diff --git a/src/OpenStack/Storage/ObjectStorage/Container.php b/src/OpenStack/Storage/ObjectStorage/Container.php index a74b735..413e2d9 100644 --- a/src/OpenStack/Storage/ObjectStorage/Container.php +++ b/src/OpenStack/Storage/ObjectStorage/Container.php @@ -2,17 +2,17 @@ /* ============================================================================ (c) Copyright 2012-2014 Hewlett-Packard Development Company, L.P. - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at - http://www.apache.org/licenses/LICENSE-2.0 + http://www.apache.org/licenses/LICENSE-2.0 - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. ============================================================================ */ /** * Contains the class for ObjectStorage Container objects. @@ -63,1007 +63,1035 @@ use OpenStack\Transport\GuzzleClient; * * @todo Add support for container metadata. */ -class Container implements \Countable, \IteratorAggregate { - /** - * The prefix for any piece of metadata passed in HTTP headers. - */ - const METADATA_HEADER_PREFIX = 'X-Object-Meta-'; - const CONTAINER_METADATA_HEADER_PREFIX = 'X-Container-Meta-'; +class Container implements \Countable, \IteratorAggregate +{ + /** + * The prefix for any piece of metadata passed in HTTP headers. + */ + const METADATA_HEADER_PREFIX = 'X-Object-Meta-'; + const CONTAINER_METADATA_HEADER_PREFIX = 'X-Container-Meta-'; + //protected $properties = array(); + protected $name = NULL; - //protected $properties = array(); - protected $name = NULL; + // These were both changed from 0 to NULL to allow + // lazy loading. + protected $count = NULL; + protected $bytes = NULL; - // These were both changed from 0 to NULL to allow - // lazy loading. - protected $count = NULL; - protected $bytes = NULL; + protected $token; + protected $url; + protected $baseUrl; + protected $acl; + protected $metadata; - protected $token; - protected $url; - protected $baseUrl; - protected $acl; - protected $metadata; + /** + * The HTTP Client + */ + protected $client; - /** - * The HTTP Client - */ - protected $client; + /** + * Transform a metadata array into headers. + * + * This is used when storing an object in a container. + * + * @param array $metadata An associative array of metadata. Metadata is not + * escaped in any way (there is no codified spec by which to escape), so + * make sure that keys are alphanumeric (dashes allowed) and values are + * ASCII-armored with no newlines. + * @param string $prefix A prefix for the metadata headers. + * + * @return array An array of headers. + * + * @see http://docs.openstack.org/bexar/openstack-object-storage/developer/content/ch03s03.html#d5e635 + * @see http://docs.openstack.org/bexar/openstack-object-storage/developer/content/ch03s03.html#d5e700 + */ + public static function generateMetadataHeaders(array $metadata, $prefix = NULL) + { + if (empty($prefix)) { + $prefix = Container::METADATA_HEADER_PREFIX; + } + $headers = array(); + foreach ($metadata as $key => $val) { + $headers[$prefix . $key] = $val; + } - /** - * Transform a metadata array into headers. - * - * This is used when storing an object in a container. - * - * @param array $metadata An associative array of metadata. Metadata is not - * escaped in any way (there is no codified spec by which to escape), so - * make sure that keys are alphanumeric (dashes allowed) and values are - * ASCII-armored with no newlines. - * @param string $prefix A prefix for the metadata headers. - * - * @return array An array of headers. - * @see http://docs.openstack.org/bexar/openstack-object-storage/developer/content/ch03s03.html#d5e635 - * @see http://docs.openstack.org/bexar/openstack-object-storage/developer/content/ch03s03.html#d5e700 - */ - public static function generateMetadataHeaders(array $metadata, $prefix = NULL) { - if (empty($prefix)) { - $prefix = Container::METADATA_HEADER_PREFIX; + return $headers; } - $headers = array(); - foreach ($metadata as $key => $val) { - $headers[$prefix . $key] = $val; - } - return $headers; - } - /** - * Create an object URL. - * - * Given a base URL and an object name, create an object URL. - * - * This is useful because object names can contain certain characters - * (namely slashes (`/`)) that are normally URLencoded when they appear - * inside of path sequences. - * - * Swift does not distinguish between `%2F` and a slash character, so - * this is not strictly necessary. - * - * @param string $base The base URL. This is not altered; it is just prepended - * to the returned string. - * @param string $oname The name of the object. - * - * @return string The URL to the object. Characters that need escaping will be - * escaped, while slash characters are not. Thus, the URL will look pathy. - */ - public static function objectUrl($base, $oname) { - if (strpos($oname, '/') === FALSE) { - return $base . '/' . rawurlencode($oname); + /** + * Create an object URL. + * + * Given a base URL and an object name, create an object URL. + * + * This is useful because object names can contain certain characters + * (namely slashes (`/`)) that are normally URLencoded when they appear + * inside of path sequences. + * + * Swift does not distinguish between `%2F` and a slash character, so + * this is not strictly necessary. + * + * @param string $base The base URL. This is not altered; it is just prepended + * to the returned string. + * @param string $oname The name of the object. + * + * @return string The URL to the object. Characters that need escaping will be + * escaped, while slash characters are not. Thus, the URL will + * look pathy. + */ + public static function objectUrl($base, $oname) + { + if (strpos($oname, '/') === FALSE) { + return $base . '/' . rawurlencode($oname); + } + + $oParts = explode('/', $oname); + $buffer = array(); + foreach ($oParts as $part) { + $buffer[] = rawurlencode($part); + } + $newname = implode('/', $buffer); + + return $base . '/' . $newname; } - $oParts = explode('/', $oname); - $buffer = array(); - foreach ($oParts as $part) { - $buffer[] = rawurlencode($part); - } - $newname = implode('/', $buffer); - return $base . '/' . $newname; - } + /** + * Extract object attributes from HTTP headers. + * + * When OpenStack sends object attributes, it sometimes embeds them in + * HTTP headers with a prefix. This function parses the headers and + * returns the attributes as name/value pairs. + * + * Note that no decoding (other than the minimum amount necessary) is + * done to the attribute names or values. The Open Stack Swift + * documentation does not prescribe encoding standards for name or + * value data, so it is left up to implementors to choose their own + * strategy. + * + * @param array $headers An associative array of HTTP headers. + * @param string $prefix The prefix on metadata headers. + * + * @return array An associative array of name/value attribute pairs. + */ + public static function extractHeaderAttributes($headers, $prefix = NULL) + { + if (empty($prefix)) { + $prefix = Container::METADATA_HEADER_PREFIX; + } + $attributes = array(); + $offset = strlen($prefix); + foreach ($headers as $header => $value) { - /** - * Extract object attributes from HTTP headers. - * - * When OpenStack sends object attributes, it sometimes embeds them in - * HTTP headers with a prefix. This function parses the headers and - * returns the attributes as name/value pairs. - * - * Note that no decoding (other than the minimum amount necessary) is - * done to the attribute names or values. The Open Stack Swift - * documentation does not prescribe encoding standards for name or - * value data, so it is left up to implementors to choose their own - * strategy. - * - * @param array $headers An associative array of HTTP headers. - * @param string $prefix The prefix on metadata headers. - * - * @return array An associative array of name/value attribute pairs. - */ - public static function extractHeaderAttributes($headers, $prefix = NULL) { - if (empty($prefix)) { - $prefix = Container::METADATA_HEADER_PREFIX; - } - $attributes = array(); - $offset = strlen($prefix); - foreach ($headers as $header => $value) { + $index = strpos($header, $prefix); + if ($index === 0) { + $key = substr($header, $offset); + $attributes[$key] = $value; + } + } - $index = strpos($header, $prefix); - if ($index === 0) { - $key = substr($header, $offset); - $attributes[$key] = $value; - } - } - return $attributes; - } - - /** - * Create a new Container from JSON data. - * - * This is used in lieue of a standard constructor when - * fetching containers from ObjectStorage. - * - * @param array $jsonArray An associative array as returned by - * json_decode($foo, TRUE); - * @param string $token The auth token. - * @param string $url The base URL. The container name is automatically - * appended to this at construction time. - * @param \OpenStack\Transport\ClientInterface $client A HTTP transport client. - * - * @return \OpenStack\Storage\ObjectStorage\Container A new container object. - */ - public static function newFromJSON($jsonArray, $token, $url, \OpenStack\Transport\ClientInterface $client = NULL) { - $container = new Container($jsonArray['name'], NULL, NULL, $client); - - $container->baseUrl = $url; - - $container->url = $url . '/' . rawurlencode($jsonArray['name']); - $container->token = $token; - - // Access to count and bytes is basically controlled. This is is to - // prevent a local copy of the object from getting out of sync with - // the remote copy. - if (!empty($jsonArray['count'])) { - $container->count = $jsonArray['count']; + return $attributes; } - if (!empty($jsonArray['bytes'])) { - $container->bytes = $jsonArray['bytes']; + /** + * Create a new Container from JSON data. + * + * This is used in lieue of a standard constructor when + * fetching containers from ObjectStorage. + * + * @param array $jsonArray An associative array as returned by + * json_decode($foo, TRUE); + * @param string $token The auth token. + * @param string $url The base URL. The container name is automatically + * appended to this at construction time. + * @param \OpenStack\Transport\ClientInterface $client A HTTP transport client. + * + * @return \OpenStack\Storage\ObjectStorage\Container A new container object. + */ + public static function newFromJSON($jsonArray, $token, $url, \OpenStack\Transport\ClientInterface $client = NULL) + { + $container = new Container($jsonArray['name'], NULL, NULL, $client); + + $container->baseUrl = $url; + + $container->url = $url . '/' . rawurlencode($jsonArray['name']); + $container->token = $token; + + // Access to count and bytes is basically controlled. This is is to + // prevent a local copy of the object from getting out of sync with + // the remote copy. + if (!empty($jsonArray['count'])) { + $container->count = $jsonArray['count']; + } + + if (!empty($jsonArray['bytes'])) { + $container->bytes = $jsonArray['bytes']; + } + + //syslog(LOG_WARNING, print_r($jsonArray, TRUE)); + return $container; } - //syslog(LOG_WARNING, print_r($jsonArray, TRUE)); + /** + * Given an OpenStack HTTP response, build a Container. + * + * This factory is intended for use by low-level libraries. In most + * cases, the standard constructor is preferred for client-size + * Container initialization. + * + * @param string $name The name of the container. + * @param object $response \OpenStack\Transport\Response The HTTP response object from the Transporter layer + * @param string $token The auth token. + * @param string $url The base URL. The container name is automatically + * appended to this at construction time. + * @param \OpenStack\Transport\ClientInterface $client A HTTP transport client. + * + * @return \OpenStack\Storage\ObjectStorage\Container The Container object, initialized and ready for use. + */ + public static function newFromResponse($name, $response, $token, $url, \OpenStack\Transport\ClientInterface $client = NULL) + { + $container = new Container($name, NULL, NULL, $client); + $container->bytes = $response->getHeader('X-Container-Bytes-Used', 0); + $container->count = $response->getHeader('X-Container-Object-Count', 0); + $container->baseUrl = $url; + $container->url = $url . '/' . rawurlencode($name); + $container->token = $token; - return $container; - } + $headers = self::reformatHeaders($response->getHeaders()); - /** - * Given an OpenStack HTTP response, build a Container. - * - * This factory is intended for use by low-level libraries. In most - * cases, the standard constructor is preferred for client-size - * Container initialization. - * - * @param string $name The name of the container. - * @param object $response \OpenStack\Transport\Response The HTTP response object from the Transporter layer - * @param string $token The auth token. - * @param string $url The base URL. The container name is automatically - * appended to this at construction time. - * @param \OpenStack\Transport\ClientInterface $client A HTTP transport client. - * - * @return \OpenStack\Storage\ObjectStorage\Container The Container object, - * initialized and ready for use. - */ - public static function newFromResponse($name, $response, $token, $url, \OpenStack\Transport\ClientInterface $client = NULL) { - $container = new Container($name, NULL, NULL, $client); - $container->bytes = $response->getHeader('X-Container-Bytes-Used', 0); - $container->count = $response->getHeader('X-Container-Object-Count', 0); - $container->baseUrl = $url; - $container->url = $url . '/' . rawurlencode($name); - $container->token = $token; + $container->acl = ACL::newFromHeaders($headers); - $headers = self::reformatHeaders($response->getHeaders()); + $prefix = Container::CONTAINER_METADATA_HEADER_PREFIX; + $metadata = Container::extractHeaderAttributes($headers, $prefix); + $container->setMetadata($metadata); - $container->acl = ACL::newFromHeaders($headers); - - $prefix = Container::CONTAINER_METADATA_HEADER_PREFIX; - $metadata = Container::extractHeaderAttributes($headers, $prefix); - $container->setMetadata($metadata); - - return $container; - } - - /** - * Construct a new Container. - * - * Typically a container should be created by ObjectStorage::createContainer(). - * Get existing containers with ObjectStorage::container() or - * ObjectStorage::containers(). Using the constructor directly has some - * side effects of which you should be aware. - * - * Simply creating a container does not save the container remotely. - * - * Also, this does no checking of the underlying container. That is, simply - * constructing a Container in no way guarantees that such a container exists - * on the origin object store. - * - * The constructor involves a selective lazy loading. If a new container is created, - * and one of its accessors is called before the accessed values are initialized, then - * this will make a network round-trip to get the container from the remote server. - * - * Containers loaded from ObjectStorage::container() or Container::newFromRemote() - * will have all of the necessary values set, and thus will not require an extra network - * transaction to fetch properties. - * - * The practical result of this: - * - * - If you are creating a new container, it is best to do so with - * ObjectStorage::createContainer(). - * - If you are manipulating an existing container, it is best to load the - * container with ObjectStorage::container(). - * - If you are simply using the container to fetch resources from the - * container, you may wish to use `new Container($name, $url, $token)` - * and then load objects from that container. Note, however, that - * manipulating the container directly will likely involve an extra HTTP - * transaction to load the container data. - * - When in doubt, use the ObjectStorage methods. That is always the safer - * option. - * - * @param string $name The name. - * @param string $url The full URL to the container. - * @param string $token The auth token. - * @param \OpenStack\Transport\ClientInterface $client A HTTP transport client. - */ - public function __construct($name , $url = NULL, $token = NULL, \OpenStack\Transport\ClientInterface $client = NULL) { - $this->name = $name; - $this->url = $url; - $this->token = $token; - - // Guzzle is the default client to use. - if (is_null($client)) { - $this->client = new GuzzleClient(); - } - else { - $this->client = $client; - } - } - - /** - * Get the name of this container. - * - * @return string The name of the container. - */ - public function name() { - return $this->name; - } - - /** - * Get the number of bytes in this container. - * - * @return int The number of bytes in this container. - */ - public function bytes() { - if (is_null($this->bytes)) { - $this->loadExtraData(); - } - return $this->bytes; - } - - /** - * Get the container metadata. - * - * Metadata (also called tags) are name/value pairs that can be - * attached to a container. - * - * Names can be no longer than 128 characters, and values can be no - * more than 256. UTF-8 or ASCII characters are allowed, though ASCII - * seems to be preferred. - * - * If the container was loaded from a container listing, the metadata - * will be fetched in a new HTTP request. This is because container - * listings do not supply the metadata, while loading a container - * directly does. - * - * @return array An array of metadata name/value pairs. - */ - public function metadata() { - - // If created from JSON, metadata does not get fetched. - if (!isset($this->metadata)) { - $this->loadExtraData(); - } - return $this->metadata; - } - - /** - * Set the tags on the container. - * - * Container metadata (sometimes called "tags") provides a way of - * storing arbitrary name/value pairs on a container. - * - * Since saving a container is a function of the ObjectStorage - * itself, if you change the metadta, you will need to call - * ObjectStorage::updateContainer() to save the new container metadata - * on the remote object storage. - * - * (Similarly, when it comes to objects, an object's metdata is saved - * by the container.) - * - * Names can be no longer than 128 characters, and values can be no - * more than 256. UTF-8 or ASCII characters are allowed, though ASCII - * seems to be preferred. - * - * @return \OpenStack\Storage\ObjectStorage\Container $this so the method can - * be used in chaining. - */ - public function setMetadata($metadata) { - $this->metadata = $metadata; - - return $this; - } - - /** - * Get the number of items in this container. - * - * Since Container implements Countable, the PHP builtin count() can be used - * on a Container instance: - * - * count(); - * ?> - * - * @return int The number of items in this container. - */ - public function count() { - if (is_null($this->count)) { - $this->loadExtraData(); - } - return $this->count; - } - - /** - * Save an Object into Object Storage. - * - * This takes an \OpenStack\Storage\ObjectStorage\Object - * and stores it in the given container in the present - * container on the remote object store. - * - * @param object $obj \OpenStack\Storage\ObjectStorage\Object The object to - * store. - * @param resource $file An optional file argument that, if set, will be - * treated as the contents of the object. - * - * @return boolean TRUE if the object was saved. - * @throws \OpenStack\Transport\LengthRequiredException if the Content-Length - * could not be determined and chunked encoding was not enabled. This should - * not occur for this class, which always automatically generates - * Content-Length headers. However, subclasses could generate this error. - * @throws \OpenStack\Transport\UnprocessableEntityException if the checksum - * passed here does not match the checksum calculated remotely. - * @throws \OpenStack\Exception when an unexpected (usually network-related) - * error condition arises. - */ - public function save(Object $obj, $file = NULL) { - - if (empty($this->token)) { - throw new \OpenStack\Exception('Container does not have an auth token.'); - } - if (empty($this->url)) { - throw new \OpenStack\Exception('Container does not have a URL to send data.'); + return $container; } - //$url = $this->url . '/' . rawurlencode($obj->name()); - $url = self::objectUrl($this->url, $obj->name()); + /** + * Construct a new Container. + * + * Typically a container should be created by ObjectStorage::createContainer(). + * Get existing containers with ObjectStorage::container() or + * ObjectStorage::containers(). Using the constructor directly has some + * side effects of which you should be aware. + * + * Simply creating a container does not save the container remotely. + * + * Also, this does no checking of the underlying container. That is, simply + * constructing a Container in no way guarantees that such a container exists + * on the origin object store. + * + * The constructor involves a selective lazy loading. If a new container is created, + * and one of its accessors is called before the accessed values are initialized, then + * this will make a network round-trip to get the container from the remote server. + * + * Containers loaded from ObjectStorage::container() or Container::newFromRemote() + * will have all of the necessary values set, and thus will not require an extra network + * transaction to fetch properties. + * + * The practical result of this: + * + * - If you are creating a new container, it is best to do so with + * ObjectStorage::createContainer(). + * - If you are manipulating an existing container, it is best to load the + * container with ObjectStorage::container(). + * - If you are simply using the container to fetch resources from the + * container, you may wish to use `new Container($name, $url, $token)` + * and then load objects from that container. Note, however, that + * manipulating the container directly will likely involve an extra HTTP + * transaction to load the container data. + * - When in doubt, use the ObjectStorage methods. That is always the safer + * option. + * + * @param string $name The name. + * @param string $url The full URL to the container. + * @param string $token The auth token. + * @param \OpenStack\Transport\ClientInterface $client A HTTP transport client. + */ + public function __construct($name , $url = NULL, $token = NULL, \OpenStack\Transport\ClientInterface $client = NULL) + { + $this->name = $name; + $this->url = $url; + $this->token = $token; - // See if we have any metadata. - $headers = array(); - $md = $obj->metadata(); - if (!empty($md)) { - $headers = self::generateMetadataHeaders($md, Container::METADATA_HEADER_PREFIX); + // Guzzle is the default client to use. + if (is_null($client)) { + $this->client = new GuzzleClient(); + } else { + $this->client = $client; + } } - - // Set the content type. - $headers['Content-Type'] = $obj->contentType(); - - - // Add content encoding, if necessary. - $encoding = $obj->encoding(); - if (!empty($encoding)) { - $headers['Content-Encoding'] = rawurlencode($encoding); + /** + * Get the name of this container. + * + * @return string The name of the container. + */ + public function name() + { + return $this->name; } - // Add content disposition, if necessary. - $disposition = $obj->disposition(); - if (!empty($disposition)) { - $headers['Content-Disposition'] = $disposition; + /** + * Get the number of bytes in this container. + * + * @return int The number of bytes in this container. + */ + public function bytes() + { + if (is_null($this->bytes)) { + $this->loadExtraData(); + } + + return $this->bytes; } - // Auth token. - $headers['X-Auth-Token'] = $this->token; + /** + * Get the container metadata. + * + * Metadata (also called tags) are name/value pairs that can be + * attached to a container. + * + * Names can be no longer than 128 characters, and values can be no + * more than 256. UTF-8 or ASCII characters are allowed, though ASCII + * seems to be preferred. + * + * If the container was loaded from a container listing, the metadata + * will be fetched in a new HTTP request. This is because container + * listings do not supply the metadata, while loading a container + * directly does. + * + * @return array An array of metadata name/value pairs. + */ + public function metadata() + { + // If created from JSON, metadata does not get fetched. + if (!isset($this->metadata)) { + $this->loadExtraData(); + } - // Add any custom headers: - $moreHeaders = $obj->additionalHeaders(); - if (!empty($moreHeaders)) { - $headers += $moreHeaders; + return $this->metadata; } - if (empty($file)) { - // Now build up the rest of the headers: - $headers['Etag'] = $obj->eTag(); - - // If chunked, we set transfer encoding; else - // we set the content length. - if ($obj->isChunked()) { - // How do we handle this? Does the underlying - // stream wrapper pay any attention to this? - $headers['Transfer-Encoding'] = 'chunked'; - } - else { - $headers['Content-Length'] = $obj->contentLength(); - } - $response = $this->client->doRequest($url, 'PUT', $headers, $obj->content()); - } - else { - // Rewind the file. - rewind($file); - - - // XXX: What do we do about Content-Length header? - //$headers['Transfer-Encoding'] = 'chunked'; - $stat = fstat($file); - $headers['Content-Length'] = $stat['size']; - - // Generate an eTag: - $hash = hash_init('md5'); - hash_update_stream($hash, $file); - $etag = hash_final($hash); - $headers['Etag'] = $etag; - - // Not sure if this is necessary: - rewind($file); - - $response = $this->client->doRequestWithResource($url, 'PUT', $headers, $file); + /** + * Set the tags on the container. + * + * Container metadata (sometimes called "tags") provides a way of + * storing arbitrary name/value pairs on a container. + * + * Since saving a container is a function of the ObjectStorage + * itself, if you change the metadta, you will need to call + * ObjectStorage::updateContainer() to save the new container metadata + * on the remote object storage. + * + * (Similarly, when it comes to objects, an object's metdata is saved + * by the container.) + * + * Names can be no longer than 128 characters, and values can be no + * more than 256. UTF-8 or ASCII characters are allowed, though ASCII + * seems to be preferred. + * + * @return \OpenStack\Storage\ObjectStorage\Container $this so the method can + * be used in chaining. + */ + public function setMetadata($metadata) + { + $this->metadata = $metadata; + return $this; } - if ($response->getStatusCode() != 201) { - throw new \OpenStack\Exception('An unknown error occurred while saving: ' . $response->status()); - } - return TRUE; - } + /** + * Get the number of items in this container. + * + * Since Container implements Countable, the PHP builtin count() can be used + * on a Container instance: + * + * count(); + * ?> + * + * @return int The number of items in this container. + */ + public function count() + { + if (is_null($this->count)) { + $this->loadExtraData(); + } - /** - * Update an object's metadata. - * - * This updates the metadata on an object without modifying anything - * else. This is a convenient way to set additional metadata without - * having to re-upload a potentially large object. - * - * Swift's behavior during this operation is sometimes unpredictable, - * particularly in cases where custom headers have been set. - * Use with caution. - * - * @param object $obj \OpenStack\Storage\ObjectStorage\Object The object to - * update. - * - * @return boolean TRUE if the metadata was updated. - * @throws \OpenStack\Transport\FileNotFoundException if the object does not - * already exist on the object storage. - */ - public function updateMetadata(Object $obj) { - //$url = $this->url . '/' . rawurlencode($obj->name()); - $url = self::objectUrl($this->url, $obj->name()); - $headers = array(); - - // See if we have any metadata. We post this even if there - // is no metadata. - $md = $obj->metadata(); - if (!empty($md)) { - $headers = self::generateMetadataHeaders($md, Container::METADATA_HEADER_PREFIX); - } - $headers['X-Auth-Token'] = $this->token; - - // In spite of the documentation's claim to the contrary, - // content type IS reset during this operation. - $headers['Content-Type'] = $obj->contentType(); - - // The POST verb is for updating headers. - $response = $this->client->doRequest($url, 'POST', $headers, $obj->content()); - - if ($response->getStatusCode() != 202) { - throw new \OpenStack\Exception('An unknown error occurred while saving: ' . $response->status()); - } - return TRUE; - } - - /** - * Copy an object to another place in object storage. - * - * An object can be copied within a container. Essentially, this will - * give you duplicates of the file, each with a new name. - * - * An object can be copied to another container if the name of the - * other container is specified, and if that container already exists. - * - * Note that there is no MOVE operation. You must copy and then DELETE - * in order to achieve that. - * - * @param object $obj \OpenStack\Storage\ObjectStorage::Object The object to - * copy. This object MUST already be saved on the remote server. The body of - * the object is not sent. Instead, the copy operation is performed on the - * remote server. You can, and probably should, use a RemoteObject here. - * @param string $newName The new name of this object. If you are copying a - * cross containers, the name can be the same. If you are copying within - * the same container, though, you will need to supply a new name. - * @param string $container The name of the alternate container. If this is - * set, the object will be saved into this container. If this is not sent, - * the copy will be performed inside of the original container. - */ - public function copy(Object $obj, $newName, $container = NULL) { - //$sourceUrl = $obj->url(); // This doesn't work with Object; only with RemoteObject. - $sourceUrl = self::objectUrl($this->url, $obj->name()); - - if (empty($newName)) { - throw new \OpenStack\Exception("An object name is required to copy the object."); + return $this->count; } - // Figure out what container we store in. - if (empty($container)) { - $container = $this->name; - } - $container = rawurlencode($container); - $destUrl = self::objectUrl('/' . $container, $newName); + /** + * Save an Object into Object Storage. + * + * This takes an \OpenStack\Storage\ObjectStorage\Object + * and stores it in the given container in the present + * container on the remote object store. + * + * @param object $obj \OpenStack\Storage\ObjectStorage\Object The object to + * store. + * @param resource $file An optional file argument that, if set, will be + * treated as the contents of the object. + * + * @return boolean TRUE if the object was saved. + * + * @throws \OpenStack\Transport\LengthRequiredException if the Content-Length could not be determined and + * chunked encoding was not enabled. This should not occur + * for this class, which always automatically generates + * Content-Length headers. However, subclasses could + * generate this error. + * @throws \OpenStack\Transport\UnprocessableEntityException if the checksum passed here does not match the checksum + * calculated remotely. + * @throws \OpenStack\Exception when an unexpected (usually network-related) error + * condition arises. + */ + public function save(Object $obj, $file = NULL) + { + if (empty($this->token)) { + throw new \OpenStack\Exception('Container does not have an auth token.'); + } + if (empty($this->url)) { + throw new \OpenStack\Exception('Container does not have a URL to send data.'); + } - $headers = array( - 'X-Auth-Token' => $this->token, - 'Destination' => $destUrl, - 'Content-Type' => $obj->contentType(), - ); + //$url = $this->url . '/' . rawurlencode($obj->name()); + $url = self::objectUrl($this->url, $obj->name()); - $response = $this->client->doRequest($sourceUrl, 'COPY', $headers); + // See if we have any metadata. + $headers = array(); + $md = $obj->metadata(); + if (!empty($md)) { + $headers = self::generateMetadataHeaders($md, Container::METADATA_HEADER_PREFIX); + } - if ($response->getStatusCode() != 201) { - throw new \OpenStack\Exception("An unknown condition occurred during copy. " . $response->getStatusCode()); - } - return TRUE; - } + // Set the content type. + $headers['Content-Type'] = $obj->contentType(); - /** - * Get the object with the given name. - * - * This fetches a single object with the given name. It downloads the - * entire object at once. This is useful if the object is small (under - * a few megabytes) and the content of the object will be used. For - * example, this is the right operation for accessing a text file - * whose contents will be processed. - * - * For larger files or files whose content may never be accessed, use - * remoteObject(), which delays loading the content until one of its - * content methods (e.g. RemoteObject::content()) is called. - * - * This does not yet support the following features of Swift: - * - * - Byte range queries. - * - If-Modified-Since/If-Unmodified-Since - * - If-Match/If-None-Match - * - * @param string $name The name of the object to load. - * - * @return \OpenStack\Storage\ObjectStorage\RemoteObject A remote object with - * the content already stored locally. - */ - public function object($name) { - $url = self::objectUrl($this->url, $name); - $headers = array(); + // Add content encoding, if necessary. + $encoding = $obj->encoding(); + if (!empty($encoding)) { + $headers['Content-Encoding'] = rawurlencode($encoding); + } - // Auth token. - $headers['X-Auth-Token'] = $this->token; + // Add content disposition, if necessary. + $disposition = $obj->disposition(); + if (!empty($disposition)) { + $headers['Content-Disposition'] = $disposition; + } - $response = $this->client->doRequest($url, 'GET', $headers); + // Auth token. + $headers['X-Auth-Token'] = $this->token; - if ($response->getStatusCode() != 200) { - throw new \OpenStack\Exception('An unknown error occurred while saving: ' . $response->status()); + // Add any custom headers: + $moreHeaders = $obj->additionalHeaders(); + if (!empty($moreHeaders)) { + $headers += $moreHeaders; + } + + if (empty($file)) { + // Now build up the rest of the headers: + $headers['Etag'] = $obj->eTag(); + + // If chunked, we set transfer encoding; else + // we set the content length. + if ($obj->isChunked()) { + // How do we handle this? Does the underlying + // stream wrapper pay any attention to this? + $headers['Transfer-Encoding'] = 'chunked'; + } else { + $headers['Content-Length'] = $obj->contentLength(); + } + $response = $this->client->doRequest($url, 'PUT', $headers, $obj->content()); + } else { + // Rewind the file. + rewind($file); + + + // XXX: What do we do about Content-Length header? + //$headers['Transfer-Encoding'] = 'chunked'; + $stat = fstat($file); + $headers['Content-Length'] = $stat['size']; + + // Generate an eTag: + $hash = hash_init('md5'); + hash_update_stream($hash, $file); + $etag = hash_final($hash); + $headers['Etag'] = $etag; + + // Not sure if this is necessary: + rewind($file); + + $response = $this->client->doRequestWithResource($url, 'PUT', $headers, $file); + + } + + if ($response->getStatusCode() != 201) { + throw new \OpenStack\Exception('An unknown error occurred while saving: ' . $response->status()); + } + + return TRUE; } - $remoteObject = RemoteObject::newFromHeaders($name, self::reformatHeaders($response->getHeaders()), $this->token, $url, $this->client); - $remoteObject->setContent($response->getBody()); + /** + * Update an object's metadata. + * + * This updates the metadata on an object without modifying anything + * else. This is a convenient way to set additional metadata without + * having to re-upload a potentially large object. + * + * Swift's behavior during this operation is sometimes unpredictable, + * particularly in cases where custom headers have been set. + * Use with caution. + * + * @param object $obj \OpenStack\Storage\ObjectStorage\Object The object to update. + * + * @return boolean TRUE if the metadata was updated. + * + * @throws \OpenStack\Transport\FileNotFoundException if the object does not already exist on the object storage. + */ + public function updateMetadata(Object $obj) + { + //$url = $this->url . '/' . rawurlencode($obj->name()); + $url = self::objectUrl($this->url, $obj->name()); + $headers = array(); - return $remoteObject; - } + // See if we have any metadata. We post this even if there + // is no metadata. + $md = $obj->metadata(); + if (!empty($md)) { + $headers = self::generateMetadataHeaders($md, Container::METADATA_HEADER_PREFIX); + } + $headers['X-Auth-Token'] = $this->token; - /** - * Fetch an object, but delay fetching its contents. - * - * This retrieves all of the information about an object except for - * its contents. Size, hash, metadata, and modification date - * information are all retrieved and wrapped. - * - * The data comes back as a RemoteObject, which can be used to - * transparently fetch the object's content, too. - * - * Why Use This? - * - * The regular object() call will fetch an entire object, including - * its content. This may not be desireable for cases where the object - * is large. - * - * This method can featch the relevant metadata, but delay fetching - * the content until it is actually needed. - * - * Since RemoteObject extends Object, all of the calls that can be - * made to an Object can also be made to a RemoteObject. Be aware, - * though, that calling RemoteObject::content() will initiate another - * network operation. - * - * @param string $name The name of the object to fetch. - * - * @return \OpenStack\Storage\ObjectStorage\RemoteObject A remote object ready - * for use. - */ - public function proxyObject($name) { - $url = self::objectUrl($this->url, $name); - $headers = array( - 'X-Auth-Token' => $this->token, - ); + // In spite of the documentation's claim to the contrary, + // content type IS reset during this operation. + $headers['Content-Type'] = $obj->contentType(); - $response = $this->client->doRequest($url, 'HEAD', $headers); + // The POST verb is for updating headers. + $response = $this->client->doRequest($url, 'POST', $headers, $obj->content()); - if ($response->getStatusCode() != 200) { - throw new \OpenStack\Exception('An unknown error occurred while saving: ' . $response->status()); + if ($response->getStatusCode() != 202) { + throw new \OpenStack\Exception('An unknown error occurred while saving: ' . $response->status()); + } + + return TRUE; } - $headers = self::reformatHeaders($response->getHeaders()); + /** + * Copy an object to another place in object storage. + * + * An object can be copied within a container. Essentially, this will + * give you duplicates of the file, each with a new name. + * + * An object can be copied to another container if the name of the + * other container is specified, and if that container already exists. + * + * Note that there is no MOVE operation. You must copy and then DELETE + * in order to achieve that. + * + * @param object $obj \OpenStack\Storage\ObjectStorage::Object The object to + * copy. This object MUST already be saved on the remote server. The body of + * the object is not sent. Instead, the copy operation is performed on the + * remote server. You can, and probably should, use a RemoteObject here. + * @param string $newName The new name of this object. If you are copying a + * cross containers, the name can be the same. If you are copying within + * the same container, though, you will need to supply a new name. + * @param string $container The name of the alternate container. If this is + * set, the object will be saved into this container. If this is not sent, + * the copy will be performed inside of the original container. + */ + public function copy(Object $obj, $newName, $container = NULL) + { + //$sourceUrl = $obj->url(); // This doesn't work with Object; only with RemoteObject. + $sourceUrl = self::objectUrl($this->url, $obj->name()); - $obj = RemoteObject::newFromHeaders($name, $headers, $this->token, $url, $this->client); + if (empty($newName)) { + throw new \OpenStack\Exception("An object name is required to copy the object."); + } - return $obj; - } - /** - * This has been replaced with proxyObject(). - * - * @deprecated - */ - public function remoteObject($name) { - return $this->proxyObject($name); - } + // Figure out what container we store in. + if (empty($container)) { + $container = $this->name; + } + $container = rawurlencode($container); + $destUrl = self::objectUrl('/' . $container, $newName); - /** - * Get a list of objects in this container. - * - * This will return a list of objects in the container. With no parameters, it - * will attempt to return a listing of all objects in the container. However, - * by setting contraints, you can retrieve only a specific subset of objects. - * - * Note that OpenStacks Swift will return no more than 10,000 objects - * per request. When dealing with large datasets, you are encouraged - * to use paging. - * - * Paging - * - * Paging is done with a combination of a limit and a marker. The - * limit is an integer indicating the maximum number of items to - * return. The marker is the string name of an object. Typically, this - * is the last object in the previously returned set. The next batch - * will begin with the next item after the marker (assuming the marker - * is found.) - * - * @param int $limit An integer indicating the maximum number of items to - * return. This cannot be greater than the Swift maximum (10k). - * @param string $marker The name of the object to start with. The query will - * begin with the next object AFTER this one. - * - * @return array List of RemoteObject or Subdir instances. - */ - public function objects($limit = NULL, $marker = NULL) { - $params = array(); - return $this->objectQuery($params, $limit, $marker); - } + $headers = array( + 'X-Auth-Token' => $this->token, + 'Destination' => $destUrl, + 'Content-Type' => $obj->contentType(), + ); - /** - * Retrieve a list of Objects with the given prefix. - * - * Object Storage containers support directory-like organization. To - * get a list of items inside of a particular "subdirectory", provide - * the directory name as a "prefix". This will return only objects - * that begin with that prefix. - * - * (Directory-like behavior is also supported by using "directory - * markers". See objectsByPath().) - * - * Prefixes - * - * Prefixes are basically substring patterns that are matched against - * files on the remote object storage. - * - * When a prefix is used, object storage will begin to return not just - * Object instsances, but also Subdir instances. A Subdir is simply a - * container for a "path name". - * - * Delimiters - * - * Object Storage (OpenStack Swift) does not have a native concept of - * files and directories when it comes to paths. Instead, it merely - * represents them and simulates their behavior under specific - * circumstances. - * - * The default behavior (when prefixes are used) is to treat the '/' - * character as a delimiter. Thus, when it encounters a name like - * this: `foo/bar/baz.txt` and the prefix is `foo/`, it will - * parse return a Subdir called `foo/bar`. - * - * Similarly, if you store a file called `foo:bar:baz.txt` and then - * set the delimiter to `:` and the prefix to `foo:`, it will return - * the Subdir `foo:bar`. However, merely setting the delimiter back to - * `/` will not allow you to query `foo/bar` and get the contents of - * `foo:bar`. - * - * Setting $delimiter will tell the Object Storage server which - * character to parse the filenames on. This means that if you use - * delimiters other than '/', you need to be very consistent with your - * usage or else you may get surprising results. - * - * @param string $prefix The leading prefix. - * @param string $delimiter The character used to delimit names. By default, - * this is '/'. - * @param int $limit An integer indicating the maximum number of items to - * return. This cannot be greater than the Swift maximum (10k). - * @param string $marker The name of the object to start with. The query will - * begin with the next object AFTER this one. - * - * @return array List of RemoteObject or Subdir instances. - */ - public function objectsWithPrefix($prefix, $delimiter = '/', $limit = NULL, $marker = NULL) { - $params = array( - 'prefix' => $prefix, - 'delimiter' => $delimiter, - ); - return $this->objectQuery($params, $limit, $marker); - } + $response = $this->client->doRequest($sourceUrl, 'COPY', $headers); - /** - * Specify a path (subdirectory) to traverse. - * - * OpenStack Swift provides two basic ways to handle directory-like - * structures. The first is using a prefix (see objectsWithPrefix()). - * The second is to create directory markers and use a path. - * - * A directory marker is just a file with a name that is - * directory-like. You create it exactly as you create any other file. - * Typically, it is 0 bytes long. - * - * save($dir); - * ?> - * - * Using objectsByPath() with directory markers will return a list of - * Object instances, some of which are regular files, and some of - * which are just empty directory marker files. When creating - * directory markers, you may wish to set metadata or content-type - * information indicating that they are directory markers. - * - * At one point, the OpenStack documentation suggested that the path - * method was legacy. More recent versions of the documentation no - * longer indicate this. - * - * @param string $path The path prefix. - * @param string $delimiter The character used to delimit names. By default, - * this is '/'. - * @param int $limit An integer indicating the maximum number of items to - * return. This cannot be greater than the Swift maximum (10k). - * @param string $marker The name of the object to start with. The query will - * begin with the next object AFTER this one. - */ - public function objectsByPath($path, $delimiter = '/', $limit = NULL, $marker = NULL) { - $params = array( - 'path' => $path, - 'delimiter' => $delimiter, - ); - return $this->objectQuery($params, $limit, $marker); - } + if ($response->getStatusCode() != 201) { + throw new \OpenStack\Exception("An unknown condition occurred during copy. " . $response->getStatusCode()); + } - /** - * Get the URL to this container. - * - * Any container that has been created will have a valid URL. If the - * Container was set to be public (See - * ObjectStorage::createContainer()) will be accessible by this URL. - * - * @return string The URL. - */ - public function url() { - return $this->url; - } - - /** - * Get the ACL. - * - * Currently, if the ACL wasn't added during object construction, - * calling acl() will trigger a request to the remote server to fetch - * the ACL. Since only some Swift calls return ACL data, this is an - * unavoidable artifact. - * - * Calling this on a Container that has not been stored on the remote - * ObjectStorage will produce an error. However, this should not be an - * issue, since containers should always come from one of the - * ObjectStorage methods. - * - * @todo Determine how to get the ACL from JSON data. - * - * @return \OpenStack\Storage\ObjectStorage\ACL An ACL, or NULL if the ACL - * could not be retrieved. - */ - public function acl() { - if (!isset($this->acl)) { - $this->loadExtraData(); - } - return $this->acl; - } - - /** - * Get missing fields. - * - * Not all containers come fully instantiated. This method is sometimes - * called to "fill in" missing fields. - * - * @return \OpenStack\Storage\ObjectStorage\Container - */ - protected function loadExtraData() { - - // If URL and token are empty, we are dealing with - // a local item that has not been saved, and was not - // created with Container::createContainer(). We treat - // this as an error condition. - if (empty($this->url) || empty($this->token)) { - throw new \OpenStack\Exception('Remote data cannot be fetched. Tokena and endpoint URL are required.'); - } - // Do a GET on $url to fetch headers. - $headers = array( - 'X-Auth-Token' => $this->token, - ); - $response = $this->client->doRequest($this->url, 'GET', $headers); - $headers = self::reformatHeaders($response->getHeaders()); - // Get ACL. - $this->acl = ACL::newFromHeaders($headers); - - // Update size and count. - $this->bytes = $response->getHeader('X-Container-Bytes-Used', 0); - $this->count = $response->getHeader('X-Container-Object-Count', 0); - - // Get metadata. - $prefix = Container::CONTAINER_METADATA_HEADER_PREFIX; - $this->setMetadata(Container::extractHeaderAttributes($headers, $prefix)); - - return $this; - } - - /** - * Perform the HTTP query for a list of objects and de-serialize the - * results. - */ - protected function objectQuery($params = array(), $limit = NULL, $marker = NULL) { - if (isset($limit)) { - $params['limit'] = (int) $limit; - if (!empty($marker)) { - $params['marker'] = (string) $marker; - } + return TRUE; } - // We always want JSON. - $params['format'] = 'json'; + /** + * Get the object with the given name. + * + * This fetches a single object with the given name. It downloads the + * entire object at once. This is useful if the object is small (under + * a few megabytes) and the content of the object will be used. For + * example, this is the right operation for accessing a text file + * whose contents will be processed. + * + * For larger files or files whose content may never be accessed, use + * remoteObject(), which delays loading the content until one of its + * content methods (e.g. RemoteObject::content()) is called. + * + * This does not yet support the following features of Swift: + * + * - Byte range queries. + * - If-Modified-Since/If-Unmodified-Since + * - If-Match/If-None-Match + * + * @param string $name The name of the object to load. + * + * @return \OpenStack\Storage\ObjectStorage\RemoteObject A remote object with the content already stored locally. + */ + public function object($name) + { + $url = self::objectUrl($this->url, $name); + $headers = array(); - $query = http_build_query($params); - $query = str_replace('%2F', '/', $query); - $url = $this->url . '?' . $query; + // Auth token. + $headers['X-Auth-Token'] = $this->token; - $headers = array( - 'X-Auth-Token' => $this->token, - ); + $response = $this->client->doRequest($url, 'GET', $headers); - $response = $this->client->doRequest($url, 'GET', $headers); + if ($response->getStatusCode() != 200) { + throw new \OpenStack\Exception('An unknown error occurred while saving: ' . $response->status()); + } - // The only codes that should be returned are 200 and the ones - // already thrown by doRequest. - if ($response->getStatusCode() != 200) { - throw new \OpenStack\Exception('An unknown exception occurred while processing the request.'); + $remoteObject = RemoteObject::newFromHeaders($name, self::reformatHeaders($response->getHeaders()), $this->token, $url, $this->client); + $remoteObject->setContent($response->getBody()); + + return $remoteObject; } - $json = $response->json(); + /** + * Fetch an object, but delay fetching its contents. + * + * This retrieves all of the information about an object except for + * its contents. Size, hash, metadata, and modification date + * information are all retrieved and wrapped. + * + * The data comes back as a RemoteObject, which can be used to + * transparently fetch the object's content, too. + * + * Why Use This? + * + * The regular object() call will fetch an entire object, including + * its content. This may not be desireable for cases where the object + * is large. + * + * This method can featch the relevant metadata, but delay fetching + * the content until it is actually needed. + * + * Since RemoteObject extends Object, all of the calls that can be + * made to an Object can also be made to a RemoteObject. Be aware, + * though, that calling RemoteObject::content() will initiate another + * network operation. + * + * @param string $name The name of the object to fetch. + * + * @return \OpenStack\Storage\ObjectStorage\RemoteObject A remote object ready for use. + */ + public function proxyObject($name) + { + $url = self::objectUrl($this->url, $name); + $headers = array( + 'X-Auth-Token' => $this->token, + ); - // Turn the array into a list of RemoteObject instances. - $list = array(); - foreach ($json as $item) { - if (!empty($item['subdir'])) { - $list[] = new Subdir($item['subdir'], $params['delimiter']); - } - elseif (empty($item['name'])) { - throw new \OpenStack\Exception('Unexpected entity returned.'); - } - else { - //$url = $this->url . '/' . rawurlencode($item['name']); - $url = self::objectUrl($this->url, $item['name']); - $list[] = RemoteObject::newFromJSON($item, $this->token, $url, $this->client); - } + $response = $this->client->doRequest($url, 'HEAD', $headers); + + if ($response->getStatusCode() != 200) { + throw new \OpenStack\Exception('An unknown error occurred while saving: ' . $response->status()); + } + + $headers = self::reformatHeaders($response->getHeaders()); + + $obj = RemoteObject::newFromHeaders($name, $headers, $this->token, $url, $this->client); + + return $obj; + } + /** + * This has been replaced with proxyObject(). + * + * @deprecated + */ + public function remoteObject($name) + { + return $this->proxyObject($name); } - return $list; - } + /** + * Get a list of objects in this container. + * + * This will return a list of objects in the container. With no parameters, it + * will attempt to return a listing of all objects in the container. However, + * by setting contraints, you can retrieve only a specific subset of objects. + * + * Note that OpenStacks Swift will return no more than 10,000 objects + * per request. When dealing with large datasets, you are encouraged + * to use paging. + * + * Paging + * + * Paging is done with a combination of a limit and a marker. The + * limit is an integer indicating the maximum number of items to + * return. The marker is the string name of an object. Typically, this + * is the last object in the previously returned set. The next batch + * will begin with the next item after the marker (assuming the marker + * is found.) + * + * @param int $limit An integer indicating the maximum number of items to + * return. This cannot be greater than the Swift maximum (10k). + * @param string $marker The name of the object to start with. The query will + * begin with the next object AFTER this one. + * + * @return array List of RemoteObject or Subdir instances. + */ + public function objects($limit = NULL, $marker = NULL) + { + $params = array(); - /** - * Return the iterator of contents. - * - * A Container is Iterable. This means that you can use a container in - * a `foreach` loop directly: - * - * name(); - * } - * ?> - * - * The above is equivalent to doing the following: - * - * objects(); - * foreach ($objects as $object) { - * print $object->name(); - * } - * ?> - * - * Note that there is no way to pass any constraints into an iterator. - * You cannot limit the number of items, set an marker, or add a - * prefix. - */ - public function getIterator() { - return new \ArrayIterator($this->objects()); - } - - /** - * Remove the named object from storage. - * - * @param string $name The name of the object to remove. - * - * @return boolean TRUE if the file was deleted, FALSE if no such file is - * found. - */ - public function delete($name) { - $url = self::objectUrl($this->url, $name); - $headers = array( - 'X-Auth-Token' => $this->token, - ); - - try { - $response = $this->client->doRequest($url, 'DELETE', $headers); - } - catch (\OpenStack\Transport\FileNotFoundException $fnfe) { - return FALSE; + return $this->objectQuery($params, $limit, $marker); } - if ($response->getStatusCode() != 204) { - throw new \OpenStack\Exception("An unknown exception occured while deleting $name."); + /** + * Retrieve a list of Objects with the given prefix. + * + * Object Storage containers support directory-like organization. To + * get a list of items inside of a particular "subdirectory", provide + * the directory name as a "prefix". This will return only objects + * that begin with that prefix. + * + * (Directory-like behavior is also supported by using "directory + * markers". See objectsByPath().) + * + * Prefixes + * + * Prefixes are basically substring patterns that are matched against + * files on the remote object storage. + * + * When a prefix is used, object storage will begin to return not just + * Object instsances, but also Subdir instances. A Subdir is simply a + * container for a "path name". + * + * Delimiters + * + * Object Storage (OpenStack Swift) does not have a native concept of + * files and directories when it comes to paths. Instead, it merely + * represents them and simulates their behavior under specific + * circumstances. + * + * The default behavior (when prefixes are used) is to treat the '/' + * character as a delimiter. Thus, when it encounters a name like + * this: `foo/bar/baz.txt` and the prefix is `foo/`, it will + * parse return a Subdir called `foo/bar`. + * + * Similarly, if you store a file called `foo:bar:baz.txt` and then + * set the delimiter to `:` and the prefix to `foo:`, it will return + * the Subdir `foo:bar`. However, merely setting the delimiter back to + * `/` will not allow you to query `foo/bar` and get the contents of + * `foo:bar`. + * + * Setting $delimiter will tell the Object Storage server which + * character to parse the filenames on. This means that if you use + * delimiters other than '/', you need to be very consistent with your + * usage or else you may get surprising results. + * + * @param string $prefix The leading prefix. + * @param string $delimiter The character used to delimit names. By default, + * this is '/'. + * @param int $limit An integer indicating the maximum number of items to + * return. This cannot be greater than the Swift maximum (10k). + * @param string $marker The name of the object to start with. The query will + * begin with the next object AFTER this one. + * + * @return array List of RemoteObject or Subdir instances. + */ + public function objectsWithPrefix($prefix, $delimiter = '/', $limit = NULL, $marker = NULL) + { + $params = array( + 'prefix' => $prefix, + 'delimiter' => $delimiter, + ); + + return $this->objectQuery($params, $limit, $marker); } - return TRUE; - } + /** + * Specify a path (subdirectory) to traverse. + * + * OpenStack Swift provides two basic ways to handle directory-like + * structures. The first is using a prefix (see objectsWithPrefix()). + * The second is to create directory markers and use a path. + * + * A directory marker is just a file with a name that is + * directory-like. You create it exactly as you create any other file. + * Typically, it is 0 bytes long. + * + * save($dir); + * ?> + * + * Using objectsByPath() with directory markers will return a list of + * Object instances, some of which are regular files, and some of + * which are just empty directory marker files. When creating + * directory markers, you may wish to set metadata or content-type + * information indicating that they are directory markers. + * + * At one point, the OpenStack documentation suggested that the path + * method was legacy. More recent versions of the documentation no + * longer indicate this. + * + * @param string $path The path prefix. + * @param string $delimiter The character used to delimit names. By default, + * this is '/'. + * @param int $limit An integer indicating the maximum number of items to + * return. This cannot be greater than the Swift maximum (10k). + * @param string $marker The name of the object to start with. The query will + * begin with the next object AFTER this one. + */ + public function objectsByPath($path, $delimiter = '/', $limit = NULL, $marker = NULL) + { + $params = array( + 'path' => $path, + 'delimiter' => $delimiter, + ); - /** - * Reformat the headers array to remove a nested array. - * - * For example, headers coming in could be in the format: - * - * $headers = [ - * 'Content-Type' => [ - * [0] => 'Foo', - * ], - * ]; - * - * This method would reformat the array into: - * - * $headers = [ - * 'Content-Type' => 'Foo', - * ]; - * - * Note, for cases where multiple values for a header are needed this method - * should not be used. - * - * @param array $headers A headers array from the response. - * @return array A new shallower array. - */ - public static function reformatHeaders(array $headers) { - $newHeaders = []; - - foreach ($headers as $name => $header) { - $newHeaders[$name] = $header[0]; + return $this->objectQuery($params, $limit, $marker); } - return $newHeaders; - } + /** + * Get the URL to this container. + * + * Any container that has been created will have a valid URL. If the + * Container was set to be public (See + * ObjectStorage::createContainer()) will be accessible by this URL. + * + * @return string The URL. + */ + public function url() + { + return $this->url; + } + + /** + * Get the ACL. + * + * Currently, if the ACL wasn't added during object construction, + * calling acl() will trigger a request to the remote server to fetch + * the ACL. Since only some Swift calls return ACL data, this is an + * unavoidable artifact. + * + * Calling this on a Container that has not been stored on the remote + * ObjectStorage will produce an error. However, this should not be an + * issue, since containers should always come from one of the + * ObjectStorage methods. + * + * @todo Determine how to get the ACL from JSON data. + * + * @return \OpenStack\Storage\ObjectStorage\ACL An ACL, or NULL if the ACL could not be retrieved. + */ + public function acl() + { + if (!isset($this->acl)) { + $this->loadExtraData(); + } + + return $this->acl; + } + + /** + * Get missing fields. + * + * Not all containers come fully instantiated. This method is sometimes + * called to "fill in" missing fields. + * + * @return \OpenStack\Storage\ObjectStorage\Container + */ + protected function loadExtraData() + { + // If URL and token are empty, we are dealing with + // a local item that has not been saved, and was not + // created with Container::createContainer(). We treat + // this as an error condition. + if (empty($this->url) || empty($this->token)) { + throw new \OpenStack\Exception('Remote data cannot be fetched. Tokena and endpoint URL are required.'); + } + // Do a GET on $url to fetch headers. + $headers = array( + 'X-Auth-Token' => $this->token, + ); + $response = $this->client->doRequest($this->url, 'GET', $headers); + $headers = self::reformatHeaders($response->getHeaders()); + // Get ACL. + $this->acl = ACL::newFromHeaders($headers); + + // Update size and count. + $this->bytes = $response->getHeader('X-Container-Bytes-Used', 0); + $this->count = $response->getHeader('X-Container-Object-Count', 0); + + // Get metadata. + $prefix = Container::CONTAINER_METADATA_HEADER_PREFIX; + $this->setMetadata(Container::extractHeaderAttributes($headers, $prefix)); + + return $this; + } + + /** + * Perform the HTTP query for a list of objects and de-serialize the + * results. + */ + protected function objectQuery($params = array(), $limit = NULL, $marker = NULL) + { + if (isset($limit)) { + $params['limit'] = (int) $limit; + if (!empty($marker)) { + $params['marker'] = (string) $marker; + } + } + + // We always want JSON. + $params['format'] = 'json'; + + $query = http_build_query($params); + $query = str_replace('%2F', '/', $query); + $url = $this->url . '?' . $query; + + $headers = array( + 'X-Auth-Token' => $this->token, + ); + + $response = $this->client->doRequest($url, 'GET', $headers); + + // The only codes that should be returned are 200 and the ones + // already thrown by doRequest. + if ($response->getStatusCode() != 200) { + throw new \OpenStack\Exception('An unknown exception occurred while processing the request.'); + } + + $json = $response->json(); + + // Turn the array into a list of RemoteObject instances. + $list = array(); + foreach ($json as $item) { + if (!empty($item['subdir'])) { + $list[] = new Subdir($item['subdir'], $params['delimiter']); + } elseif (empty($item['name'])) { + throw new \OpenStack\Exception('Unexpected entity returned.'); + } else { + //$url = $this->url . '/' . rawurlencode($item['name']); + $url = self::objectUrl($this->url, $item['name']); + $list[] = RemoteObject::newFromJSON($item, $this->token, $url, $this->client); + } + } + + return $list; + } + + /** + * Return the iterator of contents. + * + * A Container is Iterable. This means that you can use a container in + * a `foreach` loop directly: + * + * name(); + * } + * ?> + * + * The above is equivalent to doing the following: + * + * objects(); + * foreach ($objects as $object) { + * print $object->name(); + * } + * ?> + * + * Note that there is no way to pass any constraints into an iterator. + * You cannot limit the number of items, set an marker, or add a + * prefix. + */ + public function getIterator() + { + return new \ArrayIterator($this->objects()); + } + + /** + * Remove the named object from storage. + * + * @param string $name The name of the object to remove. + * + * @return boolean TRUE if the file was deleted, FALSE if no such file is + * found. + */ + public function delete($name) + { + $url = self::objectUrl($this->url, $name); + $headers = array( + 'X-Auth-Token' => $this->token, + ); + + try { + $response = $this->client->doRequest($url, 'DELETE', $headers); + } catch (\OpenStack\Transport\FileNotFoundException $fnfe) { + return FALSE; + } + + if ($response->getStatusCode() != 204) { + throw new \OpenStack\Exception("An unknown exception occured while deleting $name."); + } + + return TRUE; + } + + /** + * Reformat the headers array to remove a nested array. + * + * For example, headers coming in could be in the format: + * + * $headers = [ + * 'Content-Type' => [ + * [0] => 'Foo', + * ], + * ]; + * + * This method would reformat the array into: + * + * $headers = [ + * 'Content-Type' => 'Foo', + * ]; + * + * Note, for cases where multiple values for a header are needed this method + * should not be used. + * + * @param array $headers A headers array from the response. + * + * @return array A new shallower array. + */ + public static function reformatHeaders(array $headers) + { + $newHeaders = []; + + foreach ($headers as $name => $header) { + $newHeaders[$name] = $header[0]; + } + + return $newHeaders; + } } diff --git a/src/OpenStack/Storage/ObjectStorage/ContainerNotEmptyException.php b/src/OpenStack/Storage/ObjectStorage/ContainerNotEmptyException.php index a7fab18..6991ffb 100644 --- a/src/OpenStack/Storage/ObjectStorage/ContainerNotEmptyException.php +++ b/src/OpenStack/Storage/ObjectStorage/ContainerNotEmptyException.php @@ -2,17 +2,17 @@ /* ============================================================================ (c) Copyright 2012-2014 Hewlett-Packard Development Company, L.P. - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at - http://www.apache.org/licenses/LICENSE-2.0 + http://www.apache.org/licenses/LICENSE-2.0 - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. ============================================================================ */ /** * Contains exception class for ContainerNotEmptyException. diff --git a/src/OpenStack/Storage/ObjectStorage/ContentVerificationException.php b/src/OpenStack/Storage/ObjectStorage/ContentVerificationException.php index 0061e45..da24032 100644 --- a/src/OpenStack/Storage/ObjectStorage/ContentVerificationException.php +++ b/src/OpenStack/Storage/ObjectStorage/ContentVerificationException.php @@ -2,17 +2,17 @@ /* ============================================================================ (c) Copyright 2012-2014 Hewlett-Packard Development Company, L.P. - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at - http://www.apache.org/licenses/LICENSE-2.0 + http://www.apache.org/licenses/LICENSE-2.0 - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. ============================================================================ */ /** * Contains the ContentVerificationException object. diff --git a/src/OpenStack/Storage/ObjectStorage/Object.php b/src/OpenStack/Storage/ObjectStorage/Object.php index 5c8bf1c..81fd1e4 100644 --- a/src/OpenStack/Storage/ObjectStorage/Object.php +++ b/src/OpenStack/Storage/ObjectStorage/Object.php @@ -2,17 +2,17 @@ /* ============================================================================ (c) Copyright 2012-2014 Hewlett-Packard Development Company, L.P. - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at - http://www.apache.org/licenses/LICENSE-2.0 + http://www.apache.org/licenses/LICENSE-2.0 - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. ============================================================================ */ /** * Contains the class Object for ObjectStorage. @@ -46,457 +46,480 @@ namespace OpenStack\Storage\ObjectStorage; * Likewise, a Container instance can retrieve Object instances from the * remote object store. */ -class Object { +class Object +{ + const DEFAULT_CONTENT_TYPE = 'application/octet-stream'; - const DEFAULT_CONTENT_TYPE = 'application/octet-stream'; + /** + * The name of the object. + * + * This can be path-like, subject to OpenStack's definition + * of "path-like". + */ + protected $name; - /** - * The name of the object. - * - * This can be path-like, subject to OpenStack's definition - * of "path-like". - */ - protected $name; + /** + * The content. + * + * Subclasses needn't use this to store an object's content, + * as they may prefer filesystem backing. + */ + protected $content; - /** - * The content. - * - * Subclasses needn't use this to store an object's content, - * as they may prefer filesystem backing. - */ - protected $content; + /** + * The content type. + * + * The default type is 'application/octet-stream', which marks this as + * a generic byte stream. + */ + protected $contentType = self::DEFAULT_CONTENT_TYPE; - /** - * The content type. - * - * The default type is 'application/octet-stream', which marks this as - * a generic byte stream. - */ - protected $contentType = self::DEFAULT_CONTENT_TYPE; + /** + * Associative array of stored metadata. + */ + protected $metadata = array(); - /** - * Associative array of stored metadata. - */ - protected $metadata = array(); + protected $contentEncoding; + protected $contentDisposition; - protected $contentEncoding; - protected $contentDisposition; + /** + * Extension mechanism for new headers. + */ + protected $additionalHeaders = array(); - /** - * Extension mechanism for new headers. - */ - protected $additionalHeaders = array(); + /** + * Construct a new object for storage. + * + * @param string $name A name (may be pathlike) for the object. + * @param string $content Optional content to store in this object. This is + * the same as calling setContent(). + * @param string $type Optional content type for this content. This is the + * same as calling setContentType(). + */ + public function __construct($name, $content = NULL, $type = NULL) + { + $this->name = $name; - /** - * Construct a new object for storage. - * - * @param string $name A name (may be pathlike) for the object. - * @param string $content Optional content to store in this object. This is - * the same as calling setContent(). - * @param string $type Optional content type for this content. This is the - * same as calling setContentType(). - */ - public function __construct($name, $content = NULL, $type = NULL) { - $this->name = $name; - - if (!is_null($content)) { - $this->content = $content; - } - if (!empty($type)) { - $this->contentType = $type; - } - } - - /** - * Set the metadata. - * - * OpenStack allows you to specify metadata for a file. Metadata items - * must follow these conventions: - * - * - names must contain only letters, numbers, and short dashes. Since - * OpenStack normalizes the name to begin with uppercase, it is - * suggested that you follow this convetion: Foo, not foo. Or you - * can do your own normalizing (such as converting all to lowercase. - * OpenStack limits the name length to 126 unicode chars. - * - values must be encoded if they contain newlines or binary data. - * While the exact encoding is up to you, Base-64 encoding is probably - * your best bet. OpenStack limits the value to 256 unicode chars. - * - * (The docs are ambiguous -- they say chars, but they may mean - * bytes.) - * - * This library does only minimal processing of metadata, and does no - * error checking, escaping, etc. This is up to the implementor. The - * OpenStack Swift implementation does not dictate what encoding is - * used, though it suggests url encoding of both name and values. - * - * Currently, no length checking is performed in the library, nor is - * any encoding of the data performed. - * - * IMPORTANT: Current versions of OpenStack Swift normalize metadata - * names so that the name is always given an initial capital leter. - * That is, `foo` becomes `Foo`. - * - * @param array $array An associative array of metadata names to values. - * - * @return \OpenStack\Storage\ObjectStorage\Object $this so the method can be - * used in chaining. - */ - public function setMetadata(array $array) { - $this->metadata = $array; - - return $this; - } - - /** - * Get any associated metadata. - * - * This returns an associative array of all metadata for this object. - * - * @return array An associative array of metadata. This may be empty. - */ - public function metadata() { - return $this->metadata; - } - - /** - * Override (change) the name of an object. - * - * Note that this changes only the local copy of an object. It - * does not rename the remote copy. In fact, changing the local name - * and then saving it will result in a new object being created in the - * object store. - * - * To copy an object: - * @see \OpenStack\Storage\ObjectStorage\Container::copyObject(). - * - * @param string $name A file or object name. - * - * @return \OpenStack\Storage\ObjectStorage\Object $this so the method can be - * used in chaining. - */ - public function setName($name) { - $this->name = $name; - return $this; - } - - /** - * Get the name. - * - * Returns the name of an object. If the name has been overwritten - * using setName(), this will return the latest (overwritten) name. - * - * @return string The name of the object. - */ - public function name() { - return $this->name; - } - - /** - * Set the content type (MIME type) for the object. - * - * Object storage is, to a certain degree, content-type aware. For - * that reason, a content type is mandatory. - * - * The default MIME type used is `application/octet-stream`, which is - * the generic content type for a byte stream. Where possible, you - * should set a more accurate content type. - * - * All HTTP type options are allowed. So, for example, you can add a - * charset to a text type: - * - * setContentType('text/html; charset=iso-8859-13'); - * ?> - * - * Content type is not parsed or verified locally (though it is - * remotely). It can be dangerous, too, to allow users to specify a - * content type. - * - * @param string $type A valid content type. - * - * @return \OpenStack\Storage\ObjectStorage\Object $this so the method can be - * used in chaining. - */ - public function setContentType($type) { - $this->contentType = $type; - return $this; - } - - /** - * Get the content type. - * - * This returns the currently set content type. - * - * @return string The content type, including any additional options. - */ - public function contentType() { - return $this->contentType; - } - - /** - * Set the content for this object. - * - * Place the content into the object. Typically, this is string - * content that will be stored remotely. - * - * PHP's string is backed by a robust system that can accomodate - * moderately sized files. However, it is best to keep strings short - * (<2MB, for example -- test for your own system's sweet spot). - * Larger data may be better handled with file system entries or - * database storage. - * - * Note that the OpenStack will not allow files larger than 5G, and - * PHP will likely croak well before that marker. So use discretion. - * - * @param string $content The content of the object. - * @param string $type The content type (MIME type). This can be set here for - * convenience, or you can call setContentType() directly. - * - * @return \OpenStack\Storage\ObjectStorage\Object $this so the method can be - * used in chaining. - */ - public function setContent($content, $type = NULL) { - $this->content = $content; - if (!empty($type)) { - $this->contentType = $type; - } - return $this; - } - - /** - * Retrieve the content. - * - * Retrieve the ENTIRE content of an object. - * - * Note that this may be binary data (depending on what the original - * content is). PHP strings are generally binary safe, but use this - * with caution if you do not know what kind of data is stored in an - * object. - * - * OpenStack does not do anything to validate that the content type is - * accurate. While contentType() is intended to provide useful - * information, poorly managed data can be written with the wrong - * content type. - * - * When extending this class, you should make sure that this function - * returns the entire contents of an object. - * - * @return string The content of the file. - */ - public function content() { - return $this->content; - } - - /** - * Calculate the content length. - * - * This returns the number of bytes in a piece of content (not - * the number of characters). Among other things, it is used to let - * the remote object store know how big of an object to expect when - * transmitting data. - * - * When extending this class, you should make sure to calculate the - * content length appropriately. - * - * @return int The length of the content, in bytes. - */ - public function contentLength() { - // strlen() is binary safe (or at least it seems to be). - return strlen($this->content); - } - - /** - * Generate an ETag for the ObjectStorage server. - * - * OpenStack uses ETag to pass validation data. This generates an ETag - * using an MD5 hash of the content. - * - * When extending this class, generate an ETag by creating an MD5 of - * the entire object's content (but not the metadata or name). - * - * @return string An MD5 value as a string of 32 hex digits (0-9a-f). - */ - public function eTag() { - return md5($this->content); - } - - /** - * Set the encoding for a file. - * - * You can use content encoding on compressed content to indicate to - * the receiving agent that a file is encoded using a specific - * compression type. - * - * Typical compression types are 'gzip', 'zip', and 'compress', though - * many others exist. - * - * This allows you, for example, to save a zipped file, yet preserve - * its underlying content type. For example, for a gzipped text/plain - * file, you can set the content type to "text/plain" and the encoding - * to "gzip". This allows many user agents to receive the compressed - * data and automatically decompress them and display them correctly. - * - * @param string $encoding A valid encoding type. - * - * @return \OpenStack\Storage\ObjectStorage\Object $this so the method can be - * used in chaining. - */ - public function setEncoding($encoding) { - $this->contentEncoding = $encoding; - - return $this; - } - - /** - * Get the encoding (if any) for this object. - * - * Encoding is used to indicate how a file was encoded or compressed. - * See setEncoding() for more information. - * - * @return string The encoding type. - */ - public function encoding() { - return $this->contentEncoding; - } - - /** - * Set the content disposition. - * - * This makes it possible to have the file act like a download (in a - * browser or similar agent), even if the MIME type normally triggers - * a display. - * - * The typical value for this is: - * - * setDisposition('attachment; filename=foo.png'); - * ?> - * - * A disposition string should not include any newline characters or - * binary data. - * - * @param string $disposition A valid disposition declaration. These are - * defined in various HTTP specifications. - * - * @return \OpenStack\Storage\ObjectStorage\Object $this so the method can be - * used in chaining. - */ - public function setDisposition($disposition) { - $this->contentDisposition = $disposition; - - return $this; - } - - /** - * Get the current disposition string, if any. - * - * See setDisposition() for discussion. - * - * @return string The disposition string, or NULL if none is set. - */ - public function disposition() { - return $this->contentDisposition; - } - - /** - * Set additional headers for storage. - * - * EXPERT: You will need to understand OpenStack internals to use this - * effectively. - * - * Headers set here will be added to the HTTP request during save - * operations. They are not merged into existing headers until - * save-time. - * - * This provides a mechanism for adding extension headers. CORS - * headers and possibly others are stored by Swift, but have no - * semantic value to Swift or to popular user agents. - * - * There are a few things to note about this mechanism: - * - * - Existing headers cannot be overwritten. Only new headers can be - * added. - * - Headers are not merged. They are simply sent to the remote - * server. A new object must be retrieved from the server before - * these headers will be accessible. - * - Swift only stores certain headers. If you supply an unrecognized - * header to Swift, it may simply ignore it. - * - The RemoteObject::headers() method provides access to all of the - * headers returned from Swift. - * - Headers are merged in as they are, with no cleaning, encoding, or - * checking. You must ensure that the headers are in the proper - * format. - * - * @param array $headers An associative array where each name is an HTTP - * header name, and each value is the HTTP header value. No encoding or - * escaping is done. - * - * @return \OpenStack\Storage\ObjectStorage\Object $this so the method can be - * used in chaining. - */ - public function setAdditionalHeaders($headers) { - $this->additionalHeaders = $headers; - return $this; - } - - /** - * Return additional headers. - * - * Headers here have likely not been stored remotely until - * Container::save() is called on the object. - */ - public function additionalHeaders() { - return $this->additionalHeaders; - } - - /** - * Remove headers. - * - * This takes an array of header names, and removes - * any matching headers. Typically, only headers set - * by setAdditionalHeaders() are removed from an Object. - * (RemoteObject works differently). - * - * Many headers are generated automatically, such as - * Content-Type and Content-Length. Removing these - * will simply result in their being regenerated. - * - * @param array $keys The header names to be removed. - * - * @return \OpenStack\Storage\ObjectStorage\Object $this for the current - * object so it can be used in chaining methods. - */ - public function removeHeaders($keys) { - foreach ($keys as $k) { - unset($this->additionalHeaders[$k]); + if (!is_null($content)) { + $this->content = $content; + } + if (!empty($type)) { + $this->contentType = $type; + } } - return $this; - } + /** + * Set the metadata. + * + * OpenStack allows you to specify metadata for a file. Metadata items + * must follow these conventions: + * + * - names must contain only letters, numbers, and short dashes. Since + * OpenStack normalizes the name to begin with uppercase, it is + * suggested that you follow this convetion: Foo, not foo. Or you + * can do your own normalizing (such as converting all to lowercase. + * OpenStack limits the name length to 126 unicode chars. + * - values must be encoded if they contain newlines or binary data. + * While the exact encoding is up to you, Base-64 encoding is probably + * your best bet. OpenStack limits the value to 256 unicode chars. + * + * (The docs are ambiguous -- they say chars, but they may mean + * bytes.) + * + * This library does only minimal processing of metadata, and does no + * error checking, escaping, etc. This is up to the implementor. The + * OpenStack Swift implementation does not dictate what encoding is + * used, though it suggests url encoding of both name and values. + * + * Currently, no length checking is performed in the library, nor is + * any encoding of the data performed. + * + * IMPORTANT: Current versions of OpenStack Swift normalize metadata + * names so that the name is always given an initial capital leter. + * That is, `foo` becomes `Foo`. + * + * @param array $array An associative array of metadata names to values. + * + * @return \OpenStack\Storage\ObjectStorage\Object $this so the method can be + * used in chaining. + */ + public function setMetadata(array $array) + { + $this->metadata = $array; - /** - * This object should be transmitted in chunks. - * - * Indicates whether or not this object should be transmitted as - * chunked data (in HTTP). - * - * This should be used when (a) the file size is large, or (b) the - * exact size of the file is unknown. - * - * If this returns TRUE, it does not guarantee that the data - * will be transmitted in chunks. But it recommends that the - * underlying transport layer use chunked encoding. - * - * The contentLength() method is not called for chunked transfers. So - * if this returns TRUE, contentLength() is ignored. - * - * @return boolean TRUE to recommend chunked transfer, FALSE otherwise. - */ - public function isChunked() { - // Currently, this value is hard-coded. The default Object - // implementation does not get chunked. - return FALSE; - } + return $this; + } + + /** + * Get any associated metadata. + * + * This returns an associative array of all metadata for this object. + * + * @return array An associative array of metadata. This may be empty. + */ + public function metadata() + { + return $this->metadata; + } + + /** + * Override (change) the name of an object. + * + * Note that this changes only the local copy of an object. It + * does not rename the remote copy. In fact, changing the local name + * and then saving it will result in a new object being created in the + * object store. + * + * To copy an object: + * @see \OpenStack\Storage\ObjectStorage\Container::copyObject(). + * + * @param string $name A file or object name. + * + * @return \OpenStack\Storage\ObjectStorage\Object $this so the method can be + * used in chaining. + */ + public function setName($name) + { + $this->name = $name; + + return $this; + } + + /** + * Get the name. + * + * Returns the name of an object. If the name has been overwritten + * using setName(), this will return the latest (overwritten) name. + * + * @return string The name of the object. + */ + public function name() + { + return $this->name; + } + + /** + * Set the content type (MIME type) for the object. + * + * Object storage is, to a certain degree, content-type aware. For + * that reason, a content type is mandatory. + * + * The default MIME type used is `application/octet-stream`, which is + * the generic content type for a byte stream. Where possible, you + * should set a more accurate content type. + * + * All HTTP type options are allowed. So, for example, you can add a + * charset to a text type: + * + * setContentType('text/html; charset=iso-8859-13'); + * ?> + * + * Content type is not parsed or verified locally (though it is + * remotely). It can be dangerous, too, to allow users to specify a + * content type. + * + * @param string $type A valid content type. + * + * @return \OpenStack\Storage\ObjectStorage\Object $this so the method can be + * used in chaining. + */ + public function setContentType($type) + { + $this->contentType = $type; + + return $this; + } + + /** + * Get the content type. + * + * This returns the currently set content type. + * + * @return string The content type, including any additional options. + */ + public function contentType() + { + return $this->contentType; + } + + /** + * Set the content for this object. + * + * Place the content into the object. Typically, this is string + * content that will be stored remotely. + * + * PHP's string is backed by a robust system that can accomodate + * moderately sized files. However, it is best to keep strings short + * (<2MB, for example -- test for your own system's sweet spot). + * Larger data may be better handled with file system entries or + * database storage. + * + * Note that the OpenStack will not allow files larger than 5G, and + * PHP will likely croak well before that marker. So use discretion. + * + * @param string $content The content of the object. + * @param string $type The content type (MIME type). This can be set here for + * convenience, or you can call setContentType() directly. + * + * @return \OpenStack\Storage\ObjectStorage\Object $this so the method can be + * used in chaining. + */ + public function setContent($content, $type = NULL) + { + $this->content = $content; + if (!empty($type)) { + $this->contentType = $type; + } + + return $this; + } + + /** + * Retrieve the content. + * + * Retrieve the ENTIRE content of an object. + * + * Note that this may be binary data (depending on what the original + * content is). PHP strings are generally binary safe, but use this + * with caution if you do not know what kind of data is stored in an + * object. + * + * OpenStack does not do anything to validate that the content type is + * accurate. While contentType() is intended to provide useful + * information, poorly managed data can be written with the wrong + * content type. + * + * When extending this class, you should make sure that this function + * returns the entire contents of an object. + * + * @return string The content of the file. + */ + public function content() + { + return $this->content; + } + + /** + * Calculate the content length. + * + * This returns the number of bytes in a piece of content (not + * the number of characters). Among other things, it is used to let + * the remote object store know how big of an object to expect when + * transmitting data. + * + * When extending this class, you should make sure to calculate the + * content length appropriately. + * + * @return int The length of the content, in bytes. + */ + public function contentLength() + { + // strlen() is binary safe (or at least it seems to be). + return strlen($this->content); + } + + /** + * Generate an ETag for the ObjectStorage server. + * + * OpenStack uses ETag to pass validation data. This generates an ETag + * using an MD5 hash of the content. + * + * When extending this class, generate an ETag by creating an MD5 of + * the entire object's content (but not the metadata or name). + * + * @return string An MD5 value as a string of 32 hex digits (0-9a-f). + */ + public function eTag() + { + return md5($this->content); + } + + /** + * Set the encoding for a file. + * + * You can use content encoding on compressed content to indicate to + * the receiving agent that a file is encoded using a specific + * compression type. + * + * Typical compression types are 'gzip', 'zip', and 'compress', though + * many others exist. + * + * This allows you, for example, to save a zipped file, yet preserve + * its underlying content type. For example, for a gzipped text/plain + * file, you can set the content type to "text/plain" and the encoding + * to "gzip". This allows many user agents to receive the compressed + * data and automatically decompress them and display them correctly. + * + * @param string $encoding A valid encoding type. + * + * @return \OpenStack\Storage\ObjectStorage\Object $this so the method can be + * used in chaining. + */ + public function setEncoding($encoding) + { + $this->contentEncoding = $encoding; + + return $this; + } + + /** + * Get the encoding (if any) for this object. + * + * Encoding is used to indicate how a file was encoded or compressed. + * See setEncoding() for more information. + * + * @return string The encoding type. + */ + public function encoding() + { + return $this->contentEncoding; + } + + /** + * Set the content disposition. + * + * This makes it possible to have the file act like a download (in a + * browser or similar agent), even if the MIME type normally triggers + * a display. + * + * The typical value for this is: + * + * setDisposition('attachment; filename=foo.png'); + * ?> + * + * A disposition string should not include any newline characters or + * binary data. + * + * @param string $disposition A valid disposition declaration. These are + * defined in various HTTP specifications. + * + * @return \OpenStack\Storage\ObjectStorage\Object $this so the method can be + * used in chaining. + */ + public function setDisposition($disposition) + { + $this->contentDisposition = $disposition; + + return $this; + } + + /** + * Get the current disposition string, if any. + * + * See setDisposition() for discussion. + * + * @return string The disposition string, or NULL if none is set. + */ + public function disposition() + { + return $this->contentDisposition; + } + + /** + * Set additional headers for storage. + * + * EXPERT: You will need to understand OpenStack internals to use this + * effectively. + * + * Headers set here will be added to the HTTP request during save + * operations. They are not merged into existing headers until + * save-time. + * + * This provides a mechanism for adding extension headers. CORS + * headers and possibly others are stored by Swift, but have no + * semantic value to Swift or to popular user agents. + * + * There are a few things to note about this mechanism: + * + * - Existing headers cannot be overwritten. Only new headers can be + * added. + * - Headers are not merged. They are simply sent to the remote + * server. A new object must be retrieved from the server before + * these headers will be accessible. + * - Swift only stores certain headers. If you supply an unrecognized + * header to Swift, it may simply ignore it. + * - The RemoteObject::headers() method provides access to all of the + * headers returned from Swift. + * - Headers are merged in as they are, with no cleaning, encoding, or + * checking. You must ensure that the headers are in the proper + * format. + * + * @param array $headers An associative array where each name is an HTTP + * header name, and each value is the HTTP header value. No encoding or + * escaping is done. + * + * @return \OpenStack\Storage\ObjectStorage\Object $this so the method can be + * used in chaining. + */ + public function setAdditionalHeaders($headers) + { + $this->additionalHeaders = $headers; + + return $this; + } + + /** + * Return additional headers. + * + * Headers here have likely not been stored remotely until + * Container::save() is called on the object. + */ + public function additionalHeaders() + { + return $this->additionalHeaders; + } + + /** + * Remove headers. + * + * This takes an array of header names, and removes + * any matching headers. Typically, only headers set + * by setAdditionalHeaders() are removed from an Object. + * (RemoteObject works differently). + * + * Many headers are generated automatically, such as + * Content-Type and Content-Length. Removing these + * will simply result in their being regenerated. + * + * @param array $keys The header names to be removed. + * + * @return \OpenStack\Storage\ObjectStorage\Object $this for the current + * object so it can be used in chaining methods. + */ + public function removeHeaders($keys) + { + foreach ($keys as $k) { + unset($this->additionalHeaders[$k]); + } + + return $this; + } + + /** + * This object should be transmitted in chunks. + * + * Indicates whether or not this object should be transmitted as + * chunked data (in HTTP). + * + * This should be used when (a) the file size is large, or (b) the + * exact size of the file is unknown. + * + * If this returns TRUE, it does not guarantee that the data + * will be transmitted in chunks. But it recommends that the + * underlying transport layer use chunked encoding. + * + * The contentLength() method is not called for chunked transfers. So + * if this returns TRUE, contentLength() is ignored. + * + * @return boolean TRUE to recommend chunked transfer, FALSE otherwise. + */ + public function isChunked() + { + // Currently, this value is hard-coded. The default Object + // implementation does not get chunked. + return FALSE; + } } diff --git a/src/OpenStack/Storage/ObjectStorage/ReadOnlyObjectException.php b/src/OpenStack/Storage/ObjectStorage/ReadOnlyObjectException.php index b8e89cb..a15ebba 100644 --- a/src/OpenStack/Storage/ObjectStorage/ReadOnlyObjectException.php +++ b/src/OpenStack/Storage/ObjectStorage/ReadOnlyObjectException.php @@ -2,17 +2,17 @@ /* ============================================================================ (c) Copyright 2012-2014 Hewlett-Packard Development Company, L.P. - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at - http://www.apache.org/licenses/LICENSE-2.0 + http://www.apache.org/licenses/LICENSE-2.0 - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. ============================================================================ */ namespace OpenStack\Storage\ObjectStorage; diff --git a/src/OpenStack/Storage/ObjectStorage/RemoteObject.php b/src/OpenStack/Storage/ObjectStorage/RemoteObject.php index 1b52ce1..214c16a 100644 --- a/src/OpenStack/Storage/ObjectStorage/RemoteObject.php +++ b/src/OpenStack/Storage/ObjectStorage/RemoteObject.php @@ -2,17 +2,17 @@ /* ============================================================================ (c) Copyright 2012-2014 Hewlett-Packard Development Company, L.P. - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at - http://www.apache.org/licenses/LICENSE-2.0 + http://www.apache.org/licenses/LICENSE-2.0 - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. ============================================================================ */ /** * Contains the RemoteObject class. @@ -42,606 +42,627 @@ use OpenStack\Transport\GuzzleClient; * object is modified so that its local contents differ from the remote * stored copy, it is marked dirty (see isDirty()). */ -class RemoteObject extends Object { +class RemoteObject extends Object +{ + protected $contentLength = 0; + protected $etag = ''; + protected $lastModified = 0; - protected $contentLength = 0; - protected $etag = ''; - protected $lastModified = 0; + protected $contentVerification = TRUE; + protected $caching = FALSE; - protected $contentVerification = TRUE; - protected $caching = FALSE; + /** + * All headers received from a remote are stored in this array. + * Implementing subclasses can access this array for complete access + * to the HTTP headers. + * + * This will be empty if the object was constructed from JSON, and may + * serve as a good indicator that the object does not have all + * attributes set. + */ + protected $allHeaders = array(); - /** - * All headers received from a remote are stored in this array. - * Implementing subclasses can access this array for complete access - * to the HTTP headers. - * - * This will be empty if the object was constructed from JSON, and may - * serve as a good indicator that the object does not have all - * attributes set. - */ - protected $allHeaders = array(); + /** + * The HTTP Client + */ + protected $client; - /** - * The HTTP Client - */ - protected $client; + /** + * Create a new RemoteObject from JSON data. + * + * @param array $data The JSON data as an array. + * @param string $token The authentication token. + * @param $url The URL to the object on the remote server + * @param \OpenStack\Transport\ClientInterface $client A HTTP transport client. + */ + public static function newFromJSON($data, $token, $url, \OpenStack\Transport\ClientInterface $client = NULL) + { + $object = new RemoteObject($data['name']); + $object->setContentType($data['content_type']); - /** - * Create a new RemoteObject from JSON data. - * - * @param array $data The JSON data as an array. - * @param string $token The authentication token. - * @param $url The URL to the object on the remote server - * @param \OpenStack\Transport\ClientInterface $client A HTTP transport client. - */ - public static function newFromJSON($data, $token, $url, \OpenStack\Transport\ClientInterface $client = NULL) { + $object->contentLength = (int) $data['bytes']; + $object->etag = (string) $data['hash']; + $object->lastModified = strtotime($data['last_modified']); - $object = new RemoteObject($data['name']); - $object->setContentType($data['content_type']); + $object->token = $token; + $object->url = $url; - $object->contentLength = (int) $data['bytes']; - $object->etag = (string) $data['hash']; - $object->lastModified = strtotime($data['last_modified']); + // FIXME: What do we do about HTTP header data that doesn't come + // back in JSON? - $object->token = $token; - $object->url = $url; + if (is_null($client)) { + $client = new GuzzleClient(); + } + $object->setClient($client); - // FIXME: What do we do about HTTP header data that doesn't come - // back in JSON? - - if (is_null($client)) { - $client = new GuzzleClient(); - } - $object->setClient($client); - - return $object; - } - - /** - * Create a new RemoteObject from HTTP headers. - * - * This is used to create objects from GET and HEAD requests, which - * return all of the metadata inside of the headers. - * - * @param string $name The name of the object. - * @param array $headers An associative array of HTTP headers in the exact - * format documented by OpenStack's API docs. - * @param string $token The current auth token (used for issuing subsequent - * requests). - * @param string $url The URL to the object in the object storage. Used for - * issuing subsequent requests. - * - * @return \OpenStack\Storage\ObjectStorage\RemoteObject A new RemoteObject. - */ - public static function newFromHeaders($name, $headers, $token, $url, \OpenStack\Transport\ClientInterface $client = NULL) { - $object = new RemoteObject($name); - - //$object->allHeaders = $headers; - $object->setHeaders($headers); - - //throw new \Exception(print_r($headers, TRUE)); - - // Fix inconsistant header. - if (isset($headers['ETag'])) { - $headers['Etag'] = $headers['ETag']; + return $object; } - $object->setContentType($headers['Content-Type']); - $object->contentLength = empty($headers['Content-Length']) ? 0 : (int) $headers['Content-Length']; - $object->etag = (string) $headers['Etag']; // ETag is now Etag. - $object->lastModified = strtotime($headers['Last-Modified']); + /** + * Create a new RemoteObject from HTTP headers. + * + * This is used to create objects from GET and HEAD requests, which + * return all of the metadata inside of the headers. + * + * @param string $name The name of the object. + * @param array $headers An associative array of HTTP headers in the exact + * format documented by OpenStack's API docs. + * @param string $token The current auth token (used for issuing subsequent + * requests). + * @param string $url The URL to the object in the object storage. Used for + * issuing subsequent requests. + * + * @return \OpenStack\Storage\ObjectStorage\RemoteObject A new RemoteObject. + */ + public static function newFromHeaders($name, $headers, $token, $url, \OpenStack\Transport\ClientInterface $client = NULL) + { + $object = new RemoteObject($name); - // Set the metadata, too. - $object->setMetadata(Container::extractHeaderAttributes($headers)); + //$object->allHeaders = $headers; + $object->setHeaders($headers); + + //throw new \Exception(print_r($headers, TRUE)); + + // Fix inconsistant header. + if (isset($headers['ETag'])) { + $headers['Etag'] = $headers['ETag']; + } + + $object->setContentType($headers['Content-Type']); + $object->contentLength = empty($headers['Content-Length']) ? 0 : (int) $headers['Content-Length']; + $object->etag = (string) $headers['Etag']; // ETag is now Etag. + $object->lastModified = strtotime($headers['Last-Modified']); + + // Set the metadata, too. + $object->setMetadata(Container::extractHeaderAttributes($headers)); - // If content encoding and disposition exist, set them on the - // object. - if (!empty($headers['Content-Disposition'])) { - $object->setDisposition($headers['Content-Disposition']); + // If content encoding and disposition exist, set them on the + // object. + if (!empty($headers['Content-Disposition'])) { + $object->setDisposition($headers['Content-Disposition']); - } - if (!empty($headers['Content-Encoding'])) { - $object->setEncoding($headers['Content-Encoding']); + } + if (!empty($headers['Content-Encoding'])) { + $object->setEncoding($headers['Content-Encoding']); + } + + $object->token = $token; + $object->url = $url; + + if (is_null($client)) { + $client = new GuzzleClient(); + } + $object->setClient($client); + + return $object; } - $object->token = $token; - $object->url = $url; - - if (is_null($client)) { - $client = new GuzzleClient(); - } - $object->setClient($client); - - return $object; - } - - /** - * Set the HTTP Client to use. - * - * @param OpenStackTransportClientInterface $client The HTTP Client - */ - public function setClient(\OpenStack\Transport\ClientInterface $client) { - $this->client = $client; - } - - /** - * Get the URL to this object. - * - * If this object has been stored remotely, it will have - * a valid URL. - * - * @return string - * A URL to the object. The following considerations apply: - * - If the container is public, this URL can be loaded without - * authentication. You can, for example, pass the URL to a browser - * user agent. - * - If this object has never been saved remotely, then there will be - * no URL, and this will return NULL. - */ - public function url() { - return $this->url; - } - - - public function contentLength() { - if (!empty($this->content)) { - return parent::contentLength(); - } - return $this->contentLength; - } - - public function eTag() { - - if (!empty($this->content)) { - return parent::eTag(); + /** + * Set the HTTP Client to use. + * + * @param OpenStackTransportClientInterface $client The HTTP Client + */ + public function setClient(\OpenStack\Transport\ClientInterface $client) + { + $this->client = $client; } - return $this->etag; - } - - /** - * Get the modification time, as reported by the server. - * - * This returns an integer timestamp indicating when the server's - * copy of this file was last modified. - */ - public function lastModified() { - return $this->lastModified; - } - - public function metadata() { - // How do we get this? - return $this->metadata; - } - - /** - * Set the headers - * - * @return \OpenStack\Storage\ObjectStorage\RemoteObject $this for the current - * object so it can be used in chaining methods. - */ - public function setHeaders($headers) { - $this->allHeaders = array(); - - foreach ($headers as $name => $value) { - if (strpos($name, Container::METADATA_HEADER_PREFIX) !== 0) { - $this->allHeaders[$name] = $value; - } + /** + * Get the URL to this object. + * + * If this object has been stored remotely, it will have + * a valid URL. + * + * @return string A URL to the object. The following considerations apply: + * - If the container is public, this URL can be loaded without + * authentication. You can, for example, pass the URL to a browser + * user agent. + * - If this object has never been saved remotely, then there will be + * no URL, and this will return NULL. + */ + public function url() + { + return $this->url; } - return $this; - } - /** - * Get the HTTP headers sent by the server. - * - * EXPERT. - * - * This returns the array of minimally processed HTTP headers that - * were sent from the server. - * - * @return array An associative array of header names and values. - */ - public function headers() { - return $this->allHeaders; - } + public function contentLength() + { + if (!empty($this->content)) { + return parent::contentLength(); + } - public function additionalHeaders($mergeAll = FALSE) { - // Any additional headers will be set. Note that $this->headers will contain - // some headers that are NOT additional. But we do not know which headers are - // additional and which are from Swift because Swift does not commit to using - // a specific set of headers. - if ($mergeAll) { - $additionalHeaders = parent::additionalHeaders() + $this->allHeaders; - $this->filterHeaders($additionalHeaders); - } - else { - $additionalHeaders = parent::additionalHeaders(); + return $this->contentLength; } - return $additionalHeaders; - } + public function eTag() + { + if (!empty($this->content)) { + return parent::eTag(); + } - protected $reservedHeaders = array( - 'etag' => TRUE, 'content-length' => TRUE, - 'x-auth-token' => TRUE, - 'transfer-encoding' => TRUE, - 'x-trans-id' => TRUE, - ); - - /** - * Filter the headers. - * - * @return \OpenStack\Storage\ObjectStorage\RemoteObject $this for the current - * object so it can be used in chaining methods. - */ - public function filterHeaders(&$headers) { - $unset = array(); - foreach ($headers as $name => $value) { - $lower = strtolower($name); - if (isset($this->reservedHeaders[$lower])) { - $unset[] = $name; - } - } - foreach ($unset as $u) { - unset($headers[$u]); + return $this->etag; } - return $this; - } - - /** - * Given an array of header names. - * - * This will remove the given headers from the existing headers. - * Both additional headers and the original headers from the - * server are affected here. - * - * Note that you cannot remove metadata through this mechanism, - * as it is managed using the metadata() methods. - * - * Many headers are generated automatically, such as - * Content-Type and Content-Length. Removing these - * will simply result in their being regenerated. - * - * @param array $keys The header names to be removed. - * - * @return \OpenStack\Storage\ObjectStorage\RemoteObject $this for the current - * object so it can be used in chaining methods. - */ - public function removeHeaders($keys) { - foreach ($keys as $key) { - unset($this->allHeaders[$key]); - unset($this->additionalHeaders[$key]); + /** + * Get the modification time, as reported by the server. + * + * This returns an integer timestamp indicating when the server's + * copy of this file was last modified. + */ + public function lastModified() + { + return $this->lastModified; } - return $this; - } - - /** - * Get the content of this object. - * - * Since this is a proxy object, calling content() will cause the - * object to be fetched from the remote data storage. The result will - * be delivered as one large string. - * - * The file size, content type, etag, and modification date of the - * object are all updated during this command, too. This accounts for - * the possibility that the content was modified externally between - * the time this object was constructed and the time this method was - * executed. - * - * Be wary of using this method with large files. - * - * @return string The contents of the file as a string. - * @throws \OpenStack\Transport\FileNotFoundException when the requested - * content cannot be located on the remote server. - * @throws \OpenStack\Exception when an unknown exception (usually an abnormal - * network condition) occurs. - */ - public function content() { - - // XXX: This allows local overwrites. Is this a good idea? - if (!empty($this->content)) { - return $this->content; + public function metadata() + { + // How do we get this? + return $this->metadata; } - // Get the object, content included. - $response = $this->fetchObject(TRUE); + /** + * Set the headers + * + * @return \OpenStack\Storage\ObjectStorage\RemoteObject $this for the current object so it can be used in chaining + * methods. + */ + public function setHeaders($headers) + { + $this->allHeaders = array(); - $content = $response->getBody(); + foreach ($headers as $name => $value) { + if (strpos($name, Container::METADATA_HEADER_PREFIX) !== 0) { + $this->allHeaders[$name] = $value; + } + } - // Checksum the content. - // XXX: Right now the md5 is done even if checking is turned off. - // Should fix that. - $check = md5($content); - if ($this->isVerifyingContent() && $check != $this->etag()) { - throw new ContentVerificationException("Checksum $check does not match Etag " . $this->etag()); + return $this; } - // If we are caching, set the content locally when we retrieve - // remotely. - if ($this->isCaching()) { - $this->setContent($content); + /** + * Get the HTTP headers sent by the server. + * + * EXPERT. + * + * This returns the array of minimally processed HTTP headers that + * were sent from the server. + * + * @return array An associative array of header names and values. + */ + public function headers() + { + return $this->allHeaders; } - return $content; - } + public function additionalHeaders($mergeAll = FALSE) + { + // Any additional headers will be set. Note that $this->headers will contain + // some headers that are NOT additional. But we do not know which headers are + // additional and which are from Swift because Swift does not commit to using + // a specific set of headers. + if ($mergeAll) { + $additionalHeaders = parent::additionalHeaders() + $this->allHeaders; + $this->filterHeaders($additionalHeaders); + } else { + $additionalHeaders = parent::additionalHeaders(); + } - /** - * Get the content of this object as a file stream. - * - * This is useful for large objects. Such objects should not be read - * into memory all at once (as content() does), but should instead be - * made available as an input stream. - * - * PHP offers low-level stream support in the form of PHP stream - * wrappers, and this mechanism is used internally whenever available. - * - * If there is a local copy of the content, the stream will be read - * out of the content as if it were a temp-file backed in-memory - * resource. To ignore the local version, pass in TRUE for the - * $refresh parameter. - * - * If the content is coming from a remote copy, the stream will be - * read directly from the underlying IO stream. - * - * Each time stream() is called, a new stream is created. In most - * cases, this results in a new HTTP transaction (unless $refresh is - * FALSE and the content is already stored locally). - * - * The stream is read-only. - * - * @param boolean $refresh If this is set to TRUE, any existing local - * modifications will be ignored and the content will be refreshed from the - * server. Any local changes to the object will be discarded. - * - * @return resource A handle to the stream, which is already opened and - * positioned at the beginning of the stream. - */ - public function stream($refresh = FALSE) { - - // If we're working on local content, return that content wrapped in - // a fake IO stream. - if (!$refresh && isset($this->content)) { - return $this->localFileStream(); + return $additionalHeaders; } - // Otherwise, we fetch a fresh version from the remote server and - // return its stream handle. - $response = $this->fetchObject(TRUE); - - // Write to in-mem handle backed by a temp file. - $out = fopen('php://temp', 'rb+'); - fwrite($out, $response->getBody()); - rewind($out); - - return $out; - } - - /** - * Transform a local copy of content into a file stream. - * - * This buffers the content into a stream resource and then returns - * the stream resource. The resource is not used internally, and its - * data is never written back to the remote object storage. - */ - protected function localFileStream() { - - $tmp = fopen('php://temp', 'rw'); - fwrite($tmp, $this->content(), $this->contentLength()); - rewind($tmp); - - return $tmp; - } - - /** - * Enable or disable content caching. - * - * If a RemoteObject is set to cache then the first time content() is - * called, its results will be cached locally. This is very useful for - * small files whose content is accessed repeatedly, but can be a - * cause of memory consumption for larger files. - * - * If caching settings are changed after content is retrieved, the - * already retrieved content will not be affected, though any - * subsequent requests will use the new caching settings. That is, - * existing cached content will not be removed if caching is turned - * off. - * - * @param boolean $enabled If this is TRUE, caching will be enabled. If this - * is FALSE, caching will be disabled. - * - * @return \OpenStack\Storage\ObjectStorage\RemoteObject $this so the method - * can be used in chaining. - */ - public function setCaching($enabled) { - $this->caching = $enabled; - return $this; - } - - /** - * Indicates whether this object caches content. - * - * Importantly, this indicates whether the object will cache - * its contents, not whether anything is actually cached. - * - * @return boolean TRUE if caching is enabled, FALSE otherwise. - */ - public function isCaching() { - return $this->caching; - } - - /** - * Enable or disable content verification (checksum/md5). - * - * The default behavior of a RemoteObject is to verify that the MD5 - * provided by the server matches the locally generated MD5 of the - * file contents. - * - * If content verification is enabled, then whenever the content is - * fetched from the remote server, its checksum is calculated and - * tested against the ETag value. This provides a layer of assurance - * that the payload of the HTTP request was not altered during - * transmission. - * - * This featured can be turned off, which is sometimes necessary on - * systems that do not correctly produce MD5s. Turning this off might - * also provide a small performance improvement on large files, but at - * the expense of security. - * - * @param boolean $enabled If this is TRUE, content verification is performed. - * The content is hashed and checked against a server-supplied MD5 hashcode. - * If this is FALSE, no checking is done. - * - * @return \OpenStack\Storage\ObjectStorage\RemoteObject $this so the method - * can be used in chaining. - */ - public function setContentVerification($enabled) { - $this->contentVerification = $enabled; - return $this; - } - - /** - * Indicate whether this object verifies content (checksum). - * - * When content verification is on, RemoteObject attemts to perform a - * checksum on the object, calculating the MD5 hash of the content - * returned by the remote server, and comparing that to the server's - * supplied ETag hash. - * - * @return boolean TRUE if this is verifying, FALSE otherwise. - */ - public function isVerifyingContent() { - return $this->contentVerification; - } - - /** - * Check whether there are unsaved changes. - * - * An object is marked "dirty" if it has been altered - * locally in such a way that it no longer matches the - * remote version. - * - * The practical definition of dirtiness, for us, is this: An object - * is dirty if and only if (a) it has locally buffered content AND (b) - * the checksum of the local content does not match the checksom of - * the remote content. - * - * Not that minor differences, such as altered character encoding, may - * change the checksum value, and thus (correctly) mark the object as - * dirty. - * - * The RemoteObject implementation does not internally check dirty - * markers. It is left to implementors to ensure that dirty content is - * written to the remote server when desired. - * - * To replace dirty content with a clean copy, see refresh(). - * - * @return boolean Whether or not there are unsaved changes. - */ - public function isDirty() { - - // If there is no content, the object can't be dirty. - if (!isset($this->content)) { - return FALSE; - } - - // Content is dirty iff content is set, and it is - // different from the original content. Note that - // we are using the etag from the original headers. - if ($this->etag != md5($this->content)) { - return TRUE; - } - - return FALSE; - } - - /** - * Rebuild the local object from the remote. - * - * This refetches the object from the object store and then - * reconstructs the present object based on the refreshed data. - * - * WARNING: This will destroy any unsaved local changes. You can use - * isDirty() to determine whether or not a local change has been made. - * - * @param boolean $fetchContent If this is TRUE, the content will be - * downloaded as well. - * - * @return \OpenStack\Storage\ObjectStorage\RemoteObject $this for the current - * object so it can be used in chaining methods. - */ - public function refresh($fetchContent = FALSE) { - - // Kill old content. - unset($this->content); - - $response = $this->fetchObject($fetchContent); - - - if ($fetchContent) { - $this->setContent($response->getBody()); - } - - return $this; - } - - /** - * Helper function for fetching an object. - * - * @param boolean $fetchContent If this is set to TRUE, a GET request will be - * issued, which will cause the remote host to return the object in the - * response body. The response body is not handled, though. If this is set - * to FALSE, a HEAD request is sent, and no body is returned. - * - * @return \OpenStack\Transport\Response containing the object metadata and - * (depending on the $fetchContent flag) optionally the data. - */ - protected function fetchObject($fetchContent = FALSE) { - $method = $fetchContent ? 'GET' : 'HEAD'; - - $headers = array( - 'X-Auth-Token' => $this->token, + protected $reservedHeaders = array( + 'etag' => TRUE, 'content-length' => TRUE, + 'x-auth-token' => TRUE, + 'transfer-encoding' => TRUE, + 'x-trans-id' => TRUE, ); - $response = $this->client->doRequest($this->url, $method, $headers); + /** + * Filter the headers. + * + * @return \OpenStack\Storage\ObjectStorage\RemoteObject $this for the current object so it can be used in chaining + * methods. + */ + public function filterHeaders(&$headers) + { + $unset = array(); + foreach ($headers as $name => $value) { + $lower = strtolower($name); + if (isset($this->reservedHeaders[$lower])) { + $unset[] = $name; + } + } + foreach ($unset as $u) { + unset($headers[$u]); + } - if ($response->getStatusCode() != 200) { - throw new \OpenStack\Exception('An unknown exception occurred during transmission.'); + return $this; } - $this->extractFromHeaders($response); + /** + * Given an array of header names. + * + * This will remove the given headers from the existing headers. + * Both additional headers and the original headers from the + * server are affected here. + * + * Note that you cannot remove metadata through this mechanism, + * as it is managed using the metadata() methods. + * + * Many headers are generated automatically, such as + * Content-Type and Content-Length. Removing these + * will simply result in their being regenerated. + * + * @param array $keys The header names to be removed. + * + * @return \OpenStack\Storage\ObjectStorage\RemoteObject $this for the current object so it can be used in chaining + * methods. + */ + public function removeHeaders($keys) + { + foreach ($keys as $key) { + unset($this->allHeaders[$key]); + unset($this->additionalHeaders[$key]); + } - return $response; - } - - /** - * Extract information from HTTP headers. - * - * This is used internally to set object properties from headers. - * - * @return \OpenStack\Storage\ObjectStorage\RemoteObject $this for the current - * object so it can be used in chaining methods. - */ - protected function extractFromHeaders($response) { - $this->setContentType($response->getHeader('Content-Type') ? $response->getHeader('Content-Type') : $this->contentType()); - $this->lastModified = strtotime($response->getHeader('Last-Modified') ? $response->getHeader('Last-Modified') : 0); - $this->etag = $response->getHeader('Etag') ? $response->getHeader('Etag') : $this->etag; - $this->contentLength = (int) ($response->getHeader('Content-Length') ? $response->getHeader('Content-Length') : 0); - - $this->setDisposition($response->getHeader('Content-Disposition', NULL)); - $this->setEncoding($response->getHeader('Content-Encoding', NULL)); - - // Reset the metadata, too: - $headers = []; - foreach ($response->getHeaders() as $name => $header) { - $headers[$name] = $header[0]; + return $this; } - $this->setMetadata(Container::extractHeaderAttributes($headers)); - return $this; - } + /** + * Get the content of this object. + * + * Since this is a proxy object, calling content() will cause the + * object to be fetched from the remote data storage. The result will + * be delivered as one large string. + * + * The file size, content type, etag, and modification date of the + * object are all updated during this command, too. This accounts for + * the possibility that the content was modified externally between + * the time this object was constructed and the time this method was + * executed. + * + * Be wary of using this method with large files. + * + * @return string The contents of the file as a string. + * + * @throws \OpenStack\Transport\FileNotFoundException when the requested content cannot be located on the remote + * server. + * @throws \OpenStack\Exception when an unknown exception (usually an abnormal network + * condition) occurs. + */ + public function content() + { + // XXX: This allows local overwrites. Is this a good idea? + if (!empty($this->content)) { + return $this->content; + } + + // Get the object, content included. + $response = $this->fetchObject(TRUE); + + $content = $response->getBody(); + + // Checksum the content. + // XXX: Right now the md5 is done even if checking is turned off. + // Should fix that. + $check = md5($content); + if ($this->isVerifyingContent() && $check != $this->etag()) { + throw new ContentVerificationException("Checksum $check does not match Etag " . $this->etag()); + } + + // If we are caching, set the content locally when we retrieve + // remotely. + if ($this->isCaching()) { + $this->setContent($content); + } + + return $content; + } + + /** + * Get the content of this object as a file stream. + * + * This is useful for large objects. Such objects should not be read + * into memory all at once (as content() does), but should instead be + * made available as an input stream. + * + * PHP offers low-level stream support in the form of PHP stream + * wrappers, and this mechanism is used internally whenever available. + * + * If there is a local copy of the content, the stream will be read + * out of the content as if it were a temp-file backed in-memory + * resource. To ignore the local version, pass in TRUE for the + * $refresh parameter. + * + * If the content is coming from a remote copy, the stream will be + * read directly from the underlying IO stream. + * + * Each time stream() is called, a new stream is created. In most + * cases, this results in a new HTTP transaction (unless $refresh is + * FALSE and the content is already stored locally). + * + * The stream is read-only. + * + * @param boolean $refresh If this is set to TRUE, any existing local + * modifications will be ignored and the content will + * be refreshed from the server. Any local changes to + * the object will be discarded. + * + * @return resource A handle to the stream, which is already opened and + * positioned at the beginning of the stream. + */ + public function stream($refresh = FALSE) + { + // If we're working on local content, return that content wrapped in + // a fake IO stream. + if (!$refresh && isset($this->content)) { + return $this->localFileStream(); + } + + // Otherwise, we fetch a fresh version from the remote server and + // return its stream handle. + $response = $this->fetchObject(TRUE); + + // Write to in-mem handle backed by a temp file. + $out = fopen('php://temp', 'rb+'); + fwrite($out, $response->getBody()); + rewind($out); + + return $out; + } + + /** + * Transform a local copy of content into a file stream. + * + * This buffers the content into a stream resource and then returns + * the stream resource. The resource is not used internally, and its + * data is never written back to the remote object storage. + */ + protected function localFileStream() + { + $tmp = fopen('php://temp', 'rw'); + fwrite($tmp, $this->content(), $this->contentLength()); + rewind($tmp); + + return $tmp; + } + + /** + * Enable or disable content caching. + * + * If a RemoteObject is set to cache then the first time content() is + * called, its results will be cached locally. This is very useful for + * small files whose content is accessed repeatedly, but can be a + * cause of memory consumption for larger files. + * + * If caching settings are changed after content is retrieved, the + * already retrieved content will not be affected, though any + * subsequent requests will use the new caching settings. That is, + * existing cached content will not be removed if caching is turned + * off. + * + * @param boolean $enabled If this is TRUE, caching will be enabled. If this + * is FALSE, caching will be disabled. + * + * @return \OpenStack\Storage\ObjectStorage\RemoteObject $this so the method can be used in chaining. + */ + public function setCaching($enabled) + { + $this->caching = $enabled; + + return $this; + } + + /** + * Indicates whether this object caches content. + * + * Importantly, this indicates whether the object will cache + * its contents, not whether anything is actually cached. + * + * @return boolean TRUE if caching is enabled, FALSE otherwise. + */ + public function isCaching() + { + return $this->caching; + } + + /** + * Enable or disable content verification (checksum/md5). + * + * The default behavior of a RemoteObject is to verify that the MD5 + * provided by the server matches the locally generated MD5 of the + * file contents. + * + * If content verification is enabled, then whenever the content is + * fetched from the remote server, its checksum is calculated and + * tested against the ETag value. This provides a layer of assurance + * that the payload of the HTTP request was not altered during + * transmission. + * + * This featured can be turned off, which is sometimes necessary on + * systems that do not correctly produce MD5s. Turning this off might + * also provide a small performance improvement on large files, but at + * the expense of security. + * + * @param boolean $enabled If this is TRUE, content verification is performed. + * The content is hashed and checked against a + * server-supplied MD5 hashcode. If this is FALSE, + * no checking is done. + * + * @return \OpenStack\Storage\ObjectStorage\RemoteObject $this so the method can be used in chaining. + */ + public function setContentVerification($enabled) + { + $this->contentVerification = $enabled; + + return $this; + } + + /** + * Indicate whether this object verifies content (checksum). + * + * When content verification is on, RemoteObject attemts to perform a + * checksum on the object, calculating the MD5 hash of the content + * returned by the remote server, and comparing that to the server's + * supplied ETag hash. + * + * @return boolean TRUE if this is verifying, FALSE otherwise. + */ + public function isVerifyingContent() + { + return $this->contentVerification; + } + + /** + * Check whether there are unsaved changes. + * + * An object is marked "dirty" if it has been altered + * locally in such a way that it no longer matches the + * remote version. + * + * The practical definition of dirtiness, for us, is this: An object + * is dirty if and only if (a) it has locally buffered content AND (b) + * the checksum of the local content does not match the checksom of + * the remote content. + * + * Not that minor differences, such as altered character encoding, may + * change the checksum value, and thus (correctly) mark the object as + * dirty. + * + * The RemoteObject implementation does not internally check dirty + * markers. It is left to implementors to ensure that dirty content is + * written to the remote server when desired. + * + * To replace dirty content with a clean copy, see refresh(). + * + * @return boolean Whether or not there are unsaved changes. + */ + public function isDirty() + { + // If there is no content, the object can't be dirty. + if (!isset($this->content)) { + return FALSE; + } + + // Content is dirty iff content is set, and it is + // different from the original content. Note that + // we are using the etag from the original headers. + if ($this->etag != md5($this->content)) { + return TRUE; + } + + return FALSE; + } + + /** + * Rebuild the local object from the remote. + * + * This refetches the object from the object store and then + * reconstructs the present object based on the refreshed data. + * + * WARNING: This will destroy any unsaved local changes. You can use + * isDirty() to determine whether or not a local change has been made. + * + * @param boolean $fetchContent If this is TRUE, the content will be + * downloaded as well. + * + * @return \OpenStack\Storage\ObjectStorage\RemoteObject $this for the current object so it can be used in chaining + * methods. + */ + public function refresh($fetchContent = FALSE) + { + // Kill old content. + unset($this->content); + + $response = $this->fetchObject($fetchContent); + + + if ($fetchContent) { + $this->setContent($response->getBody()); + } + + return $this; + } + + /** + * Helper function for fetching an object. + * + * @param boolean $fetchContent If this is set to TRUE, a GET request will be + * issued, which will cause the remote host to + * return the object in the response body. The + * response body is not handled, though. If this + * is set to FALSE, a HEAD request is sent, and + * no body is returned. + * + * @return \OpenStack\Transport\Response containing the object metadata and (depending on the $fetchContent flag) + * optionally the data. + */ + protected function fetchObject($fetchContent = FALSE) + { + $method = $fetchContent ? 'GET' : 'HEAD'; + + $headers = array( + 'X-Auth-Token' => $this->token, + ); + + $response = $this->client->doRequest($this->url, $method, $headers); + + if ($response->getStatusCode() != 200) { + throw new \OpenStack\Exception('An unknown exception occurred during transmission.'); + } + + $this->extractFromHeaders($response); + + return $response; + } + + /** + * Extract information from HTTP headers. + * + * This is used internally to set object properties from headers. + * + * @return \OpenStack\Storage\ObjectStorage\RemoteObject $this for the current object so it can be used in chaining + * methods. + */ + protected function extractFromHeaders($response) + { + $this->setContentType($response->getHeader('Content-Type') ? $response->getHeader('Content-Type') : $this->contentType()); + $this->lastModified = strtotime($response->getHeader('Last-Modified') ? $response->getHeader('Last-Modified') : 0); + $this->etag = $response->getHeader('Etag') ? $response->getHeader('Etag') : $this->etag; + $this->contentLength = (int) ($response->getHeader('Content-Length') ? $response->getHeader('Content-Length') : 0); + + $this->setDisposition($response->getHeader('Content-Disposition', NULL)); + $this->setEncoding($response->getHeader('Content-Encoding', NULL)); + + // Reset the metadata, too: + $headers = []; + foreach ($response->getHeaders() as $name => $header) { + $headers[$name] = $header[0]; + } + $this->setMetadata(Container::extractHeaderAttributes($headers)); + + return $this; + } } diff --git a/src/OpenStack/Storage/ObjectStorage/StreamWrapper.php b/src/OpenStack/Storage/ObjectStorage/StreamWrapper.php index 7a75d86..cd77303 100644 --- a/src/OpenStack/Storage/ObjectStorage/StreamWrapper.php +++ b/src/OpenStack/Storage/ObjectStorage/StreamWrapper.php @@ -2,17 +2,17 @@ /* ============================================================================ (c) Copyright 2012-2014 Hewlett-Packard Development Company, L.P. - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at - http://www.apache.org/licenses/LICENSE-2.0 + http://www.apache.org/licenses/LICENSE-2.0 - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. ============================================================================ */ /** * Contains the stream wrapper for `swift://` URLs. @@ -241,1254 +241,1274 @@ use \OpenStack\Storage\ObjectStorage; * @todo The service catalog should be cached in the context like the token so that * it can be retrieved later. */ -class StreamWrapper { +class StreamWrapper +{ + const DEFAULT_SCHEME = 'swift'; - const DEFAULT_SCHEME = 'swift'; + /** + * Cache of auth token -> service catalog. + * + * This will eventually be replaced by a better system, but for a system of + * moderate complexity, many, many file operations may be run during the + * course of a request. Caching the catalog can prevent numerous calls + * to identity services. + */ + protected static $serviceCatalogCache = array(); - /** - * Cache of auth token -> service catalog. - * - * This will eventually be replaced by a better system, but for a system of - * moderate complexity, many, many file operations may be run during the - * course of a request. Caching the catalog can prevent numerous calls - * to identity services. - */ - protected static $serviceCatalogCache = array(); + /** + * The stream context. + * + * This is set automatically when the stream wrapper is created by + * PHP. Note that it is not set through a constructor. + */ + public $context; + protected $contextArray = array(); - /** - * The stream context. - * - * This is set automatically when the stream wrapper is created by - * PHP. Note that it is not set through a constructor. - */ - public $context; - protected $contextArray = array(); + protected $schemeName = self::DEFAULT_SCHEME; + protected $authToken; - protected $schemeName = self::DEFAULT_SCHEME; - protected $authToken; + // File flags. These should probably be replaced by O_ const's at some point. + protected $isBinary = FALSE; + protected $isText = TRUE; + protected $isWriting = FALSE; + protected $isReading = FALSE; + protected $isTruncating = FALSE; + protected $isAppending = FALSE; + protected $noOverwrite = FALSE; + protected $createIfNotFound = TRUE; + /** + * If this is TRUE, no data is ever sent to the remote server. + */ + protected $isNeverDirty = FALSE; - // File flags. These should probably be replaced by O_ const's at some point. - protected $isBinary = FALSE; - protected $isText = TRUE; - protected $isWriting = FALSE; - protected $isReading = FALSE; - protected $isTruncating = FALSE; - protected $isAppending = FALSE; - protected $noOverwrite = FALSE; - protected $createIfNotFound = TRUE; + protected $triggerErrors = FALSE; - /** - * If this is TRUE, no data is ever sent to the remote server. - */ - protected $isNeverDirty = FALSE; + /** + * Indicate whether the local differs from remote. + * + * When the file is modified in such a way that + * it needs to be written remotely, the isDirty flag + * is set to TRUE. + */ + protected $isDirty = FALSE; - protected $triggerErrors = FALSE; + /** + * Object storage instance. + */ + protected $store; - /** - * Indicate whether the local differs from remote. - * - * When the file is modified in such a way that - * it needs to be written remotely, the isDirty flag - * is set to TRUE. - */ - protected $isDirty = FALSE; + /** + * The Container. + */ + protected $container; - /** - * Object storage instance. - */ - protected $store; + /** + * The Object. + */ + protected $obj; - /** - * The Container. - */ - protected $container; + /** + * The IO stream for the Object. + */ + protected $objStream; - /** - * The Object. - */ - protected $obj; + /** + * Directory listing. + * + * Used for directory methods. + */ + protected $dirListing = array(); + protected $dirIndex = 0; + protected $dirPrefix = ''; - /** - * The IO stream for the Object. - */ - protected $objStream; + /** + * Close a directory. + * + * This closes a directory handle, freeing up the resources. + * + * + * + * NB: Some versions of PHP 5.3 don't clear all buffers when + * closing, and the handle can occasionally remain accessible for + * some period of time. + */ + public function dir_closedir() + { + $this->dirIndex = 0; + $this->dirListing = array(); - /** - * Directory listing. - * - * Used for directory methods. - */ - protected $dirListing = array(); - protected $dirIndex = 0; - protected $dirPrefix = ''; - - /** - * Close a directory. - * - * This closes a directory handle, freeing up the resources. - * - * - * - * NB: Some versions of PHP 5.3 don't clear all buffers when - * closing, and the handle can occasionally remain accessible for - * some period of time. - */ - public function dir_closedir() { - $this->dirIndex = 0; - $this->dirListing = array(); - - //syslog(LOG_WARNING, "CLOSEDIR called."); - - return TRUE; - } - - /** - * Open a directory for reading. - * - * - * - * See opendir() and scandir(). - * - * @param string $path The URL to open. - * @param int $options Unused. - * - * @return boolean TRUE if the directory is opened, FALSE otherwise. - */ - public function dir_opendir($path, $options) { - $url = $this->parseUrl($path); - - if (empty($url['host'])) { - trigger_error('Container name is required.' , E_USER_WARNING); - return FALSE; + //syslog(LOG_WARNING, "CLOSEDIR called."); + return TRUE; } - try { - $this->initializeObjectStorage(); - $container = $this->store->container($url['host']); + /** + * Open a directory for reading. + * + * + * + * See opendir() and scandir(). + * + * @param string $path The URL to open. + * @param int $options Unused. + * + * @return boolean TRUE if the directory is opened, FALSE otherwise. + */ + public function dir_opendir($path, $options) + { + $url = $this->parseUrl($path); - if (empty($url['path'])) { - $this->dirPrefix = ''; - } - else { - $this->dirPrefix = $url['path']; - } + if (empty($url['host'])) { + trigger_error('Container name is required.' , E_USER_WARNING); - $sep = '/'; - - - $this->dirListing = $container->objectsWithPrefix($this->dirPrefix, $sep); - } - catch (\OpenStack\Exception $e) { - trigger_error('Directory could not be opened: ' . $e->getMessage(), E_USER_WARNING); - return FALSE; - } - - return TRUE; - } - - /** - * Read an entry from the directory. - * - * This gets a single line from the directory. - * - * - * - * @return string The name of the resource or FALSE when the directory has no - * more entries. - */ - public function dir_readdir() { - // If we are at the end of the listing, return FALSE. - if (count($this->dirListing) <= $this->dirIndex) { - return FALSE; - } - - $curr = $this->dirListing[$this->dirIndex]; - $this->dirIndex++; - - if ($curr instanceof \OpenStack\Storage\ObjectStorage\Subdir) { - $fullpath = $curr->path(); - } - else { - $fullpath = $curr->name(); - } - - if (!empty($this->dirPrefix)) { - $len = strlen($this->dirPrefix); - $fullpath = substr($fullpath, $len); - } - return $fullpath; - } - - /** - * Rewind to the beginning of the listing. - * - * This repositions the read pointer at the first entry in the directory. - * - * - */ - public function dir_rewinddir() { - $this->dirIndex = 0; - } - - /* - public function mkdir($path, $mode, $options) { - - } - - public function rmdir($path, $options) { - - } - */ - - /** - * Rename a swift object. - * - * This works by copying the object (metadata) and - * then removing the original version. - * - * This DOES support cross-container renaming. - * - * @see Container::copy(). - * - * 'foo@example.com', - * // 'tenantid' => '1234', // You can use this instead of tenantname - * 'username' => 'foobar', - * 'password' => 'baz', - * 'endpoint' => 'https://auth.example.com', - * )); - * - * $from = 'swift://containerOne/file.txt'; - * $to = 'swift://containerTwo/file.txt'; - * - * // Rename can also take a context as a third param. - * rename($from, $to); - * - * ?> - * - * @param string $path_from A swift URL that exists on the remote. - * @param string $path_to A swift URL to another path. - * - * @return boolean TRUE on success, FALSE otherwise. - */ - public function rename($path_from, $path_to) { - $this->initializeObjectStorage(); - $src = $this->parseUrl($path_from); - $dest = $this->parseUrl($path_to); - - if ($src['scheme'] != $dest['scheme']) { - trigger_error("I'm too stupid to copy across protocols.", E_USER_WARNING); - } - - if ( empty($src['host']) || empty($src['path']) - || empty($dest['host']) || empty($dest['path'])) { - trigger_error('Container and path are required for both source and destination URLs.', E_USER_WARNING); - return FALSE; - } - - try { - $container = $this->store->container($src['host']); - - $object = $container->remoteObject($src['path']); - - $ret = $container->copy($object, $dest['path'], $dest['host']); - if ($ret) { - return $container->delete($src['path']); - } - } - catch (\OpenStack\Exception $e) { - trigger_error('Rename was not completed: ' . $e->getMessage(), E_USER_WARNING); - return FALSE; - } - } - - /* - public function copy($path_from, $path_to) { - throw new \Exception("UNDOCUMENTED."); - } - */ - - /** - * Cast stream into a lower-level stream. - * - * This is used for stream_select() and perhaps others.Because it exposes - * the lower-level buffer objects, this function can have unexpected - * side effects. - * - * @return resource this returns the underlying stream. - */ - public function stream_cast($cast_as) { - return $this->objStream; - } - - /** - * Close a stream, writing if necessary. - * - * - * - * This will close the present stream. Importantly, - * this will also write to the remote object storage if - * any changes have been made locally. - * - * @see stream_open(). - */ - public function stream_close() { - - try { - $this->writeRemote(); - } - catch (\OpenStack\Exception $e) { - trigger_error('Error while closing: ' . $e->getMessage(), E_USER_NOTICE); - return FALSE; - } - - // Force-clear the memory hogs. - unset($this->obj); - fclose($this->objStream); - } - - /** - * Check whether the stream has reached its end. - * - * This checks whether the stream has reached the - * end of the object's contents. - * - * Called when `feof()` is called on a stream. - * - * @see stream_seek(). - * - * @return boolean TRUE if it has reached the end, FALSE otherwise. - */ - public function stream_eof() { - return feof($this->objStream); - } - - /** - * Initiate saving data on the remote object storage. - * - * If the local copy of this object has been modified, - * it is written remotely. - * - * Called when `fflush()` is called on a stream. - */ - public function stream_flush() { - try { - $this->writeRemote(); - } - catch (\OpenStack\Exception $e) { - syslog(LOG_WARNING, $e); - trigger_error('Error while flushing: ' . $e->getMessage(), E_USER_NOTICE); - return FALSE; - } - } - - /** - * Write data to the remote object storage. - * - * Internally, this is used by flush and close. - */ - protected function writeRemote() { - - $contentType = $this->cxt('content_type'); - if (!empty($contentType)) { - $this->obj->setContentType($contentType); - } - - // Skip debug streams. - if ($this->isNeverDirty) { - return; - } - - // Stream is dirty and needs a write. - if ($this->isDirty) { - $position = ftell($this->objStream); - - rewind($this->objStream); - $this->container->save($this->obj, $this->objStream); - - fseek($this->objStream, SEEK_SET, $position); - - } - $this->isDirty = FALSE; - } - - /* - * Locking is currently unsupported. - * - * There is no remote support for locking a - * file. - public function stream_lock($operation) { - - } - */ - - /** - * Open a stream resource. - * - * This opens a given stream resource and prepares it for reading or writing. - * - * 'foobar', - * 'tenantid' => '987654321', - * 'password' => 'eieio', - * 'endpoint' => 'https://auth.example.com', - * )); - * ?> - * - * $file = fopen('swift://myContainer/myObject.csv', 'rb', FALSE, $cxt); - * while ($bytes = fread($file, 8192)) { - * print $bytes; - * } - * fclose($file); - * ?> - * - * If a file is opened in write mode, its contents will be retrieved from the - * remote storage and cached locally for manipulation. If the file is opened - * in a write-only mode, the contents will be created locally and then pushed - * remotely as necessary. - * - * During this operation, the remote host may need to be contacted for - * authentication as well as for file retrieval. - * - * @param string $path The URL to the resource. See the class description for - * details, but typically this expects URLs in the form `swift://CONTAINER/OBJECT`. - * @param string $mode Any of the documented mode strings. See fopen(). For - * any file that is in a writing mode, the file will be saved remotely on - * flush or close. Note that there is an extra mode: 'nope'. It acts like - * 'c+' except that it is never written remotely. This is useful for - * debugging the stream locally without sending that data to object storage. - * (Note that data is still fetched -- just never written.) - * @param int $options An OR'd list of options. Only STREAM_REPORT_ERRORS has - * any meaning to this wrapper, as it is not working with local files. - * @param string $opened_path This is not used, as this wrapper deals only - * with remote objects. - */ - public function stream_open($path, $mode, $options, &$opened_path) { - - //syslog(LOG_WARNING, "I received this URL: " . $path); - - // If STREAM_REPORT_ERRORS is set, we are responsible for - // all error handling while opening the stream. - if (STREAM_REPORT_ERRORS & $options) { - $this->triggerErrors = TRUE; - } - - // Using the mode string, set the internal mode. - $this->setMode($mode); - - // Parse the URL. - $url = $this->parseUrl($path); - //syslog(LOG_WARNING, print_r($url, TRUE)); - - // Container name is required. - if (empty($url['host'])) { - //if ($this->triggerErrors) { - trigger_error('No container name was supplied in ' . $path, E_USER_WARNING); - //} - return FALSE; - } - - // A path to an object is required. - if (empty($url['path'])) { - //if ($this->triggerErrors) { - trigger_error('No object name was supplied in ' . $path, E_USER_WARNING); - //} - return FALSE; - } - - // We set this because it is possible to bind another scheme name, - // and we need to know that name if it's changed. - //$this->schemeName = isset($url['scheme']) ? $url['scheme'] : self::DEFAULT_SCHEME; - if (isset($url['scheme'])) { - $this->schemeName == $url['scheme']; - } - - // Now we find out the container name. We walk a fine line here, because we don't - // create a new container, but we don't want to incur heavy network - // traffic, either. So we have to assume that we have a valid container - // until we issue our first request. - $containerName = $url['host']; - - // Object name. - $objectName = $url['path']; - - // XXX: We reserve the query string for passing additional params. - - try { - $this->initializeObjectStorage(); - } - catch (\OpenStack\Exception $e) { - trigger_error('Failed to init object storage: ' . $e->getMessage(), E_USER_WARNING); - return FALSE; - } - - //syslog(LOG_WARNING, "Container: " . $containerName); - - // Now we need to get the container. Doing a server round-trip here gives - // us the peace of mind that we have an actual container. - // XXX: Should we make it possible to get a container blindly, without the - // server roundtrip? - try { - $this->container = $this->store->container($containerName); - } - catch (\OpenStack\Transport\FileNotFoundException $e) { - trigger_error('Container not found.', E_USER_WARNING); - return FALSE; - } - - try{ - // Now we fetch the file. Only under certain circumstances do we generate - // an error if the file is not found. - // FIXME: We should probably allow a context param that can be set to - // mark the file as lazily fetched. - $this->obj = $this->container->object($objectName); - $stream = $this->obj->stream(); - $streamMeta = stream_get_meta_data($stream); - - // Support 'x' and 'x+' modes. - if ($this->noOverwrite) { - //if ($this->triggerErrors) { - trigger_error('File exists and cannot be overwritten.', E_USER_WARNING); - //} - return FALSE; - } - - // If we need to write to it, we need a writable - // stream. Also, if we need to block reading, this - // will require creating an alternate stream. - if ($this->isWriting && ($streamMeta['mode'] == 'r' || !$this->isReading)) { - $newMode = $this->isReading ? 'rb+' : 'wb'; - $tmpStream = fopen('php://temp', $newMode); - stream_copy_to_stream($stream, $tmpStream); - - // Skip rewinding if we can. - if (!$this->isAppending) { - rewind($tmpStream); + return FALSE; } - $this->objStream = $tmpStream; - } - else { - $this->objStream = $this->obj->stream(); - } + try { + $this->initializeObjectStorage(); + $container = $this->store->container($url['host']); - // Append mode requires seeking to the end. - if ($this->isAppending) { - fseek($this->objStream, -1, SEEK_END); - } + if (empty($url['path'])) { + $this->dirPrefix = ''; + } else { + $this->dirPrefix = $url['path']; + } + + $sep = '/'; + + $this->dirListing = $container->objectsWithPrefix($this->dirPrefix, $sep); + } catch (\OpenStack\Exception $e) { + trigger_error('Directory could not be opened: ' . $e->getMessage(), E_USER_WARNING); + + return FALSE; + } + + return TRUE; } - // If a 404 is thrown, we need to determine whether - // or not a new file should be created. - catch (\OpenStack\Transport\FileNotFoundException $nf) { + /** + * Read an entry from the directory. + * + * This gets a single line from the directory. + * + * + * + * @return string The name of the resource or FALSE when the directory has no + * more entries. + */ + public function dir_readdir() + { + // If we are at the end of the listing, return FALSE. + if (count($this->dirListing) <= $this->dirIndex) { + return FALSE; + } - // For many modes, we just go ahead and create. - if ($this->createIfNotFound) { - $this->obj = new Object($objectName); - $this->objStream = fopen('php://temp', 'rb+'); + $curr = $this->dirListing[$this->dirIndex]; + $this->dirIndex++; + + if ($curr instanceof \OpenStack\Storage\ObjectStorage\Subdir) { + $fullpath = $curr->path(); + } else { + $fullpath = $curr->name(); + } + + if (!empty($this->dirPrefix)) { + $len = strlen($this->dirPrefix); + $fullpath = substr($fullpath, $len); + } + + return $fullpath; + } + + /** + * Rewind to the beginning of the listing. + * + * This repositions the read pointer at the first entry in the directory. + * + * + */ + public function dir_rewinddir() + { + $this->dirIndex = 0; + } + + /* + public function mkdir($path, $mode, $options) + { + } + + public function rmdir($path, $options) + { + } + */ + + /** + * Rename a swift object. + * + * This works by copying the object (metadata) and + * then removing the original version. + * + * This DOES support cross-container renaming. + * + * @see Container::copy(). + * + * 'foo@example.com', + * // 'tenantid' => '1234', // You can use this instead of tenantname + * 'username' => 'foobar', + * 'password' => 'baz', + * 'endpoint' => 'https://auth.example.com', + * )); + * + * $from = 'swift://containerOne/file.txt'; + * $to = 'swift://containerTwo/file.txt'; + * + * // Rename can also take a context as a third param. + * rename($from, $to); + * + * ?> + * + * @param string $path_from A swift URL that exists on the remote. + * @param string $path_to A swift URL to another path. + * + * @return boolean TRUE on success, FALSE otherwise. + */ + public function rename($path_from, $path_to) + { + $this->initializeObjectStorage(); + $src = $this->parseUrl($path_from); + $dest = $this->parseUrl($path_to); + + if ($src['scheme'] != $dest['scheme']) { + trigger_error("I'm too stupid to copy across protocols.", E_USER_WARNING); + } + + if ( empty($src['host']) || empty($src['path']) + || empty($dest['host']) || empty($dest['path'])) { + trigger_error('Container and path are required for both source and destination URLs.', E_USER_WARNING); + + return FALSE; + } + + try { + $container = $this->store->container($src['host']); + + $object = $container->remoteObject($src['path']); + + $ret = $container->copy($object, $dest['path'], $dest['host']); + if ($ret) { + return $container->delete($src['path']); + } + } catch (\OpenStack\Exception $e) { + trigger_error('Rename was not completed: ' . $e->getMessage(), E_USER_WARNING); + + return FALSE; + } + } + + /* + public function copy($path_from, $path_to) + { + throw new \Exception("UNDOCUMENTED."); + } + */ + + /** + * Cast stream into a lower-level stream. + * + * This is used for stream_select() and perhaps others.Because it exposes + * the lower-level buffer objects, this function can have unexpected + * side effects. + * + * @return resource this returns the underlying stream. + */ + public function stream_cast($cast_as) + { + return $this->objStream; + } + + /** + * Close a stream, writing if necessary. + * + * + * + * This will close the present stream. Importantly, + * this will also write to the remote object storage if + * any changes have been made locally. + * + * @see stream_open(). + */ + public function stream_close() + { + try { + $this->writeRemote(); + } catch (\OpenStack\Exception $e) { + trigger_error('Error while closing: ' . $e->getMessage(), E_USER_NOTICE); + + return FALSE; + } + + // Force-clear the memory hogs. + unset($this->obj); + fclose($this->objStream); + } + + /** + * Check whether the stream has reached its end. + * + * This checks whether the stream has reached the + * end of the object's contents. + * + * Called when `feof()` is called on a stream. + * + * @see stream_seek(). + * + * @return boolean TRUE if it has reached the end, FALSE otherwise. + */ + public function stream_eof() + { + return feof($this->objStream); + } + + /** + * Initiate saving data on the remote object storage. + * + * If the local copy of this object has been modified, + * it is written remotely. + * + * Called when `fflush()` is called on a stream. + */ + public function stream_flush() + { + try { + $this->writeRemote(); + } catch (\OpenStack\Exception $e) { + syslog(LOG_WARNING, $e); + trigger_error('Error while flushing: ' . $e->getMessage(), E_USER_NOTICE); + + return FALSE; + } + } + + /** + * Write data to the remote object storage. + * + * Internally, this is used by flush and close. + */ + protected function writeRemote() + { + $contentType = $this->cxt('content_type'); + if (!empty($contentType)) { + $this->obj->setContentType($contentType); + } + + // Skip debug streams. + if ($this->isNeverDirty) { + return; + } + + // Stream is dirty and needs a write. + if ($this->isDirty) { + $position = ftell($this->objStream); + + rewind($this->objStream); + $this->container->save($this->obj, $this->objStream); + + fseek($this->objStream, SEEK_SET, $position); + + } + $this->isDirty = FALSE; + } + + /* + * Locking is currently unsupported. + * + * There is no remote support for locking a + * file. + public function stream_lock($operation) + { + } + */ + + /** + * Open a stream resource. + * + * This opens a given stream resource and prepares it for reading or writing. + * + * 'foobar', + * 'tenantid' => '987654321', + * 'password' => 'eieio', + * 'endpoint' => 'https://auth.example.com', + * )); + * ?> + * + * $file = fopen('swift://myContainer/myObject.csv', 'rb', FALSE, $cxt); + * while ($bytes = fread($file, 8192)) { + * print $bytes; + * } + * fclose($file); + * ?> + * + * If a file is opened in write mode, its contents will be retrieved from the + * remote storage and cached locally for manipulation. If the file is opened + * in a write-only mode, the contents will be created locally and then pushed + * remotely as necessary. + * + * During this operation, the remote host may need to be contacted for + * authentication as well as for file retrieval. + * + * @param string $path The URL to the resource. See the class description for + * details, but typically this expects URLs in the form `swift://CONTAINER/OBJECT`. + * @param string $mode Any of the documented mode strings. See fopen(). For + * any file that is in a writing mode, the file will be saved remotely on + * flush or close. Note that there is an extra mode: 'nope'. It acts like + * 'c+' except that it is never written remotely. This is useful for + * debugging the stream locally without sending that data to object storage. + * (Note that data is still fetched -- just never written.) + * @param int $options An OR'd list of options. Only STREAM_REPORT_ERRORS has + * any meaning to this wrapper, as it is not working with local files. + * @param string $opened_path This is not used, as this wrapper deals only + * with remote objects. + */ + public function stream_open($path, $mode, $options, &$opened_path) + { + //syslog(LOG_WARNING, "I received this URL: " . $path); + + // If STREAM_REPORT_ERRORS is set, we are responsible for + // all error handling while opening the stream. + if (STREAM_REPORT_ERRORS & $options) { + $this->triggerErrors = TRUE; + } + + // Using the mode string, set the internal mode. + $this->setMode($mode); + + // Parse the URL. + $url = $this->parseUrl($path); + //syslog(LOG_WARNING, print_r($url, TRUE)); + + // Container name is required. + if (empty($url['host'])) { + //if ($this->triggerErrors) { + trigger_error('No container name was supplied in ' . $path, E_USER_WARNING); + //} + return FALSE; + } + + // A path to an object is required. + if (empty($url['path'])) { + //if ($this->triggerErrors) { + trigger_error('No object name was supplied in ' . $path, E_USER_WARNING); + //} + return FALSE; + } + + // We set this because it is possible to bind another scheme name, + // and we need to know that name if it's changed. + //$this->schemeName = isset($url['scheme']) ? $url['scheme'] : self::DEFAULT_SCHEME; + if (isset($url['scheme'])) { + $this->schemeName == $url['scheme']; + } + + // Now we find out the container name. We walk a fine line here, because we don't + // create a new container, but we don't want to incur heavy network + // traffic, either. So we have to assume that we have a valid container + // until we issue our first request. + $containerName = $url['host']; + + // Object name. + $objectName = $url['path']; + + // XXX: We reserve the query string for passing additional params. + + try { + $this->initializeObjectStorage(); + } catch (\OpenStack\Exception $e) { + trigger_error('Failed to init object storage: ' . $e->getMessage(), E_USER_WARNING); + + return FALSE; + } + + //syslog(LOG_WARNING, "Container: " . $containerName); + + // Now we need to get the container. Doing a server round-trip here gives + // us the peace of mind that we have an actual container. + // XXX: Should we make it possible to get a container blindly, without the + // server roundtrip? + try { + $this->container = $this->store->container($containerName); + } catch (\OpenStack\Transport\FileNotFoundException $e) { + trigger_error('Container not found.', E_USER_WARNING); + + return FALSE; + } + + try { + // Now we fetch the file. Only under certain circumstances do we generate + // an error if the file is not found. + // FIXME: We should probably allow a context param that can be set to + // mark the file as lazily fetched. + $this->obj = $this->container->object($objectName); + $stream = $this->obj->stream(); + $streamMeta = stream_get_meta_data($stream); + + // Support 'x' and 'x+' modes. + if ($this->noOverwrite) { + //if ($this->triggerErrors) { + trigger_error('File exists and cannot be overwritten.', E_USER_WARNING); + //} + return FALSE; + } + + // If we need to write to it, we need a writable + // stream. Also, if we need to block reading, this + // will require creating an alternate stream. + if ($this->isWriting && ($streamMeta['mode'] == 'r' || !$this->isReading)) { + $newMode = $this->isReading ? 'rb+' : 'wb'; + $tmpStream = fopen('php://temp', $newMode); + stream_copy_to_stream($stream, $tmpStream); + + // Skip rewinding if we can. + if (!$this->isAppending) { + rewind($tmpStream); + } + + $this->objStream = $tmpStream; + } else { + $this->objStream = $this->obj->stream(); + } + + // Append mode requires seeking to the end. + if ($this->isAppending) { + fseek($this->objStream, -1, SEEK_END); + } + } + + // If a 404 is thrown, we need to determine whether + // or not a new file should be created. + catch (\OpenStack\Transport\FileNotFoundException $nf) { + + // For many modes, we just go ahead and create. + if ($this->createIfNotFound) { + $this->obj = new Object($objectName); + $this->objStream = fopen('php://temp', 'rb+'); + $this->isDirty = TRUE; + } else { + //if ($this->triggerErrors) { + trigger_error($nf->getMessage(), E_USER_WARNING); + //} + return FALSE; + } + + } + // All other exceptions are fatal. + catch (\OpenStack\Exception $e) { + //if ($this->triggerErrors) { + trigger_error('Failed to fetch object: ' . $e->getMessage(), E_USER_WARNING); + //} + return FALSE; + } + + // At this point, we have a file that may be read-only. It also may be + // reading off of a socket. It will be positioned at the beginning of + // the stream. + return TRUE; + } + + /** + * Read N bytes from the stream. + * + * This will read up to the requested number of bytes. Or, upon + * hitting the end of the file, it will return NULL. + * + * @see fread(), fgets(), and so on for examples. + * + * 'me@example.com', + * 'username' => 'me@example.com', + * 'password' => 'secret', + * 'endpoint' => 'https://auth.example.com', + * )); + * + * $content = file_get_contents('swift://public/myfile.txt', FALSE, $cxt); + * ?> + * + * @param int $count The number of bytes to read (usually 8192). + * + * @return string The data read. + */ + public function stream_read($count) + { + return fread($this->objStream, $count); + } + + /** + * Perform a seek. + * + * This is called whenever `fseek()` or `rewind()` is called on a + * Swift stream. + * + * IMPORTANT: Unlike the PHP core, this library + * allows you to `fseek()` inside of a file opened + * in append mode ('a' or 'a+'). + */ + public function stream_seek($offset, $whence) + { + $ret = fseek($this->objStream, $offset, $whence); + + // fseek returns 0 for success, -1 for failure. + // We need to return TRUE for success, FALSE for failure. + return $ret === 0; + } + + /** + * Set options on the underlying stream. + * + * The settings here do not trickle down to the network socket, which is + * left open for only a brief period of time. Instead, they impact the middle + * buffer stream, where the file is read and written to between flush/close + * operations. Thus, tuning these will not have any impact on network + * performance. + * + * See stream_set_blocking(), stream_set_timeout(), and stream_write_buffer(). + */ + public function stream_set_option($option, $arg1, $arg2) + { + switch ($option) { + case STREAM_OPTION_BLOCKING: + return stream_set_blocking($this->objStream, $arg1); + case STREAM_OPTION_READ_TIMEOUT: + // XXX: Should this have any effect on the lower-level + // socket, too? Or just the buffered tmp storage? + return stream_set_timeout($this->objStream, $arg1, $arg2); + case STREAM_OPTION_WRITE_BUFFER: + return stream_set_write_buffer($this->objStream, $arg2); + } + + } + + /** + * Perform stat()/lstat() operations. + * + * + * + * To use standard `stat()` on a Swift stream, you will + * need to set account information (tenant ID, username, password, + * etc.) through \OpenStack\Bootstrap::setConfiguration(). + * + * @return array The stats array. + */ + public function stream_stat() + { + $stat = fstat($this->objStream); + + // FIXME: Need to calculate the length of the $objStream. + //$contentLength = $this->obj->contentLength(); + $contentLength = $stat['size']; + + return $this->generateStat($this->obj, $this->container, $contentLength); + } + + /** + * Get the current position in the stream. + * + * @see `ftell()` and `fseek()`. + * + * @return int The current position in the stream. + */ + public function stream_tell() + { + return ftell($this->objStream); + } + + /** + * Write data to stream. + * + * This writes data to the local stream buffer. Data + * is not pushed remotely until stream_close() or + * stream_flush() is called. + * + * @param string $data Data to write to the stream. + * + * @return int The number of bytes written. 0 indicates and error. + */ + public function stream_write($data) + { $this->isDirty = TRUE; - } - else { - //if ($this->triggerErrors) { - trigger_error($nf->getMessage(), E_USER_WARNING); - //} - return FALSE; - } - } - // All other exceptions are fatal. - catch (\OpenStack\Exception $e) { - //if ($this->triggerErrors) { - trigger_error('Failed to fetch object: ' . $e->getMessage(), E_USER_WARNING); - //} - return FALSE; + return fwrite($this->objStream, $data); } - // At this point, we have a file that may be read-only. It also may be - // reading off of a socket. It will be positioned at the beginning of - // the stream. + /** + * Unlink a file. + * + * This removes the remote copy of the file. Like a normal unlink operation, + * it does not destroy the (local) file handle until the file is closed. + * Therefore you can continue accessing the object locally. + * + * Note that OpenStack Swift does not draw a distinction between file objects + * and "directory" objects (where the latter is a 0-byte object). This will + * delete either one. If you are using directory markers, not that deleting + * a marker will NOT delete the contents of the "directory". + * + * You will need to use \OpenStack\Bootstrap::setConfiguration() to set the + * necessary stream configuration, since `unlink()` does not take a context. + * + * @param string $path The URL. + * + * @return boolean TRUE if the file was deleted, FALSE otherwise. + */ + public function unlink($path) + { + $url = $this->parseUrl($path); - return TRUE; - } + // Host is required. + if (empty($url['host'])) { + trigger_error('Container name is required.', E_USER_WARNING); - /** - * Read N bytes from the stream. - * - * This will read up to the requested number of bytes. Or, upon - * hitting the end of the file, it will return NULL. - * - * @see fread(), fgets(), and so on for examples. - * - * 'me@example.com', - * 'username' => 'me@example.com', - * 'password' => 'secret', - * 'endpoint' => 'https://auth.example.com', - * )); - * - * $content = file_get_contents('swift://public/myfile.txt', FALSE, $cxt); - * ?> - * - * @param int $count The number of bytes to read (usually 8192). - * - * @return string The data read. - */ - public function stream_read($count) { - return fread($this->objStream, $count); - } - - /** - * Perform a seek. - * - * This is called whenever `fseek()` or `rewind()` is called on a - * Swift stream. - * - * IMPORTANT: Unlike the PHP core, this library - * allows you to `fseek()` inside of a file opened - * in append mode ('a' or 'a+'). - */ - public function stream_seek($offset, $whence) { - $ret = fseek($this->objStream, $offset, $whence); - - // fseek returns 0 for success, -1 for failure. - // We need to return TRUE for success, FALSE for failure. - return $ret === 0; - } - - /** - * Set options on the underlying stream. - * - * The settings here do not trickle down to the network socket, which is - * left open for only a brief period of time. Instead, they impact the middle - * buffer stream, where the file is read and written to between flush/close - * operations. Thus, tuning these will not have any impact on network - * performance. - * - * See stream_set_blocking(), stream_set_timeout(), and stream_write_buffer(). - */ - public function stream_set_option($option, $arg1, $arg2) { - switch ($option) { - case STREAM_OPTION_BLOCKING: - return stream_set_blocking($this->objStream, $arg1); - case STREAM_OPTION_READ_TIMEOUT: - // XXX: Should this have any effect on the lower-level - // socket, too? Or just the buffered tmp storage? - return stream_set_timeout($this->objStream, $arg1, $arg2); - case STREAM_OPTION_WRITE_BUFFER: - return stream_set_write_buffer($this->objStream, $arg2); - } - - } - - /** - * Perform stat()/lstat() operations. - * - * - * - * To use standard `stat()` on a Swift stream, you will - * need to set account information (tenant ID, username, password, - * etc.) through \OpenStack\Bootstrap::setConfiguration(). - * - * @return array The stats array. - */ - public function stream_stat() { - $stat = fstat($this->objStream); - - // FIXME: Need to calculate the length of the $objStream. - //$contentLength = $this->obj->contentLength(); - $contentLength = $stat['size']; - - return $this->generateStat($this->obj, $this->container, $contentLength); - } - - /** - * Get the current position in the stream. - * - * @see `ftell()` and `fseek()`. - * - * @return int The current position in the stream. - */ - public function stream_tell() { - return ftell($this->objStream); - } - - /** - * Write data to stream. - * - * This writes data to the local stream buffer. Data - * is not pushed remotely until stream_close() or - * stream_flush() is called. - * - * @param string $data Data to write to the stream. - * - * @return int The number of bytes written. 0 indicates and error. - */ - public function stream_write($data) { - $this->isDirty = TRUE; - return fwrite($this->objStream, $data); - } - - /** - * Unlink a file. - * - * This removes the remote copy of the file. Like a normal unlink operation, - * it does not destroy the (local) file handle until the file is closed. - * Therefore you can continue accessing the object locally. - * - * Note that OpenStack Swift does not draw a distinction between file objects - * and "directory" objects (where the latter is a 0-byte object). This will - * delete either one. If you are using directory markers, not that deleting - * a marker will NOT delete the contents of the "directory". - * - * You will need to use \OpenStack\Bootstrap::setConfiguration() to set the - * necessary stream configuration, since `unlink()` does not take a context. - * - * @param string $path The URL. - * - * @return boolean TRUE if the file was deleted, FALSE otherwise. - */ - public function unlink($path) { - $url = $this->parseUrl($path); - - // Host is required. - if (empty($url['host'])) { - trigger_error('Container name is required.', E_USER_WARNING); - return FALSE; - } - - // I suppose we could allow deleting containers, - // but that isn't really the purpose of the - // stream wrapper. - if (empty($url['path'])) { - trigger_error('Path is required.', E_USER_WARNING); - return FALSE; - } - - try { - $this->initializeObjectStorage(); - // $container = $this->store->container($url['host']); - $name = $url['host']; - $token = $this->store->token(); - $endpoint_url = $this->store->url() . '/' . rawurlencode($name); - $client = $this->cxt('transport_client', NULL); - $container = new \OpenStack\Storage\ObjectStorage\Container($name, $endpoint_url, $token, $client); - return $container->delete($url['path']); - } - catch (\OpenStack\Exception $e) { - trigger_error('Error during unlink: ' . $e->getMessage(), E_USER_WARNING); - return FALSE; - } - - } - - /** - * @see stream_stat(). - */ - public function url_stat($path, $flags) { - $url = $this->parseUrl($path); - - if (empty($url['host']) || empty($url['path'])) { - if ($flags & STREAM_URL_STAT_QUIET) { - trigger_error('Container name (host) and path are required.', E_USER_WARNING); - } - return FALSE; - } - - try { - $this->initializeObjectStorage(); - - // Since we are throwing the $container away without really using its - // internals, we create an unchecked container. It may not actually - // exist on the server, which will cause a 404 error. - //$container = $this->store->container($url['host']); - $name = $url['host']; - $token = $this->store->token(); - $endpoint_url = $this->store->url() . '/' . rawurlencode($name); - $client = $this->cxt('transport_client', NULL); - $container = new \OpenStack\Storage\ObjectStorage\Container($name, $endpoint_url, $token, $client); - $obj = $container->remoteObject($url['path']); - } - catch(\OpenStack\Exception $e) { - // Apparently file_exists does not set STREAM_URL_STAT_QUIET. - //if ($flags & STREAM_URL_STAT_QUIET) { - //trigger_error('Could not stat remote file: ' . $e->getMessage(), E_USER_WARNING); - //} - return FALSE; - } - - if ($flags & STREAM_URL_STAT_QUIET) { - try { - return @$this->generateStat($obj, $container, $obj->contentLength()); - } - catch (\OpenStack\Exception $e) { - return FALSE; - } - } - return $this->generateStat($obj, $container, $obj->contentLength()); - } - - /** - * Get the Object. - * - * This provides low-level access to the - * \OpenStack\Storage\ObjectStorage::Object instance in which the content - * is stored. - * - * Accessing the object's payload (Object::content()) is strongly - * discouraged, as it will modify the pointers in the stream that the - * stream wrapper is using. - * - * HOWEVER, accessing the Object's metadata properties, content type, - * and so on is okay. Changes to this data will be written on the next - * flush, provided that the file stream data has also been changed. - * - * To access this: - * - * object(); - * ?> - */ - public function object() { - return $this->obj; - } - - /** - * EXPERT: Get the ObjectStorage for this wrapper. - * - * @return object \OpenStack\ObjectStorage An ObjectStorage object. - * @see object() - */ - public function objectStorage() { - return $this->store; - } - - /** - * EXPERT: Get the auth token for this wrapper. - * - * @return string A token. - * @see object() - */ - public function token() { - return $this->store->token(); - } - - /** - * EXPERT: Get the service catalog (IdentityService) for this wrapper. - * - * This is only available when a file is opened via fopen(). - * - * @return array A service catalog. - * @see object() - */ - public function serviceCatalog() { - return self::$serviceCatalogCache[$this->token()]; - } - - /** - * Generate a reasonably accurate STAT array. - * - * Notes on mode: - * - All modes are of the (octal) form 100XXX, where - * XXX is replaced by the permission string. Thus, - * this always reports that the type is "file" (100). - * - Currently, only two permission sets are generated: - * - 770: Represents the ACL::makePrivate() perm. - * - 775: Represents the ACL::makePublic() perm. - * - * Notes on mtime/atime/ctime: - * - For whatever reason, Swift only stores one timestamp. - * We use that for mtime, atime, and ctime. - * - * Notes on size: - * - Size must be calculated externally, as it will sometimes - * be the remote's Content-Length, and it will sometimes be - * the cached stat['size'] for the underlying buffer. - */ - protected function generateStat($object, $container, $size) { - // This is not entirely accurate. Basically, if the - // file is marked public, it gets 100775, and if - // it is private, it gets 100770. - // - // Mode is always set to file (100XXX) because there - // is no alternative that is more sensible. PHP docs - // do not recommend an alternative. - // - // octdec(100770) == 33272 - // octdec(100775) == 33277 - $mode = $container->acl()->isPublic() ? 33277 : 33272; - - // We have to fake the UID value in order for is_readible()/is_writable() - // to work. Note that on Windows systems, stat does not look for a UID. - if (function_exists('posix_geteuid')) { - $uid = posix_geteuid(); - $gid = posix_getegid(); - } - else { - $uid = 0; - $gid = 0; - } - - if ($object instanceof \OpenStack\Storage\ObjectStorage\RemoteObject) { - $modTime = $object->lastModified(); - } - else { - $modTime = 0; - } - $values = array( - 'dev' => 0, - 'ino' => 0, - 'mode' => $mode, - 'nlink' => 0, - 'uid' => $uid, - 'gid' => $gid, - 'rdev' => 0, - 'size' => $size, - 'atime' => $modTime, - 'mtime' => $modTime, - 'ctime' => $modTime, - 'blksize' => -1, - 'blocks' => -1, - ); - - $final = array_values($values) + $values; - - return $final; - - } - - /////////////////////////////////////////////////////////////////// - // INTERNAL METHODS - // All methods beneath this line are not part of the Stream API. - /////////////////////////////////////////////////////////////////// - - /** - * Set the fopen mode. - * - * @param string $mode The mode string, e.g. `r+` or `wb`. - * - * @return \OpenStack\Storage\ObjectStorage\StreamWrapper $this so the method - * can be used in chaining. - */ - protected function setMode($mode) { - $mode = strtolower($mode); - - // These are largely ignored, as the remote - // object storage does not distinguish between - // text and binary files. Per the PHP recommendation - // files are treated as binary. - $this->isBinary = strpos($mode, 'b') !== FALSE; - $this->isText = strpos($mode, 't') !== FALSE; - - // Rewrite mode to remove b or t: - $mode = preg_replace('/[bt]?/', '', $mode); - - switch ($mode) { - case 'r+': - $this->isWriting = TRUE; - case 'r': - $this->isReading = TRUE; - $this->createIfNotFound = FALSE; - break; - - - case 'w+': - $this->isReading = TRUE; - case 'w': - $this->isTruncating = TRUE; - $this->isWriting = TRUE; - break; - - - case 'a+': - $this->isReading = TRUE; - case 'a': - $this->isAppending = TRUE; - $this->isWriting = TRUE; - break; - - - case 'x+': - $this->isReading = TRUE; - case 'x': - $this->isWriting = TRUE; - $this->noOverwrite = TRUE; - break; - - case 'c+': - $this->isReading = TRUE; - case 'c': - $this->isWriting = TRUE; - break; - - // nope mode: Mock read/write support, - // but never write to the remote server. - // (This is accomplished by never marking - // the stream as dirty.) - case 'nope': - $this->isReading = TRUE; - $this->isWriting = TRUE; - $this->isNeverDirty = TRUE; - break; - - // Default case is read/write - // like c+. - default: - $this->isReading = TRUE; - $this->isWriting = TRUE; - break; - - } - - return $this; - } - - /** - * Get an item out of the context. - * - * @todo Should there be an option to NOT query the Bootstrap::conf()? - * - * @param string $name The name to look up. First look it up in the context, - * then look it up in the Bootstrap config. - * @param mixed $default The default value to return if no config param was - * found. - * - * @return mixed The discovered result, or $default if specified, or NULL if - * no $default is specified. - */ - protected function cxt($name, $default = NULL) { - - // Lazilly populate the context array. - if (is_resource($this->context) && empty($this->contextArray)) { - $cxt = stream_context_get_options($this->context); - - // If a custom scheme name has been set, use that. - if (!empty($cxt[$this->schemeName])) { - $this->contextArray = $cxt[$this->schemeName]; - } - // We fall back to this just in case. - elseif (!empty($cxt[self::DEFAULT_SCHEME])) { - $this->contextArray = $cxt[self::DEFAULT_SCHEME]; - } - } - - // Should this be array_key_exists()? - if (isset($this->contextArray[$name])) { - return $this->contextArray[$name]; - } - - // Check to see if the value can be gotten from - // \OpenStack\Bootstrap. - $val = \OpenStack\Bootstrap::config($name, NULL); - if (isset($val)) { - return $val; - } - - return $default; - } - - /** - * Parse a URL. - * - * In order to provide full UTF-8 support, URLs must be - * rawurlencoded before they are passed into the stream wrapper. - * - * This parses the URL and urldecodes the container name and - * the object name. - * - * @param string $url A Swift URL. - * - * @return array An array as documented in parse_url(). - */ - protected function parseUrl($url) { - $res = parse_url($url); - - - // These have to be decode because they will later - // be encoded. - foreach ($res as $key => $val) { - if ($key == 'host') { - $res[$key] = urldecode($val); - } - elseif ($key == 'path') { - if (strpos($val, '/') === 0) { - $val = substr($val, 1); + return FALSE; } - $res[$key] = urldecode($val); - } - } - return $res; - } + // I suppose we could allow deleting containers, + // but that isn't really the purpose of the + // stream wrapper. + if (empty($url['path'])) { + trigger_error('Path is required.', E_USER_WARNING); - /** - * Based on the context, initialize the ObjectStorage. - * - * The following parameters may be set either in the stream context - * or through \OpenStack\Bootstrap::setConfiguration(): - * - * - token: An auth token. If this is supplied, authentication is skipped and - * this token is used. NOTE: You MUST set swift_endpoint if using this - * option. - * - swift_endpoint: The URL to the swift instance. This is only necessary if - * 'token' is set. Otherwise it is ignored. - * - username: A username. MUST be accompanied by 'password' and 'tenantname'. - * - password: A password. MUST be accompanied by 'username' and 'tenantname'. - * - endpoint: The URL to the authentication endpoint. Necessary if you are not - * using a 'token' and 'swift_endpoint'. - * - use_swift_auth: If this is set to TRUE, it will force the app to use - * the deprecated swiftAuth instead of IdentityService authentication. - * In general, you should avoid using this. - * - transport_client: A transport client for the HTTP requests. - * - * To find these params, the method first checks the supplied context. If the - * key is not found there, it checks the Bootstrap::conf(). - * - * @fixme This should be rewritten to use \ObjectStorage::newFromServiceCatalog(). - */ - protected function initializeObjectStorage() { + return FALSE; + } - $token = $this->cxt('token'); + try { + $this->initializeObjectStorage(); + // $container = $this->store->container($url['host']); + $name = $url['host']; + $token = $this->store->token(); + $endpoint_url = $this->store->url() . '/' . rawurlencode($name); + $client = $this->cxt('transport_client', NULL); + $container = new \OpenStack\Storage\ObjectStorage\Container($name, $endpoint_url, $token, $client); - $tenantId = $this->cxt('tenantid'); - $tenantName = $this->cxt('tenantname'); - $authUrl = $this->cxt('endpoint'); - $endpoint = $this->cxt('swift_endpoint'); - $client = $this->cxt('transport_client', NULL); + return $container->delete($url['path']); + } catch (\OpenStack\Exception $e) { + trigger_error('Error during unlink: ' . $e->getMessage(), E_USER_WARNING); - $serviceCatalog = NULL; + return FALSE; + } - if (!empty($token) && isset(self::$serviceCatalogCache[$token])) { - $serviceCatalog = self::$serviceCatalogCache[$token]; } - // FIXME: If a token is invalidated, we should try to re-authenticate. - // If context has the info we need, start from there. - if (!empty($token) && !empty($endpoint)) { - $this->store = new \OpenStack\Storage\ObjectStorage($token, $endpoint, $client); - } - // If we get here and tenant ID is not set, we can't get a container. - elseif (empty($tenantId) && empty($tenantName)) { - throw new \OpenStack\Exception('Either Tenant ID (tenantid) or Tenant Name (tenantname) is required.'); - } - elseif (empty($authUrl)) { - throw new \OpenStack\Exception('An Identity Service Endpoint (endpoint) is required.'); - } - // Try to authenticate and get a new token. - else { - $ident = $this->authenticate(); + /** + * @see stream_stat(). + */ + public function url_stat($path, $flags) + { + $url = $this->parseUrl($path); - // Update token and service catalog. The old pair may be out of date. - $token = $ident->token(); - $serviceCatalog = $ident->serviceCatalog(); - self::$serviceCatalogCache[$token] = $serviceCatalog; + if (empty($url['host']) || empty($url['path'])) { + if ($flags & STREAM_URL_STAT_QUIET) { + trigger_error('Container name (host) and path are required.', E_USER_WARNING); + } - $this->store = ObjectStorage::newFromServiceCatalog($serviceCatalog, $token, \OpenStack\Storage\ObjectStorage::DEFAULT_REGION, $client); + return FALSE; + } - /* - $catalog = $ident->serviceCatalog(ObjectStorage::SERVICE_TYPE); - if (empty($catalog) || empty($catalog[0]['endpoints'][0]['publicURL'])) { - //throw new \OpenStack\Exception('No object storage services could be found for this tenant ID.' . print_r($catalog, TRUE)); - throw new \OpenStack\Exception('No object storage services could be found for this tenant ID.'); - } - $serviceURL = $catalog[0]['endpoints'][0]['publicURL']; + try { + $this->initializeObjectStorage(); - $this->store = new ObjectStorage($token, $serviceURL); - */ + // Since we are throwing the $container away without really using its + // internals, we create an unchecked container. It may not actually + // exist on the server, which will cause a 404 error. + //$container = $this->store->container($url['host']); + $name = $url['host']; + $token = $this->store->token(); + $endpoint_url = $this->store->url() . '/' . rawurlencode($name); + $client = $this->cxt('transport_client', NULL); + $container = new \OpenStack\Storage\ObjectStorage\Container($name, $endpoint_url, $token, $client); + $obj = $container->remoteObject($url['path']); + } catch (\OpenStack\Exception $e) { + // Apparently file_exists does not set STREAM_URL_STAT_QUIET. + //if ($flags & STREAM_URL_STAT_QUIET) { + //trigger_error('Could not stat remote file: ' . $e->getMessage(), E_USER_WARNING); + //} + return FALSE; + } + + if ($flags & STREAM_URL_STAT_QUIET) { + try { + return @$this->generateStat($obj, $container, $obj->contentLength()); + } catch (\OpenStack\Exception $e) { + return FALSE; + } + } + + return $this->generateStat($obj, $container, $obj->contentLength()); } - return !empty($this->store); - - } - - protected function authenticate() { - $username = $this->cxt('username'); - $password = $this->cxt('password'); - - $tenantId = $this->cxt('tenantid'); - $tenantName = $this->cxt('tenantname'); - $authUrl = $this->cxt('endpoint'); - - $client = $this->cxt('transport_client', NULL); - - $ident = new \OpenStack\Services\IdentityService($authUrl, $client); - - // Frustrated? Go burninate. http://www.homestarrunner.com/trogdor.html - - if (!empty($username) && !empty($password)) { - $token = $ident->authenticateAsUser($username, $password, $tenantId, $tenantName); + /** + * Get the Object. + * + * This provides low-level access to the + * \OpenStack\Storage\ObjectStorage::Object instance in which the content + * is stored. + * + * Accessing the object's payload (Object::content()) is strongly + * discouraged, as it will modify the pointers in the stream that the + * stream wrapper is using. + * + * HOWEVER, accessing the Object's metadata properties, content type, + * and so on is okay. Changes to this data will be written on the next + * flush, provided that the file stream data has also been changed. + * + * To access this: + * + * object(); + * ?> + */ + public function object() + { + return $this->obj; } - else { - throw new \OpenStack\Exception('Username/password must be provided.'); - } - // Cache the service catalog. - self::$serviceCatalogCache[$token] = $ident->serviceCatalog(); - return $ident; - } + /** + * EXPERT: Get the ObjectStorage for this wrapper. + * + * @return object \OpenStack\ObjectStorage An ObjectStorage object. + * @see object() + */ + public function objectStorage() + { + return $this->store; + } + + /** + * EXPERT: Get the auth token for this wrapper. + * + * @return string A token. + * @see object() + */ + public function token() + { + return $this->store->token(); + } + + /** + * EXPERT: Get the service catalog (IdentityService) for this wrapper. + * + * This is only available when a file is opened via fopen(). + * + * @return array A service catalog. + * @see object() + */ + public function serviceCatalog() + { + return self::$serviceCatalogCache[$this->token()]; + } + + /** + * Generate a reasonably accurate STAT array. + * + * Notes on mode: + * - All modes are of the (octal) form 100XXX, where + * XXX is replaced by the permission string. Thus, + * this always reports that the type is "file" (100). + * - Currently, only two permission sets are generated: + * - 770: Represents the ACL::makePrivate() perm. + * - 775: Represents the ACL::makePublic() perm. + * + * Notes on mtime/atime/ctime: + * - For whatever reason, Swift only stores one timestamp. + * We use that for mtime, atime, and ctime. + * + * Notes on size: + * - Size must be calculated externally, as it will sometimes + * be the remote's Content-Length, and it will sometimes be + * the cached stat['size'] for the underlying buffer. + */ + protected function generateStat($object, $container, $size) + { + // This is not entirely accurate. Basically, if the + // file is marked public, it gets 100775, and if + // it is private, it gets 100770. + // + // Mode is always set to file (100XXX) because there + // is no alternative that is more sensible. PHP docs + // do not recommend an alternative. + // + // octdec(100770) == 33272 + // octdec(100775) == 33277 + $mode = $container->acl()->isPublic() ? 33277 : 33272; + + // We have to fake the UID value in order for is_readible()/is_writable() + // to work. Note that on Windows systems, stat does not look for a UID. + if (function_exists('posix_geteuid')) { + $uid = posix_geteuid(); + $gid = posix_getegid(); + } else { + $uid = 0; + $gid = 0; + } + + if ($object instanceof \OpenStack\Storage\ObjectStorage\RemoteObject) { + $modTime = $object->lastModified(); + } else { + $modTime = 0; + } + $values = array( + 'dev' => 0, + 'ino' => 0, + 'mode' => $mode, + 'nlink' => 0, + 'uid' => $uid, + 'gid' => $gid, + 'rdev' => 0, + 'size' => $size, + 'atime' => $modTime, + 'mtime' => $modTime, + 'ctime' => $modTime, + 'blksize' => -1, + 'blocks' => -1, + ); + + $final = array_values($values) + $values; + + return $final; + + } + + /////////////////////////////////////////////////////////////////// + // INTERNAL METHODS + // All methods beneath this line are not part of the Stream API. + /////////////////////////////////////////////////////////////////// + + /** + * Set the fopen mode. + * + * @param string $mode The mode string, e.g. `r+` or `wb`. + * + * @return \OpenStack\Storage\ObjectStorage\StreamWrapper $this so the method + * can be used in chaining. + */ + protected function setMode($mode) + { + $mode = strtolower($mode); + + // These are largely ignored, as the remote + // object storage does not distinguish between + // text and binary files. Per the PHP recommendation + // files are treated as binary. + $this->isBinary = strpos($mode, 'b') !== FALSE; + $this->isText = strpos($mode, 't') !== FALSE; + + // Rewrite mode to remove b or t: + $mode = preg_replace('/[bt]?/', '', $mode); + + switch ($mode) { + case 'r+': + $this->isWriting = TRUE; + case 'r': + $this->isReading = TRUE; + $this->createIfNotFound = FALSE; + break; + + + case 'w+': + $this->isReading = TRUE; + case 'w': + $this->isTruncating = TRUE; + $this->isWriting = TRUE; + break; + + + case 'a+': + $this->isReading = TRUE; + case 'a': + $this->isAppending = TRUE; + $this->isWriting = TRUE; + break; + + + case 'x+': + $this->isReading = TRUE; + case 'x': + $this->isWriting = TRUE; + $this->noOverwrite = TRUE; + break; + + case 'c+': + $this->isReading = TRUE; + case 'c': + $this->isWriting = TRUE; + break; + + // nope mode: Mock read/write support, + // but never write to the remote server. + // (This is accomplished by never marking + // the stream as dirty.) + case 'nope': + $this->isReading = TRUE; + $this->isWriting = TRUE; + $this->isNeverDirty = TRUE; + break; + + // Default case is read/write + // like c+. + default: + $this->isReading = TRUE; + $this->isWriting = TRUE; + break; + + } + + return $this; + } + + /** + * Get an item out of the context. + * + * @todo Should there be an option to NOT query the Bootstrap::conf()? + * + * @param string $name The name to look up. First look it up in the context, + * then look it up in the Bootstrap config. + * @param mixed $default The default value to return if no config param was + * found. + * + * @return mixed The discovered result, or $default if specified, or NULL if + * no $default is specified. + */ + protected function cxt($name, $default = NULL) + { + // Lazilly populate the context array. + if (is_resource($this->context) && empty($this->contextArray)) { + $cxt = stream_context_get_options($this->context); + + // If a custom scheme name has been set, use that. + if (!empty($cxt[$this->schemeName])) { + $this->contextArray = $cxt[$this->schemeName]; + } + // We fall back to this just in case. + elseif (!empty($cxt[self::DEFAULT_SCHEME])) { + $this->contextArray = $cxt[self::DEFAULT_SCHEME]; + } + } + + // Should this be array_key_exists()? + if (isset($this->contextArray[$name])) { + return $this->contextArray[$name]; + } + + // Check to see if the value can be gotten from + // \OpenStack\Bootstrap. + $val = \OpenStack\Bootstrap::config($name, NULL); + if (isset($val)) { + return $val; + } + + return $default; + } + + /** + * Parse a URL. + * + * In order to provide full UTF-8 support, URLs must be + * rawurlencoded before they are passed into the stream wrapper. + * + * This parses the URL and urldecodes the container name and + * the object name. + * + * @param string $url A Swift URL. + * + * @return array An array as documented in parse_url(). + */ + protected function parseUrl($url) + { + $res = parse_url($url); + + + // These have to be decode because they will later + // be encoded. + foreach ($res as $key => $val) { + if ($key == 'host') { + $res[$key] = urldecode($val); + } elseif ($key == 'path') { + if (strpos($val, '/') === 0) { + $val = substr($val, 1); + } + $res[$key] = urldecode($val); + + } + } + + return $res; + } + + /** + * Based on the context, initialize the ObjectStorage. + * + * The following parameters may be set either in the stream context + * or through \OpenStack\Bootstrap::setConfiguration(): + * + * - token: An auth token. If this is supplied, authentication is skipped and + * this token is used. NOTE: You MUST set swift_endpoint if using this + * option. + * - swift_endpoint: The URL to the swift instance. This is only necessary if + * 'token' is set. Otherwise it is ignored. + * - username: A username. MUST be accompanied by 'password' and 'tenantname'. + * - password: A password. MUST be accompanied by 'username' and 'tenantname'. + * - endpoint: The URL to the authentication endpoint. Necessary if you are not + * using a 'token' and 'swift_endpoint'. + * - use_swift_auth: If this is set to TRUE, it will force the app to use + * the deprecated swiftAuth instead of IdentityService authentication. + * In general, you should avoid using this. + * - transport_client: A transport client for the HTTP requests. + * + * To find these params, the method first checks the supplied context. If the + * key is not found there, it checks the Bootstrap::conf(). + * + * @fixme This should be rewritten to use \ObjectStorage::newFromServiceCatalog(). + */ + protected function initializeObjectStorage() + { + $token = $this->cxt('token'); + + $tenantId = $this->cxt('tenantid'); + $tenantName = $this->cxt('tenantname'); + $authUrl = $this->cxt('endpoint'); + $endpoint = $this->cxt('swift_endpoint'); + $client = $this->cxt('transport_client', NULL); + + $serviceCatalog = NULL; + + if (!empty($token) && isset(self::$serviceCatalogCache[$token])) { + $serviceCatalog = self::$serviceCatalogCache[$token]; + } + + // FIXME: If a token is invalidated, we should try to re-authenticate. + // If context has the info we need, start from there. + if (!empty($token) && !empty($endpoint)) { + $this->store = new \OpenStack\Storage\ObjectStorage($token, $endpoint, $client); + } + // If we get here and tenant ID is not set, we can't get a container. + elseif (empty($tenantId) && empty($tenantName)) { + throw new \OpenStack\Exception('Either Tenant ID (tenantid) or Tenant Name (tenantname) is required.'); + } elseif (empty($authUrl)) { + throw new \OpenStack\Exception('An Identity Service Endpoint (endpoint) is required.'); + } + // Try to authenticate and get a new token. + else { + $ident = $this->authenticate(); + + // Update token and service catalog. The old pair may be out of date. + $token = $ident->token(); + $serviceCatalog = $ident->serviceCatalog(); + self::$serviceCatalogCache[$token] = $serviceCatalog; + + $this->store = ObjectStorage::newFromServiceCatalog($serviceCatalog, $token, \OpenStack\Storage\ObjectStorage::DEFAULT_REGION, $client); + + /* + $catalog = $ident->serviceCatalog(ObjectStorage::SERVICE_TYPE); + if (empty($catalog) || empty($catalog[0]['endpoints'][0]['publicURL'])) { + //throw new \OpenStack\Exception('No object storage services could be found for this tenant ID.' . print_r($catalog, TRUE)); + throw new \OpenStack\Exception('No object storage services could be found for this tenant ID.'); + } + $serviceURL = $catalog[0]['endpoints'][0]['publicURL']; + + $this->store = new ObjectStorage($token, $serviceURL); + */ + } + + return !empty($this->store); + + } + + protected function authenticate() + { + $username = $this->cxt('username'); + $password = $this->cxt('password'); + + $tenantId = $this->cxt('tenantid'); + $tenantName = $this->cxt('tenantname'); + $authUrl = $this->cxt('endpoint'); + + $client = $this->cxt('transport_client', NULL); + + $ident = new \OpenStack\Services\IdentityService($authUrl, $client); + + // Frustrated? Go burninate. http://www.homestarrunner.com/trogdor.html + + if (!empty($username) && !empty($password)) { + $token = $ident->authenticateAsUser($username, $password, $tenantId, $tenantName); + } else { + throw new \OpenStack\Exception('Username/password must be provided.'); + } + // Cache the service catalog. + self::$serviceCatalogCache[$token] = $ident->serviceCatalog(); + + return $ident; + } } diff --git a/src/OpenStack/Storage/ObjectStorage/StreamWrapperFS.php b/src/OpenStack/Storage/ObjectStorage/StreamWrapperFS.php index 35d0843..5f88b9c 100644 --- a/src/OpenStack/Storage/ObjectStorage/StreamWrapperFS.php +++ b/src/OpenStack/Storage/ObjectStorage/StreamWrapperFS.php @@ -2,17 +2,17 @@ /* ============================================================================ (c) Copyright 2012-2014 Hewlett-Packard Development Company, L.P. - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at - http://www.apache.org/licenses/LICENSE-2.0 + http://www.apache.org/licenses/LICENSE-2.0 - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. ============================================================================ */ /** * Contains the stream wrapper for `swiftfs://` URLs. @@ -71,144 +71,145 @@ use \OpenStack\Storage\ObjectStorage; * * @see http://us3.php.net/manual/en/class.streamwrapper.php */ -class StreamWrapperFS extends StreamWrapper { +class StreamWrapperFS extends StreamWrapper +{ + const DEFAULT_SCHEME = 'swiftfs'; + protected $schemeName = self::DEFAULT_SCHEME; - const DEFAULT_SCHEME = 'swiftfs'; - protected $schemeName = self::DEFAULT_SCHEME; + /** + * Fake a make a dir. + * + * ObjectStorage has pathy objects not directories. If no objects with a path + * prefix exist we can pass creating a directory. If objects with a path + * prefix exist adding the directory will fail. + */ + public function mkdir($uri, $mode, $options) + { + return ($this->cxt('swiftfs_fake_isdir_true', FALSE) || !($this->testDirectoryExists($uri))); - /** - * Fake a make a dir. - * - * ObjectStorage has pathy objects not directories. If no objects with a path - * prefix exist we can pass creating a directory. If objects with a path - * prefix exist adding the directory will fail. - */ - public function mkdir($uri, $mode, $options) { - - return ($this->cxt('swiftfs_fake_isdir_true', FALSE) || !($this->testDirectoryExists($uri))); - - } - - /** - * Fake Remove a directory. - * - * ObjectStorage has pathy objects not directories. If no objects with a path - * prefix exist we can pass removing it. If objects with a path prefix exist - * removing the directory will fail. - */ - public function rmdir($path, $options) { - - return !($this->testDirectoryExists($path)); - - } - - /** - * @see stream_stat(). - */ - public function url_stat($path, $flags) { - $stat = parent::url_stat($path, $flags); - - // If the file stat setup returned anything return it. - if ($stat) { - return $stat; - } - // When FALSE is returned there is no file to stat. So, we attempt to handle - // it like a directory. - else { - if ($this->cxt('swiftfs_fake_isdir_true', FALSE) || $this->testDirectoryExists($path)) { - // The directory prefix exists. Fake the directory file permissions. - return $this->fakeStat(TRUE); - } - else { - // The directory does not exist as a prefix. - return FALSE; - } - } - } - - /////////////////////////////////////////////////////////////////// - // INTERNAL METHODS - // All methods beneath this line are not part of the Stream API. - /////////////////////////////////////////////////////////////////// - - /** - * Test if a path prefix (directory like) esits. - * - * ObjectStorage has pathy objects not directories. If objects exist with a - * path prefix we can consider that the directory exists. For example, if - * we have an object at foo/bar/baz.txt and test the existance of the - * directory foo/bar/ we sould see it. - * - * @param string $path The directory path to test. - * - * @return boolean TRUE if the directory prefix exists and FALSE otherwise. - */ - protected function testDirectoryExists($path) { - $url = $this->parseUrl($path); - - if (empty($url['host'])) { - trigger_error('Container name is required.' , E_USER_WARNING); - return FALSE; } - try { - $this->initializeObjectStorage(); - $container = $this->store->container($url['host']); + /** + * Fake Remove a directory. + * + * ObjectStorage has pathy objects not directories. If no objects with a path + * prefix exist we can pass removing it. If objects with a path prefix exist + * removing the directory will fail. + */ + public function rmdir($path, $options) + { + return !($this->testDirectoryExists($path)); - if (empty($url['path'])) { - $this->dirPrefix = ''; - } - else { - $this->dirPrefix = $url['path']; - } - - $sep = '/'; - - - $dirListing = $container->objectsWithPrefix($this->dirPrefix, $sep); - - return !empty($dirListing); } - catch (\OpenStack\Exception $e) { - trigger_error('Path could not be opened: ' . $e->getMessage(), E_USER_WARNING); - return FALSE; + + /** + * @see stream_stat(). + */ + public function url_stat($path, $flags) + { + $stat = parent::url_stat($path, $flags); + + // If the file stat setup returned anything return it. + if ($stat) { + return $stat; + } + // When FALSE is returned there is no file to stat. So, we attempt to handle + // it like a directory. + else { + if ($this->cxt('swiftfs_fake_isdir_true', FALSE) || $this->testDirectoryExists($path)) { + // The directory prefix exists. Fake the directory file permissions. + return $this->fakeStat(TRUE); + } else { + // The directory does not exist as a prefix. + return FALSE; + } + } } - } - /** - * Fake stat data. - * - * Under certain conditions we have to return totally trumped-up - * stats. This generates those. - */ - protected function fakeStat($dir = FALSE) { + /////////////////////////////////////////////////////////////////// + // INTERNAL METHODS + // All methods beneath this line are not part of the Stream API. + /////////////////////////////////////////////////////////////////// - $request_time = time(); + /** + * Test if a path prefix (directory like) esits. + * + * ObjectStorage has pathy objects not directories. If objects exist with a + * path prefix we can consider that the directory exists. For example, if + * we have an object at foo/bar/baz.txt and test the existance of the + * directory foo/bar/ we sould see it. + * + * @param string $path The directory path to test. + * + * @return boolean TRUE if the directory prefix exists and FALSE otherwise. + */ + protected function testDirectoryExists($path) + { + $url = $this->parseUrl($path); - // Set inode type to directory or file. - $type = $dir ? 040000 : 0100000; - // Fake world-readible - $mode = $type + $this->cxt('swiftfs_fake_stat_mode', 0777); + if (empty($url['host'])) { + trigger_error('Container name is required.' , E_USER_WARNING); - $values = array( - 'dev' => 0, - 'ino' => 0, - 'mode' => $mode, - 'nlink' => 0, - 'uid' => posix_getuid(), - 'gid' => posix_getgid(), - 'rdev' => 0, - 'size' => 0, - 'atime' => $request_time, - 'mtime' => $request_time, - 'ctime' => $request_time, - 'blksize' => -1, - 'blocks' => -1, - ); + return FALSE; + } - $final = array_values($values) + $values; + try { + $this->initializeObjectStorage(); + $container = $this->store->container($url['host']); - return $final; - } + if (empty($url['path'])) { + $this->dirPrefix = ''; + } else { + $this->dirPrefix = $url['path']; + } + + $sep = '/'; + + + $dirListing = $container->objectsWithPrefix($this->dirPrefix, $sep); + + return !empty($dirListing); + } catch (\OpenStack\Exception $e) { + trigger_error('Path could not be opened: ' . $e->getMessage(), E_USER_WARNING); + + return FALSE; + } + } + + /** + * Fake stat data. + * + * Under certain conditions we have to return totally trumped-up + * stats. This generates those. + */ + protected function fakeStat($dir = FALSE) + { + $request_time = time(); + + // Set inode type to directory or file. + $type = $dir ? 040000 : 0100000; + // Fake world-readible + $mode = $type + $this->cxt('swiftfs_fake_stat_mode', 0777); + + $values = array( + 'dev' => 0, + 'ino' => 0, + 'mode' => $mode, + 'nlink' => 0, + 'uid' => posix_getuid(), + 'gid' => posix_getgid(), + 'rdev' => 0, + 'size' => 0, + 'atime' => $request_time, + 'mtime' => $request_time, + 'ctime' => $request_time, + 'blksize' => -1, + 'blocks' => -1, + ); + + $final = array_values($values) + $values; + + return $final; + } } diff --git a/src/OpenStack/Storage/ObjectStorage/Subdir.php b/src/OpenStack/Storage/ObjectStorage/Subdir.php index c61e08e..bb210b7 100644 --- a/src/OpenStack/Storage/ObjectStorage/Subdir.php +++ b/src/OpenStack/Storage/ObjectStorage/Subdir.php @@ -2,17 +2,17 @@ /* ============================================================================ (c) Copyright 2012-2014 Hewlett-Packard Development Company, L.P. - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at - http://www.apache.org/licenses/LICENSE-2.0 + http://www.apache.org/licenses/LICENSE-2.0 - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. ============================================================================ */ /** * Contains the Subdir class. @@ -28,48 +28,50 @@ namespace OpenStack\Storage\ObjectStorage; * * Subdirs are used for things that are directory-like. */ -class Subdir { +class Subdir +{ + /** + * @var string The path string that this subdir describes + */ + protected $path; - /** - * @var string The path string that this subdir describes - */ - protected $path; + /** + * @var string The delimiter used in this path + */ + protected $delimiter; - /** - * @var string The delimiter used in this path - */ - protected $delimiter; + /** + * Create a new subdirectory. + * + * This represents a remote response's tag for a subdirectory. + * + * @param string $path The path string that this subdir describes. + * @param string $delimiter The delimiter used in this path. + */ + public function __construct($path, $delimiter = '/') + { + $this->path = $path; + $this->delimiter = $delimiter; + } - - /** - * Create a new subdirectory. - * - * This represents a remote response's tag for a subdirectory. - * - * @param string $path The path string that this subdir describes. - * @param string $delimiter The delimiter used in this path. - */ - public function __construct($path, $delimiter = '/') { - $this->path = $path; - $this->delimiter = $delimiter; - } - - /** - * Get the path. - * - * The path is delimited using the string returned by delimiter(). - * - * @return string The path - */ - public function path() { - return $this->path; - } - /** - * Get the delimiter used by the server. - * - * @return string The value used as a delimiter. - */ - public function delimiter() { - return $this->delimiter; - } -} \ No newline at end of file + /** + * Get the path. + * + * The path is delimited using the string returned by delimiter(). + * + * @return string The path + */ + public function path() + { + return $this->path; + } + /** + * Get the delimiter used by the server. + * + * @return string The value used as a delimiter. + */ + public function delimiter() + { + return $this->delimiter; + } +} diff --git a/src/OpenStack/Transport/AuthorizationException.php b/src/OpenStack/Transport/AuthorizationException.php index bb9f423..309bdcb 100644 --- a/src/OpenStack/Transport/AuthorizationException.php +++ b/src/OpenStack/Transport/AuthorizationException.php @@ -2,17 +2,17 @@ /* ============================================================================ (c) Copyright 2012-2014 Hewlett-Packard Development Company, L.P. - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at - http://www.apache.org/licenses/LICENSE-2.0 + http://www.apache.org/licenses/LICENSE-2.0 - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. ============================================================================ */ /** * The authorization exception. diff --git a/src/OpenStack/Transport/ClientInterface.php b/src/OpenStack/Transport/ClientInterface.php index 3b2b697..450a030 100644 --- a/src/OpenStack/Transport/ClientInterface.php +++ b/src/OpenStack/Transport/ClientInterface.php @@ -2,17 +2,17 @@ /* ============================================================================ (c) Copyright 2012-2014 Hewlett-Packard Development Company, L.P. - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at - http://www.apache.org/licenses/LICENSE-2.0 + http://www.apache.org/licenses/LICENSE-2.0 - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. ============================================================================ */ /** * This file contains the interface for transporters. @@ -32,94 +32,93 @@ namespace OpenStack\Transport; * HTTP/HTTPS, and perhaps SPDY some day. The interface reflects this. * it is not designed as a protocol-neutral transport layer */ -interface ClientInterface { +interface ClientInterface +{ + const HTTP_USER_AGENT = 'OpenStack-PHP/1.0'; - const HTTP_USER_AGENT = 'OpenStack-PHP/1.0'; + /** + * Setup for the HTTP Client. + * + * @param array $options Options for the HTTP Client including: + * - headers (array) A key/value mapping of default headers for each request. + * - proxy (string) A proxy specified as a URI. + * - debug (bool) True if debug output should be displayed. + * - timeout (int) The timeout, in seconds, a request should wait before + * timing out. + * - ssl_verify (bool|string) True, the default, verifies the SSL certificate, + * FALSE disables verification, and a string is the path to a CA to verify + * against. + */ + public function __construct(array $options = []); - /** - * Setup for the HTTP Client. - * - * @param array $options Options for the HTTP Client including: - * - headers (array) A key/value mapping of default headers for each request. - * - proxy (string) A proxy specified as a URI. - * - debug (bool) True if debug output should be displayed. - * - timeout (int) The timeout, in seconds, a request should wait before - * timing out. - * - ssl_verify (bool|string) True, the default, verifies the SSL certificate, - * FALSE disables verification, and a string is the path to a CA to verify - * against. - */ - public function __construct(array $options = []); + /** + * Perform a request. + * + * Invoking this method causes a single request to be relayed over the + * transporter. The transporter MUST be capable of handling multiple + * invocations of a doRequest() call. + * + * @param string $uri The target URI. + * @param string $method The method to be sent. + * @param array $headers An array of name/value header pairs. + * @param string $body The string containing the request body. + * + * @return \OpenStack\Transport\ResponseInterface The response. The response + * is implicit rather than explicit. The interface is based on a draft for + * messages from PHP FIG. Individual implementing libraries will have their + * own reference to interfaces. For example, see Guzzle. + * + * @throws \OpenStack\Transport\ForbiddenException + * @throws \OpenStack\Transport\UnauthorizedException + * @throws \OpenStack\Transport\FileNotFoundException + * @throws \OpenStack\Transport\MethodNotAllowedException + * @throws \OpenStack\Transport\ConflictException + * @throws \OpenStack\Transport\LengthRequiredException + * @throws \OpenStack\Transport\UnprocessableEntityException + * @throws \OpenStack\Transport\ServerException + * @throws \OpenStack\Exception + */ + public function doRequest($uri, $method = 'GET', array $headers = [], $body = ''); - /** - * Perform a request. - * - * Invoking this method causes a single request to be relayed over the - * transporter. The transporter MUST be capable of handling multiple - * invocations of a doRequest() call. - * - * @param string $uri The target URI. - * @param string $method The method to be sent. - * @param array $headers An array of name/value header pairs. - * @param string $body The string containing the request body. - * - * @return \OpenStack\Transport\ResponseInterface The response. The response - * is implicit rather than explicit. The interface is based on a draft for - * messages from PHP FIG. Individual implementing libraries will have their - * own reference to interfaces. For example, see Guzzle. - * - * @throws \OpenStack\Transport\ForbiddenException - * @throws \OpenStack\Transport\UnauthorizedException - * @throws \OpenStack\Transport\FileNotFoundException - * @throws \OpenStack\Transport\MethodNotAllowedException - * @throws \OpenStack\Transport\ConflictException - * @throws \OpenStack\Transport\LengthRequiredException - * @throws \OpenStack\Transport\UnprocessableEntityException - * @throws \OpenStack\Transport\ServerException - * @throws \OpenStack\Exception - */ - public function doRequest($uri, $method = 'GET', array $headers = [], $body = ''); - - - /** - * Perform a request, but use a resource to read the body. - * - * This is a special version of the doRequest() function. - * It handles a very spefic case where... - * - * - The HTTP verb requires a body (viz. PUT, POST) - * - The body is in a resource, not a string - * - * Examples of appropriate cases for this variant: - * - * - Uploading large files. - * - Streaming data out of a stream and into an HTTP request. - * - Minimizing memory usage ($content strings are big). - * - * Note that all parameters are required. - * - * @param string $uri The target URI. - * @param string $method The method to be sent. - * @param array $headers An array of name/value header pairs. - * @param mixed $resource The string with a file path or a stream URL; or a - * file object resource. If it is a string, then it will be opened with the - * default context. So if you need a special context, you should open the - * file elsewhere and pass the resource in here. - * - * @return \OpenStack\Transport\ResponseInterface The response. The response - * is implicit rather than explicit. The interface is based on a draft for - * messages from PHP FIG. Individual implementing libraries will have their - * own reference to interfaces. For example, see Guzzle. - * - * @throws \OpenStack\Transport\ForbiddenException - * @throws \OpenStack\Transport\UnauthorizedException - * @throws \OpenStack\Transport\FileNotFoundException - * @throws \OpenStack\Transport\MethodNotAllowedException - * @throws \OpenStack\Transport\ConflictException - * @throws \OpenStack\Transport\LengthRequiredException - * @throws \OpenStack\Transport\UnprocessableEntityException - * @throws \OpenStack\Transport\ServerException - * @throws \OpenStack\Exception - */ - public function doRequestWithResource($uri, $method, array $headers = [], $resource); + /** + * Perform a request, but use a resource to read the body. + * + * This is a special version of the doRequest() function. + * It handles a very spefic case where... + * + * - The HTTP verb requires a body (viz. PUT, POST) + * - The body is in a resource, not a string + * + * Examples of appropriate cases for this variant: + * + * - Uploading large files. + * - Streaming data out of a stream and into an HTTP request. + * - Minimizing memory usage ($content strings are big). + * + * Note that all parameters are required. + * + * @param string $uri The target URI. + * @param string $method The method to be sent. + * @param array $headers An array of name/value header pairs. + * @param mixed $resource The string with a file path or a stream URL; or a + * file object resource. If it is a string, then it will be opened with the + * default context. So if you need a special context, you should open the + * file elsewhere and pass the resource in here. + * + * @return \OpenStack\Transport\ResponseInterface The response. The response + * is implicit rather than explicit. The interface is based on a draft for + * messages from PHP FIG. Individual implementing libraries will have their + * own reference to interfaces. For example, see Guzzle. + * + * @throws \OpenStack\Transport\ForbiddenException + * @throws \OpenStack\Transport\UnauthorizedException + * @throws \OpenStack\Transport\FileNotFoundException + * @throws \OpenStack\Transport\MethodNotAllowedException + * @throws \OpenStack\Transport\ConflictException + * @throws \OpenStack\Transport\LengthRequiredException + * @throws \OpenStack\Transport\UnprocessableEntityException + * @throws \OpenStack\Transport\ServerException + * @throws \OpenStack\Exception + */ + public function doRequestWithResource($uri, $method, array $headers = [], $resource); } diff --git a/src/OpenStack/Transport/ConflictException.php b/src/OpenStack/Transport/ConflictException.php index 3c23a82..8fa459c 100644 --- a/src/OpenStack/Transport/ConflictException.php +++ b/src/OpenStack/Transport/ConflictException.php @@ -2,17 +2,17 @@ /* ============================================================================ (c) Copyright 2012-2014 Hewlett-Packard Development Company, L.P. - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at - http://www.apache.org/licenses/LICENSE-2.0 + http://www.apache.org/licenses/LICENSE-2.0 - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. ============================================================================ */ namespace OpenStack\Transport; diff --git a/src/OpenStack/Transport/FileNotFoundException.php b/src/OpenStack/Transport/FileNotFoundException.php index 7ae0455..7bb5895 100644 --- a/src/OpenStack/Transport/FileNotFoundException.php +++ b/src/OpenStack/Transport/FileNotFoundException.php @@ -2,17 +2,17 @@ /* ============================================================================ (c) Copyright 2012-2014 Hewlett-Packard Development Company, L.P. - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at - http://www.apache.org/licenses/LICENSE-2.0 + http://www.apache.org/licenses/LICENSE-2.0 - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. ============================================================================ */ namespace OpenStack\Transport; diff --git a/src/OpenStack/Transport/ForbiddenException.php b/src/OpenStack/Transport/ForbiddenException.php index 9af3091..80b5508 100644 --- a/src/OpenStack/Transport/ForbiddenException.php +++ b/src/OpenStack/Transport/ForbiddenException.php @@ -2,17 +2,17 @@ /* ============================================================================ (c) Copyright 2012-2014 Hewlett-Packard Development Company, L.P. - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at - http://www.apache.org/licenses/LICENSE-2.0 + http://www.apache.org/licenses/LICENSE-2.0 - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. ============================================================================ */ /** * @file diff --git a/src/OpenStack/Transport/GuzzleClient.php b/src/OpenStack/Transport/GuzzleClient.php index 54169d4..b1111bc 100644 --- a/src/OpenStack/Transport/GuzzleClient.php +++ b/src/OpenStack/Transport/GuzzleClient.php @@ -2,17 +2,17 @@ /* ============================================================================ (c) Copyright 2014 Hewlett-Packard Development Company, L.P. - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at - http://www.apache.org/licenses/LICENSE-2.0 + http://www.apache.org/licenses/LICENSE-2.0 - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. ============================================================================ */ /** * This file contains the interface for transporter clients. @@ -20,203 +20,208 @@ namespace OpenStack\Transport; -class GuzzleClient implements ClientInterface, \Serializable { +class GuzzleClient implements ClientInterface, \Serializable +{ + const HTTP_USER_AGENT_SUFFIX = ' Guzzle/4.0'; - const HTTP_USER_AGENT_SUFFIX = ' Guzzle/4.0'; + /** + * The Guzzle client used for the implementation. + */ + protected $client; - /** - * The Guzzle client used for the implementation. - */ - protected $client; + protected $options; - protected $options; + /** + * Setup for the HTTP Client. + * + * @param array $options Options for the HTTP Client including: + * - headers (array) A key/value mapping of default headers for each request. + * - proxy (string) A proxy specified as a URI. + * - debug (bool) True if debug output should be displayed. + * - timeout (int) The timeout, in seconds, a request should wait before + * timing out. + * - ssl_verify (bool|string) True, the default, verifies the SSL certificate, + * FALSE disables verification, and a string is the path to a CA to verify + * against. + * - client (mixed) A guzzle client object to use instead of the default. + * This can be either a string to the class or an existing object. If an + * existing object is passed in the other options will be ignored. + */ + public function __construct(array $options = []) + { + $this->options = $options; - /** - * Setup for the HTTP Client. - * - * @param array $options Options for the HTTP Client including: - * - headers (array) A key/value mapping of default headers for each request. - * - proxy (string) A proxy specified as a URI. - * - debug (bool) True if debug output should be displayed. - * - timeout (int) The timeout, in seconds, a request should wait before - * timing out. - * - ssl_verify (bool|string) True, the default, verifies the SSL certificate, - * FALSE disables verification, and a string is the path to a CA to verify - * against. - * - client (mixed) A guzzle client object to use instead of the default. - * This can be either a string to the class or an existing object. If an - * existing object is passed in the other options will be ignored. - */ - public function __construct(array $options = []) { - $this->options = $options; - - $this->client = $this->setup($options); - } - - /** - * Setup is a protected method to setup the client. - * - * The functionality would typically be in the constructor. It was broken out - * to be used by the constructor and serialization process. - * - * @param array $options The options as passed to the constructor. - * @return mixed The Guzzle based client. - */ - protected function setup(array $options = []) { - // If no client has been passed in we create one. This is the default case. - if (!isset($options['client']) || is_string($options['client'])) { - $defaultOptions = ['defaults' => []]; - if (isset($options['headers'])) { - $defaultOptions['defaults']['headers'] = $options['headers']; - } - if (isset($options['proxy'])) { - $defaultOptions['defaults']['proxy'] = $options['proxy']; - } - if (isset($options['debug'])) { - $defaultOptions['defaults']['debug'] = $options['debug']; - } - if (isset($options['ssl'])) { - $defaultOptions['defaults']['verify'] = $options['ssl_verify']; - } - if (isset($options['timeout'])) { - $defaultOptions['defaults']['timeout'] = $options['timeout']; - } - - // Add a user agent if not already specificed. - if (!isset($defaultOptions['defaults']['headers']['User-Agent'])) { - $defaultOptions['defaults']['headers']['User-Agent'] = self::HTTP_USER_AGENT . self::HTTP_USER_AGENT_SUFFIX; - } - - $clientClass = '\GuzzleHttp\Client'; - if (isset($options['client']) && is_string($options['client'])) { - $clientClass = $options['client']; - } - - $options['client'] = new $clientClass($defaultOptions); + $this->client = $this->setup($options); } - return $options['client']; - } + /** + * Setup is a protected method to setup the client. + * + * The functionality would typically be in the constructor. It was broken out + * to be used by the constructor and serialization process. + * + * @param array $options The options as passed to the constructor. + * @return mixed The Guzzle based client. + */ + protected function setup(array $options = []) + { + // If no client has been passed in we create one. This is the default case. + if (!isset($options['client']) || is_string($options['client'])) { + $defaultOptions = ['defaults' => []]; + if (isset($options['headers'])) { + $defaultOptions['defaults']['headers'] = $options['headers']; + } + if (isset($options['proxy'])) { + $defaultOptions['defaults']['proxy'] = $options['proxy']; + } + if (isset($options['debug'])) { + $defaultOptions['defaults']['debug'] = $options['debug']; + } + if (isset($options['ssl'])) { + $defaultOptions['defaults']['verify'] = $options['ssl_verify']; + } + if (isset($options['timeout'])) { + $defaultOptions['defaults']['timeout'] = $options['timeout']; + } - /** - * {@inheritdoc} - */ - public function doRequest($uri, $method = 'GET', array $headers = [], $body = '') { + // Add a user agent if not already specificed. + if (!isset($defaultOptions['defaults']['headers']['User-Agent'])) { + $defaultOptions['defaults']['headers']['User-Agent'] = self::HTTP_USER_AGENT . self::HTTP_USER_AGENT_SUFFIX; + } - $options = [ - 'headers' => $headers, - 'body' => $body, - ]; + $clientClass = '\GuzzleHttp\Client'; + if (isset($options['client']) && is_string($options['client'])) { + $clientClass = $options['client']; + } - // We use our own exceptions for errors to provide a common exception - // interface to applications implementing the SDK. - try { - $response = $this->client->send($this->client->createRequest($method, $uri, $options)); - } catch(\GuzzleHttp\Exception\ClientException $e) { - $this->handleException($e); - } catch(\GuzzleHttp\Exception\ServerException $e) { - $this->handleException($e); - } catch(\GuzzleHttp\Exception\RequestException $e) { - $this->handleException($e); + $options['client'] = new $clientClass($defaultOptions); + } + + return $options['client']; } - return $response; - } + /** + * {@inheritdoc} + */ + public function doRequest($uri, $method = 'GET', array $headers = [], $body = '') + { + $options = [ + 'headers' => $headers, + 'body' => $body, + ]; - /** - * {@inheritdoc} - */ - public function doRequestWithResource($uri, $method, array $headers = [], $resource) { + // We use our own exceptions for errors to provide a common exception + // interface to applications implementing the SDK. + try { + $response = $this->client->send($this->client->createRequest($method, $uri, $options)); + } catch (\GuzzleHttp\Exception\ClientException $e) { + $this->handleException($e); + } catch (\GuzzleHttp\Exception\ServerException $e) { + $this->handleException($e); + } catch (\GuzzleHttp\Exception\RequestException $e) { + $this->handleException($e); + } - // Guzzle messes with the resource in such a manner that it can no longer be - // used by something else after the fact. So, we clone the content into - // temporary stream. - $tmp = $out = fopen('php://temp', 'wb+'); - stream_copy_to_stream($resource, $tmp); - - $options = [ - 'headers' => $headers, - 'body' => $tmp, - ]; - - // We use our own exceptions for errors to provide a common exception - // interface to applications implementing the SDK. - try { - $response = $this->client->send($this->client->createRequest($method, $uri, $options)); - } catch(\GuzzleHttp\Exception\ClientException $e) { - $this->handleException($e); - } catch(\GuzzleHttp\Exception\ServerException $e) { - $this->handleException($e); - } catch(\GuzzleHttp\Exception\RequestException $e) { - $this->handleException($e); + return $response; } - return $response; - } + /** + * {@inheritdoc} + */ + public function doRequestWithResource($uri, $method, array $headers = [], $resource) + { + // Guzzle messes with the resource in such a manner that it can no longer be + // used by something else after the fact. So, we clone the content into + // temporary stream. + $tmp = $out = fopen('php://temp', 'wb+'); + stream_copy_to_stream($resource, $tmp); - /** - * Handle errors on a response. - * - * @param mixed The Guzzle exception. - * - * @return \OpenStack\Transport\ResponseInterface The response. - * - * @throws \OpenStack\Transport\ForbiddenException - * @throws \OpenStack\Transport\UnauthorizedException - * @throws \OpenStack\Transport\FileNotFoundException - * @throws \OpenStack\Transport\MethodNotAllowedException - * @throws \OpenStack\Transport\ConflictException - * @throws \OpenStack\Transport\LengthRequiredException - * @throws \OpenStack\Transport\UnprocessableEntityException - * @throws \OpenStack\Transport\ServerException - * @throws \OpenStack\Exception - */ - protected function handleException($exception) { + $options = [ + 'headers' => $headers, + 'body' => $tmp, + ]; - $response = $exception->getResponse(); - $request = $exception->getRequest(); + // We use our own exceptions for errors to provide a common exception + // interface to applications implementing the SDK. + try { + $response = $this->client->send($this->client->createRequest($method, $uri, $options)); + } catch (\GuzzleHttp\Exception\ClientException $e) { + $this->handleException($e); + } catch (\GuzzleHttp\Exception\ServerException $e) { + $this->handleException($e); + } catch (\GuzzleHttp\Exception\RequestException $e) { + $this->handleException($e); + } - if (!is_null($response)) { - $code = $response->getStatusCode(); - - switch ($code) { - case '403': - throw new \OpenStack\Transport\ForbiddenException($response->getReasonPhrase()); - case '401': - throw new \OpenStack\Transport\UnauthorizedException($response->getReasonPhrase()); - case '404': - throw new \OpenStack\Transport\FileNotFoundException($response->getReasonPhrase() . " ({$response->getEffectiveUrl()})"); - case '405': - throw new \OpenStack\Transport\MethodNotAllowedException($response->getReasonPhrase() . " ({$request->getMethod()} {$response->getEffectiveUrl()})"); - case '409': - throw new \OpenStack\Transport\ConflictException($response->getReasonPhrase()); - case '412': - throw new \OpenStack\Transport\LengthRequiredException($response->getReasonPhrase()); - case '422': - throw new \OpenStack\Transport\UnprocessableEntityException($response->getReasonPhrase()); - case '500': - throw new \OpenStack\Transport\ServerException($response->getReasonPhrase()); - default: - throw new \OpenStack\Exception($response->getReasonPhrase()); - } - } - // The exception was one other than a HTTP error. For example, a HTTP layer - // timeout occurred. - else { - throw new \OpenStack\Exception($exception->getMessage()); + return $response; } - return $response; - } + /** + * Handle errors on a response. + * + * @param mixed The Guzzle exception. + * + * @return \OpenStack\Transport\ResponseInterface The response. + * + * @throws \OpenStack\Transport\ForbiddenException + * @throws \OpenStack\Transport\UnauthorizedException + * @throws \OpenStack\Transport\FileNotFoundException + * @throws \OpenStack\Transport\MethodNotAllowedException + * @throws \OpenStack\Transport\ConflictException + * @throws \OpenStack\Transport\LengthRequiredException + * @throws \OpenStack\Transport\UnprocessableEntityException + * @throws \OpenStack\Transport\ServerException + * @throws \OpenStack\Exception + */ + protected function handleException($exception) + { + $response = $exception->getResponse(); + $request = $exception->getRequest(); - public function serialize() { - $data = ['options' => $this->options]; - return serialize($data); - } + if (!is_null($response)) { + $code = $response->getStatusCode(); - public function unserialize($data) { - $vals = unserialize($data); - $this->options = $vals['options']; - $this->client = $this->setup($vals['options']); - } + switch ($code) { + case '403': + throw new \OpenStack\Transport\ForbiddenException($response->getReasonPhrase()); + case '401': + throw new \OpenStack\Transport\UnauthorizedException($response->getReasonPhrase()); + case '404': + throw new \OpenStack\Transport\FileNotFoundException($response->getReasonPhrase() . " ({$response->getEffectiveUrl()})"); + case '405': + throw new \OpenStack\Transport\MethodNotAllowedException($response->getReasonPhrase() . " ({$request->getMethod()} {$response->getEffectiveUrl()})"); + case '409': + throw new \OpenStack\Transport\ConflictException($response->getReasonPhrase()); + case '412': + throw new \OpenStack\Transport\LengthRequiredException($response->getReasonPhrase()); + case '422': + throw new \OpenStack\Transport\UnprocessableEntityException($response->getReasonPhrase()); + case '500': + throw new \OpenStack\Transport\ServerException($response->getReasonPhrase()); + default: + throw new \OpenStack\Exception($response->getReasonPhrase()); + } + } + // The exception was one other than a HTTP error. For example, a HTTP layer + // timeout occurred. + else { + throw new \OpenStack\Exception($exception->getMessage()); + } + + return $response; + } + + public function serialize() + { + $data = ['options' => $this->options]; + + return serialize($data); + } + + public function unserialize($data) + { + $vals = unserialize($data); + $this->options = $vals['options']; + $this->client = $this->setup($vals['options']); + } } diff --git a/src/OpenStack/Transport/LengthRequiredException.php b/src/OpenStack/Transport/LengthRequiredException.php index 0b54088..3889ef2 100644 --- a/src/OpenStack/Transport/LengthRequiredException.php +++ b/src/OpenStack/Transport/LengthRequiredException.php @@ -2,17 +2,17 @@ /* ============================================================================ (c) Copyright 2012-2014 Hewlett-Packard Development Company, L.P. - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at - http://www.apache.org/licenses/LICENSE-2.0 + http://www.apache.org/licenses/LICENSE-2.0 - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. ============================================================================ */ namespace OpenStack\Transport; diff --git a/src/OpenStack/Transport/MethodNotAllowedException.php b/src/OpenStack/Transport/MethodNotAllowedException.php index ae316cc..f155dc5 100644 --- a/src/OpenStack/Transport/MethodNotAllowedException.php +++ b/src/OpenStack/Transport/MethodNotAllowedException.php @@ -2,17 +2,17 @@ /* ============================================================================ (c) Copyright 2012-2014 Hewlett-Packard Development Company, L.P. - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at - http://www.apache.org/licenses/LICENSE-2.0 + http://www.apache.org/licenses/LICENSE-2.0 - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. ============================================================================ */ namespace OpenStack\Transport; diff --git a/src/OpenStack/Transport/ResponseInterface.php b/src/OpenStack/Transport/ResponseInterface.php index 52c8939..230f7a7 100644 --- a/src/OpenStack/Transport/ResponseInterface.php +++ b/src/OpenStack/Transport/ResponseInterface.php @@ -2,17 +2,17 @@ /* ============================================================================ (c) Copyright 2014 Hewlett-Packard Development Company, L.P. - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at - http://www.apache.org/licenses/LICENSE-2.0 + http://www.apache.org/licenses/LICENSE-2.0 - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. ============================================================================ */ /** * This file contains the response interface for a HTTP request. @@ -26,157 +26,158 @@ namespace OpenStack\Transport; * This interface is equivalent to the proposed PHP FIG http message interface * for a response that can be found at https://github.com/php-fig/fig-standards/blob/master/proposed/http-message.md#33-psrhttpresponseinterface */ -interface ResponseInterface { - /** - * Gets the response Status-Code, a 3-digit integer result code of the - * server's attempt to understand and satisfy the request. - * - * @return integer Status code. - */ - public function getStatusCode(); +interface ResponseInterface +{ + /** + * Gets the response Status-Code, a 3-digit integer result code of the + * server's attempt to understand and satisfy the request. + * + * @return integer Status code. + */ + public function getStatusCode(); - /** - * Gets the response Reason-Phrase, a short textual description of the - * Status-Code. - * - * Because a Reason-Phrase is not a required element in response - * Status-Line, the Reason-Phrase value MAY be null. Implementations MAY - * choose to return the default RFC 2616 recommended reason phrase for the - * response's Status-Code. - * - * @return string|null Reason phrase, or null if unknown. - */ - public function getReasonPhrase(); - - /** - * Gets the HTTP protocol version. - * - * @return string HTTP protocol version. - */ - public function getProtocolVersion(); + /** + * Gets the response Reason-Phrase, a short textual description of the + * Status-Code. + * + * Because a Reason-Phrase is not a required element in response + * Status-Line, the Reason-Phrase value MAY be null. Implementations MAY + * choose to return the default RFC 2616 recommended reason phrase for the + * response's Status-Code. + * + * @return string|null Reason phrase, or null if unknown. + */ + public function getReasonPhrase(); - /** - * Gets the body of the message. - * - * @return StreamInterface|null Returns the body, or null if not set. - */ - public function getBody(); + /** + * Gets the HTTP protocol version. + * + * @return string HTTP protocol version. + */ + public function getProtocolVersion(); - /** - * Sets the body of the message. - * - * The body MUST be a StreamInterface object. Setting the body to null MUST - * remove the existing body. - * - * @param StreamInterface|null $body Body. - * - * @return self Returns the message. - * - * @throws \InvalidArgumentException When the body is not valid. - */ - public function setBody(StreamInterface $body = null); + /** + * Gets the body of the message. + * + * @return StreamInterface|null Returns the body, or null if not set. + */ + public function getBody(); - /** - * Gets all message headers. - * - * The keys represent the header name as it will be sent over the wire, and - * each value is an array of strings associated with the header. - * - * // Represent the headers as a string - * foreach ($message->getHeaders() as $name => $values) { - * echo $name . ": " . implode(", ", $values); - * } - * - * @return array Returns an associative array of the message's headers. - */ - public function getHeaders(); + /** + * Sets the body of the message. + * + * The body MUST be a StreamInterface object. Setting the body to null MUST + * remove the existing body. + * + * @param StreamInterface|null $body Body. + * + * @return self Returns the message. + * + * @throws \InvalidArgumentException When the body is not valid. + */ + public function setBody(StreamInterface $body = null); - /** - * Checks if a header exists by the given case-insensitive name. - * - * @param string $header Case-insensitive header name. - * - * @return bool Returns true if any header names match the given header - * name using a case-insensitive string comparison. Returns false if - * no matching header name is found in the message. - */ - public function hasHeader($header); + /** + * Gets all message headers. + * + * The keys represent the header name as it will be sent over the wire, and + * each value is an array of strings associated with the header. + * + * // Represent the headers as a string + * foreach ($message->getHeaders() as $name => $values) { + * echo $name . ": " . implode(", ", $values); + * } + * + * @return array Returns an associative array of the message's headers. + */ + public function getHeaders(); - /** - * Retrieve a header by the given case-insensitive name. - * - * By default, this method returns all of the header values of the given - * case-insensitive header name as a string concatenated together using - * a comma. Because some header should not be concatenated together using a - * comma, this method provides a Boolean argument that can be used to - * retrieve the associated header values as an array of strings. - * - * @param string $header Case-insensitive header name. - * @param bool $asArray Set to true to retrieve the header value as an - * array of strings. - * - * @return array|string - */ - public function getHeader($header, $asArray = false); + /** + * Checks if a header exists by the given case-insensitive name. + * + * @param string $header Case-insensitive header name. + * + * @return bool Returns true if any header names match the given header + * name using a case-insensitive string comparison. Returns false if + * no matching header name is found in the message. + */ + public function hasHeader($header); - /** - * Sets a header, replacing any existing values of any headers with the - * same case-insensitive name. - * - * The header values MUST be a string or an array of strings. - * - * @param string $header Header name - * @param string|array $value Header value(s) - * - * @return self Returns the message. - */ - public function setHeader($header, $value); + /** + * Retrieve a header by the given case-insensitive name. + * + * By default, this method returns all of the header values of the given + * case-insensitive header name as a string concatenated together using + * a comma. Because some header should not be concatenated together using a + * comma, this method provides a Boolean argument that can be used to + * retrieve the associated header values as an array of strings. + * + * @param string $header Case-insensitive header name. + * @param bool $asArray Set to true to retrieve the header value as an + * array of strings. + * + * @return array|string + */ + public function getHeader($header, $asArray = false); - /** - * Sets headers, replacing any headers that have already been set on the - * message. - * - * The array keys MUST be a string. The array values must be either a - * string or an array of strings. - * - * @param array $headers Headers to set. - * - * @return self Returns the message. - */ - public function setHeaders(array $headers); + /** + * Sets a header, replacing any existing values of any headers with the + * same case-insensitive name. + * + * The header values MUST be a string or an array of strings. + * + * @param string $header Header name + * @param string|array $value Header value(s) + * + * @return self Returns the message. + */ + public function setHeader($header, $value); - /** - * Appends a header value to any existing values associated with the - * given header name. - * - * @param string $header Header name to add - * @param string $value Value of the header - * - * @return self - */ - public function addHeader($header, $value); + /** + * Sets headers, replacing any headers that have already been set on the + * message. + * + * The array keys MUST be a string. The array values must be either a + * string or an array of strings. + * + * @param array $headers Headers to set. + * + * @return self Returns the message. + */ + public function setHeaders(array $headers); - /** - * Merges in an associative array of headers. - * - * Each array key MUST be a string representing the case-insensitive name - * of a header. Each value MUST be either a string or an array of strings. - * For each value, the value is appended to any existing header of the same - * name, or, if a header does not already exist by the given name, then the - * header is added. - * - * @param array $headers Associative array of headers to add to the message - * - * @return self - */ - public function addHeaders(array $headers); + /** + * Appends a header value to any existing values associated with the + * given header name. + * + * @param string $header Header name to add + * @param string $value Value of the header + * + * @return self + */ + public function addHeader($header, $value); - /** - * Remove a specific header by case-insensitive name. - * - * @param string $header HTTP header to remove - * - * @return self - */ - public function removeHeader($header); -} \ No newline at end of file + /** + * Merges in an associative array of headers. + * + * Each array key MUST be a string representing the case-insensitive name + * of a header. Each value MUST be either a string or an array of strings. + * For each value, the value is appended to any existing header of the same + * name, or, if a header does not already exist by the given name, then the + * header is added. + * + * @param array $headers Associative array of headers to add to the message + * + * @return self + */ + public function addHeaders(array $headers); + + /** + * Remove a specific header by case-insensitive name. + * + * @param string $header HTTP header to remove + * + * @return self + */ + public function removeHeader($header); +} diff --git a/src/OpenStack/Transport/ServerException.php b/src/OpenStack/Transport/ServerException.php index db05dcd..04e65fb 100644 --- a/src/OpenStack/Transport/ServerException.php +++ b/src/OpenStack/Transport/ServerException.php @@ -2,17 +2,17 @@ /* ============================================================================ (c) Copyright 2012-2014 Hewlett-Packard Development Company, L.P. - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at - http://www.apache.org/licenses/LICENSE-2.0 + http://www.apache.org/licenses/LICENSE-2.0 - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. ============================================================================ */ namespace OpenStack\Transport; diff --git a/src/OpenStack/Transport/UnauthorizedException.php b/src/OpenStack/Transport/UnauthorizedException.php index 72a6d05..6d83693 100644 --- a/src/OpenStack/Transport/UnauthorizedException.php +++ b/src/OpenStack/Transport/UnauthorizedException.php @@ -2,17 +2,17 @@ /* ============================================================================ (c) Copyright 2012-2014 Hewlett-Packard Development Company, L.P. - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at - http://www.apache.org/licenses/LICENSE-2.0 + http://www.apache.org/licenses/LICENSE-2.0 - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. ============================================================================ */ /** * The authorization exception. diff --git a/src/OpenStack/Transport/UnprocessableEntityException.php b/src/OpenStack/Transport/UnprocessableEntityException.php index b3bbdc8..bdc8da2 100644 --- a/src/OpenStack/Transport/UnprocessableEntityException.php +++ b/src/OpenStack/Transport/UnprocessableEntityException.php @@ -2,17 +2,17 @@ /* ============================================================================ (c) Copyright 2012-2014 Hewlett-Packard Development Company, L.P. - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at - http://www.apache.org/licenses/LICENSE-2.0 + http://www.apache.org/licenses/LICENSE-2.0 - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. ============================================================================ */ namespace OpenStack\Transport; diff --git a/test/AuthTest.php b/test/AuthTest.php index 58828f6..460d8b8 100644 --- a/test/AuthTest.php +++ b/test/AuthTest.php @@ -2,17 +2,17 @@ /* ============================================================================ (c) Copyright 2012-2014 Hewlett-Packard Development Company, L.P. - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at - http://www.apache.org/licenses/LICENSE-2.0 + http://www.apache.org/licenses/LICENSE-2.0 - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. ============================================================================ */ /** * This is a simple command-line test for authentication. @@ -27,10 +27,10 @@ use \OpenStack\Storage\ObjectStorage; use \OpenStack\Services\IdentityService; $config = array( - 'transport' => '\OpenStack\Transport\PHPStreamTransport', - 'transport.timeout' => 240, - //'transport.debug' => 1, - 'transport.ssl.verify' => 0, + 'transport' => '\OpenStack\Transport\PHPStreamTransport', + 'transport.timeout' => 240, + //'transport.debug' => 1, + 'transport.ssl.verify' => 0, ); \OpenStack\Autoloader::useAutoloader(); @@ -48,14 +48,13 @@ In both cases, you must supply a URL to the Identity Services endpoint. $usage = "php {$argv[0]} USERNAME PASSWORD URL [TENANT_ID]"; if ($argc > 1 && $argv[1] == '--help') { - print PHP_EOL . "\t" . $usage . PHP_EOL; - print PHP_EOL . $help . PHP_EOL; - exit(1); -} -elseif ($argc < 4) { - print 'USERNAME, PASSWORD, and URL are all required.' . PHP_EOL; - print $usage . PHP_EOL; - exit(1); + print PHP_EOL . "\t" . $usage . PHP_EOL; + print PHP_EOL . $help . PHP_EOL; + exit(1); +} elseif ($argc < 4) { + print 'USERNAME, PASSWORD, and URL are all required.' . PHP_EOL; + print $usage . PHP_EOL; + exit(1); } $offset = 0; @@ -66,7 +65,7 @@ $uri = $argv[3 + $offset]; $tenantId = NULL; if (!empty($argv[4 + $offset])) { - $tenantId = $argv[4 + $offset]; + $tenantId = $argv[4 + $offset]; } /* @@ -79,8 +78,8 @@ $cs = new IdentityService($uri); $token = $cs->authenticateAsUser($user, $password, $tenantId); if (empty($token)) { - print "Authentication seemed to succeed, but no token was returned." . PHP_EOL; - exit(1); + print "Authentication seemed to succeed, but no token was returned." . PHP_EOL; + exit(1); } $t = "You are logged in as %s with token %s (good until %s)." . PHP_EOL; @@ -93,7 +92,7 @@ print "The following services are available on this user:" . PHP_EOL; $services = $cs->serviceCatalog(); foreach ($services as $service) { - print "\t" . $service['name'] . PHP_EOL; + print "\t" . $service['name'] . PHP_EOL; } //print_r($services); diff --git a/test/TestCase.php b/test/TestCase.php index 655c772..fef8268 100644 --- a/test/TestCase.php +++ b/test/TestCase.php @@ -2,17 +2,17 @@ /* ============================================================================ (c) Copyright 2012-2014 Hewlett-Packard Development Company, L.P. - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at - http://www.apache.org/licenses/LICENSE-2.0 + http://www.apache.org/licenses/LICENSE-2.0 - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. ============================================================================ */ /** * Base test case. @@ -25,233 +25,232 @@ * This group contains all of the unit testing classes. */ - namespace OpenStack\Tests; /** * @ingroup Tests */ -class TestCase extends \PHPUnit_Framework_TestCase { +class TestCase extends \PHPUnit_Framework_TestCase +{ + public static $settings = array(); - public static $settings = array(); + public static $ostore = NULL; - public static $ostore = NULL; + /** + * The IdentityService instance. + */ + public static $ident; - /** - * The IdentityService instance. - */ - public static $ident; + public static $httpClient = NULL; - public static $httpClient = NULL; + //public function __construct(score $score = NULL, locale $locale = NULL, adapter $adapter = NULL) { + public static function setUpBeforeClass() + { + global $bootstrap_settings; - //public function __construct(score $score = NULL, locale $locale = NULL, adapter $adapter = NULL) { - public static function setUpBeforeClass() { - global $bootstrap_settings; + if (!isset($bootstrap_settings)) { + $bootstrap_settings = array(); + } + self::$settings = $bootstrap_settings; - if (!isset($bootstrap_settings)) { - $bootstrap_settings = array(); - } - self::$settings = $bootstrap_settings; + //$this->setTestNamespace('Tests\Units'); + if (file_exists('test/settings.ini')) { + self::$settings += parse_ini_file('test/settings.ini'); + } else { + throw new \Exception('Could not access test/settings.ini'); + } + \OpenStack\Autoloader::useAutoloader(); + \OpenStack\Bootstrap::setConfiguration(self::$settings); - //$this->setTestNamespace('Tests\Units'); - if (file_exists('test/settings.ini')) { - self::$settings += parse_ini_file('test/settings.ini'); - } - else { - throw new \Exception('Could not access test/settings.ini'); + //parent::__construct($score, $locale, $adapter); } + /** + * Get a configuration value. + * + * Optionally, specify a default value to be used + * if none was found. + */ + public static function conf($name, $default = NULL) + { + if (isset(self::$settings[$name])) { + return self::$settings[$name]; + } - \OpenStack\Autoloader::useAutoloader(); - \OpenStack\Bootstrap::setConfiguration(self::$settings); - - //parent::__construct($score, $locale, $adapter); - } - - /** - * Get a configuration value. - * - * Optionally, specify a default value to be used - * if none was found. - */ - public static function conf($name, $default = NULL) { - if (isset(self::$settings[$name])) { - return self::$settings[$name]; + return $default; } - return $default; - } - protected $containerFixture = NULL; + protected $containerFixture = NULL; - /** - * @deprecated - */ - protected function swiftAuth() { - - $user = self::$settings['openstack.swift.account']; - $key = self::$settings['openstack.swift.key']; - $url = self::$settings['openstack.swift.url']; - //$url = self::$settings['openstack.identity.url']; - - return \OpenStack\Storage\ObjectStorage::newFromSwiftAuth($user, $key, $url, $this->getTransportClient()); - - } - - /** - * Get a handle to an IdentityService object. - * - * Authentication is performed, and the returned - * service has its tenant ID set already. - * - * identity()->token(); - * ?> - */ - protected function identity($reset = FALSE) { - - if ($reset || empty(self::$ident)) { - $user = self::conf('openstack.identity.username'); - $pass = self::conf('openstack.identity.password'); - $tenantId = self::conf('openstack.identity.tenantId'); - $url = self::conf('openstack.identity.url'); - - $is = new \OpenStack\Services\IdentityService($url); - - $token = $is->authenticateAsUser($user, $pass, $tenantId); - - self::$ident = $is; - - } - return self::$ident; - } - - protected function objectStore($reset = FALSE) { - - if ($reset || empty(self::$ostore)) { - $ident = $this->identity($reset); - - $objStore = \OpenStack\Storage\ObjectStorage::newFromIdentity($ident, self::$settings['openstack.swift.region'], $this->getTransportClient()); - - self::$ostore = $objStore; + /** + * @deprecated + */ + protected function swiftAuth() + { + $user = self::$settings['openstack.swift.account']; + $key = self::$settings['openstack.swift.key']; + $url = self::$settings['openstack.swift.url']; + //$url = self::$settings['openstack.identity.url']; + return \OpenStack\Storage\ObjectStorage::newFromSwiftAuth($user, $key, $url, $this->getTransportClient()); } - return self::$ostore; - } + /** + * Get a handle to an IdentityService object. + * + * Authentication is performed, and the returned + * service has its tenant ID set already. + * + * identity()->token(); + * ?> + */ + protected function identity($reset = FALSE) + { + if ($reset || empty(self::$ident)) { + $user = self::conf('openstack.identity.username'); + $pass = self::conf('openstack.identity.password'); + $tenantId = self::conf('openstack.identity.tenantId'); + $url = self::conf('openstack.identity.url'); - /** - * Get a container from the server. - */ - protected function containerFixture() { + $is = new \OpenStack\Services\IdentityService($url); - if (empty($this->containerFixture)) { - $store = $this->objectStore(); - $cname = self::$settings['openstack.swift.container']; + $token = $is->authenticateAsUser($user, $pass, $tenantId); - try { - $store->createContainer($cname); - $this->containerFixture = $store->container($cname); + self::$ident = $is; + + } + + return self::$ident; + } + + protected function objectStore($reset = FALSE) + { + if ($reset || empty(self::$ostore)) { + $ident = $this->identity($reset); + + $objStore = \OpenStack\Storage\ObjectStorage::newFromIdentity($ident, self::$settings['openstack.swift.region'], $this->getTransportClient()); + + self::$ostore = $objStore; + + } + + return self::$ostore; + } + + /** + * Get a container from the server. + */ + protected function containerFixture() + { + if (empty($this->containerFixture)) { + $store = $this->objectStore(); + $cname = self::$settings['openstack.swift.container']; + + try { + $store->createContainer($cname); + $this->containerFixture = $store->container($cname); + + } + // This is why PHP needs 'finally'. + catch (\Exception $e) { + // Delete the container. + $store->deleteContainer($cname); + throw $e; + } + + } + + return $this->containerFixture; + } + + /** + * Clear and destroy a container. + * + * Destroy all of the files in a container, then destroy the + * container. + * + * If the container doesn't exist, this will silently return. + * + * @param string $cname The name of the container. + */ + protected function eradicateContainer($cname) + { + $store = $this->objectStore(); + try { + $container = $store->container($cname); + } + // The container was never created. + catch (\OpenStack\Transport\FileNotFoundException $e) { + return; + } + + foreach ($container as $object) { + try { + $container->delete($object->name()); + } catch (\Exception $e) {} + } - } - // This is why PHP needs 'finally'. - catch (\Exception $e) { - // Delete the container. $store->deleteContainer($cname); - throw $e; - } } - return $this->containerFixture; - } + /** + * Retrieve the HTTP Transport Client + * + * @return \OpenStack\Transport\ClientInterface A transport client. + */ + public static function getTransportClient() + { + if (is_null(self::$httpClient)) { + $options = []; + if (isset(self::$settings['transport.proxy'])) { + $options['proxy'] = self::$settings['transport.proxy']; + } + if (isset(self::$settings['transport.debug'])) { + $options['debug'] = self::$settings['transport.debug']; + } + if (isset(self::$settings['transport.ssl.verify'])) { + $options['ssl_verify'] = self::$settings['transport.ssl.verify']; + } + if (isset(self::$settings['transport.timeout'])) { + $options['timeout'] = self::$settings['transport.timeout']; + } - /** - * Clear and destroy a container. - * - * Destroy all of the files in a container, then destroy the - * container. - * - * If the container doesn't exist, this will silently return. - * - * @param string $cname The name of the container. - */ - protected function eradicateContainer($cname) { - $store = $this->objectStore(); - try { - $container = $store->container($cname); - } - // The container was never created. - catch (\OpenStack\Transport\FileNotFoundException $e) { - return; + self::$httpClient = new self::$settings['transport']($options); + } + + return self::$httpClient; } - foreach ($container as $object) { - try { - $container->delete($object->name()); - } - catch (\Exception $e) {} + /** + * Destroy a container fixture. + * + * This should be called in any method that uses containerFixture(). + */ + protected function destroyContainerFixture() + { + $store = $this->objectStore(); + $cname = self::$settings['openstack.swift.container']; + + try { + $container = $store->container($cname); + } + // The container was never created. + catch (\OpenStack\Transport\FileNotFoundException $e) { + return; + } + + foreach ($container as $object) { + try { + $container->delete($object->name()); + } catch (\Exception $e) { + syslog(LOG_WARNING, $e); + } + } + + $store->deleteContainer($cname); } - - $store->deleteContainer($cname); - - } - - /** - * Retrieve the HTTP Transport Client - * - * @return \OpenStack\Transport\ClientInterface A transport client. - */ - public static function getTransportClient() { - - if (is_null(self::$httpClient)) { - $options = []; - if (isset(self::$settings['transport.proxy'])) { - $options['proxy'] = self::$settings['transport.proxy']; - } - if (isset(self::$settings['transport.debug'])) { - $options['debug'] = self::$settings['transport.debug']; - } - if (isset(self::$settings['transport.ssl.verify'])) { - $options['ssl_verify'] = self::$settings['transport.ssl.verify']; - } - if (isset(self::$settings['transport.timeout'])) { - $options['timeout'] = self::$settings['transport.timeout']; - } - - self::$httpClient = new self::$settings['transport']($options); - } - - return self::$httpClient; - } - - /** - * Destroy a container fixture. - * - * This should be called in any method that uses containerFixture(). - */ - protected function destroyContainerFixture() { - $store = $this->objectStore(); - $cname = self::$settings['openstack.swift.container']; - - try { - $container = $store->container($cname); - } - // The container was never created. - catch (\OpenStack\Transport\FileNotFoundException $e) { - return; - } - - foreach ($container as $object) { - try { - $container->delete($object->name()); - } - catch (\Exception $e) { - syslog(LOG_WARNING, $e); - } - } - - $store->deleteContainer($cname); - } } diff --git a/test/Tests/ACLTest.php b/test/Tests/ACLTest.php index 85e7b2f..49e6ff3 100644 --- a/test/Tests/ACLTest.php +++ b/test/Tests/ACLTest.php @@ -2,17 +2,17 @@ /* ============================================================================ (c) Copyright 2012-2014 Hewlett-Packard Development Company, L.P. - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at - http://www.apache.org/licenses/LICENSE-2.0 + http://www.apache.org/licenses/LICENSE-2.0 - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. ============================================================================ */ /** * Unit tests for ObjectStorage ACLs. @@ -25,182 +25,193 @@ use \OpenStack\Storage\ObjectStorage\ACL; /** * @ingroup Tests */ -class ACLTest extends \OpenStack\Tests\TestCase { +class ACLTest extends \OpenStack\Tests\TestCase +{ + public function testConstructor() + { + $acl = new ACL(); + $this->assertEmpty($acl->rules()); - public function testConstructor() { - $acl = new ACL(); - $this->assertEmpty($acl->rules()); + } - } + public function testAddAccount() + { + $acl = new ACL(); - public function testAddAccount() { - $acl = new ACL(); + $acl->addAccount(ACL::READ, 'test'); - $acl->addAccount(ACL::READ, 'test'); + $rules = $acl->rules(); - $rules = $acl->rules(); + $this->assertEquals(1, count($rules)); - $this->assertEquals(1, count($rules)); + $rule = array_shift($rules); - $rule = array_shift($rules); + $this->assertEquals(ACL::READ, $rule['mask']); + $this->assertEquals('test', $rule['account']); - $this->assertEquals(ACL::READ, $rule['mask']); - $this->assertEquals('test', $rule['account']); + // Test with user + $acl = new ACL(); + $acl->addAccount(ACL::WRITE, 'admin', 'earnie'); + $rules = $acl->rules(); + $rule = array_shift($rules); - // Test with user - $acl = new ACL(); - $acl->addAccount(ACL::WRITE, 'admin', 'earnie'); - $rules = $acl->rules(); - $rule = array_shift($rules); + $this->assertEquals(ACL::WRITE, $rule['mask']); + $this->assertEquals('admin', $rule['account']); + $this->assertEquals('earnie', $rule['user']); - $this->assertEquals(ACL::WRITE, $rule['mask']); - $this->assertEquals('admin', $rule['account']); - $this->assertEquals('earnie', $rule['user']); + // Test with multiple users: + $acl = new ACL(); + $acl->addAccount(ACL::WRITE, 'admin', array('earnie', 'bert')); + $rules = $acl->rules(); + $rule = array_shift($rules); - // Test with multiple users: - $acl = new ACL(); - $acl->addAccount(ACL::WRITE, 'admin', array('earnie', 'bert')); - $rules = $acl->rules(); - $rule = array_shift($rules); + $this->assertEquals(ACL::WRITE, $rule['mask']); + $this->assertEquals('admin', $rule['account']); + $this->assertEquals('earnie', $rule['user'][0]); + $this->assertEquals('bert', $rule['user'][1]); - $this->assertEquals(ACL::WRITE, $rule['mask']); - $this->assertEquals('admin', $rule['account']); - $this->assertEquals('earnie', $rule['user'][0]); - $this->assertEquals('bert', $rule['user'][1]); + } - } + public function testAddReferrer() + { + $acl = new ACL(); + $acl->addReferrer(ACL::READ, '.example.com'); + $acl->addReferrer(ACL::READ_WRITE, '-bad.example.com'); - public function testAddReferrer() { - $acl = new ACL(); - $acl->addReferrer(ACL::READ, '.example.com'); - $acl->addReferrer(ACL::READ_WRITE, '-bad.example.com'); + $rules = $acl->rules(); - $rules = $acl->rules(); + $this->assertEquals(2, count($rules)); - $this->assertEquals(2, count($rules)); + $first = array_shift($rules); + $this->assertEquals(ACL::READ, $first['mask']); + $this->assertEquals('.example.com', $first['host']); + } - $first = array_shift($rules); - $this->assertEquals(ACL::READ, $first['mask']); - $this->assertEquals('.example.com', $first['host']); - } + public function testAllowListings() + { + $acl = new ACL(); + $acl->allowListings(); + $rules = $acl->rules(); - public function testAllowListings() { - $acl = new ACL(); - $acl->allowListings(); - $rules = $acl->rules(); + $this->assertEquals(1, count($rules)); + $this->assertTrue($rules[0]['rlistings']); + $this->assertEquals(ACL::READ, $rules[0]['mask']); + } - $this->assertEquals(1, count($rules)); - $this->assertTrue($rules[0]['rlistings']); - $this->assertEquals(ACL::READ, $rules[0]['mask']); - } + public function testHeaders() + { + $acl = new ACL(); + $acl->addAccount(ACL::READ_WRITE, 'test'); - public function testHeaders() { - $acl = new ACL(); - $acl->addAccount(ACL::READ_WRITE, 'test'); + $headers = $acl->headers(); - $headers = $acl->headers(); + $this->assertEquals(2, count($headers)); + $read = $headers[ACL::HEADER_READ]; + $write = $headers[ACL::HEADER_WRITE]; - $this->assertEquals(2, count($headers)); - $read = $headers[ACL::HEADER_READ]; - $write = $headers[ACL::HEADER_WRITE]; + $this->assertEquals('test', $read); + $this->assertEquals('test', $write); - $this->assertEquals('test', $read); - $this->assertEquals('test', $write); + // Test hostname rules, which should only appear in READ. + $acl = new ACL(); + $acl->addReferrer(ACL::READ_WRITE, '.example.com'); + $headers = $acl->headers(); - // Test hostname rules, which should only appear in READ. - $acl = new ACL(); - $acl->addReferrer(ACL::READ_WRITE, '.example.com'); - $headers = $acl->headers(); + $this->assertEquals(1, count($headers), print_r($headers, TRUE)); + $read = $headers[ACL::HEADER_READ]; - $this->assertEquals(1, count($headers), print_r($headers, TRUE)); - $read = $headers[ACL::HEADER_READ]; + $this->assertEquals('.r:.example.com', $read); + } - $this->assertEquals('.r:.example.com', $read); - } + public function testToString() + { + $acl = new ACL(); + $acl->addReferrer(ACL::READ_WRITE, '.example.com'); - public function testToString() { - $acl = new ACL(); - $acl->addReferrer(ACL::READ_WRITE, '.example.com'); + $str = (string) $acl; - $str = (string) $acl; + $this->assertEquals('X-Container-Read: .r:.example.com', $str); + } - $this->assertEquals('X-Container-Read: .r:.example.com', $str); - } + public function testMakePublic() + { + $acl = (string) ACL::makePublic(); - public function testMakePublic() { - $acl = (string) ACL::makePublic(); + $this->assertEquals('X-Container-Read: .r:*,.rlistings', $acl); + } - $this->assertEquals('X-Container-Read: .r:*,.rlistings', $acl); - } + public function testMakeNonPublic() + { + $acl = (string) ACL::makeNonPublic(); - public function testMakeNonPublic() { - $acl = (string) ACL::makeNonPublic(); + $this->assertEmpty($acl); + } - $this->assertEmpty($acl); - } + public function testNewFromHeaders() + { + $headers = array( + ACL::HEADER_READ => '.r:.example.com,.rlistings,.r:-*.evil.net', + ACL::HEADER_WRITE => 'testact2, testact3:earnie, .rlistings ', + ); - public function testNewFromHeaders() { - $headers = array( - ACL::HEADER_READ => '.r:.example.com,.rlistings,.r:-*.evil.net', - ACL::HEADER_WRITE => 'testact2, testact3:earnie, .rlistings ', - ); + $acl = ACL::newFromHeaders($headers); - $acl = ACL::newFromHeaders($headers); + $rules = $acl->rules(); - $rules = $acl->rules(); + $this->assertEquals(6, count($rules)); - $this->assertEquals(6, count($rules)); + // Yay, now we get to test each one. - // Yay, now we get to test each one. + $this->assertEquals(ACL::READ, $rules[0]['mask']); + $this->assertEquals('.example.com', $rules[0]['host']); + $this->assertTrue($rules[1]['rlistings']); + $this->assertEquals('-*.evil.net', $rules[2]['host']); - $this->assertEquals(ACL::READ, $rules[0]['mask']); - $this->assertEquals('.example.com', $rules[0]['host']); - $this->assertTrue($rules[1]['rlistings']); - $this->assertEquals('-*.evil.net', $rules[2]['host']); + $this->assertEquals(ACL::WRITE, $rules[3]['mask']); + $this->assertEquals('testact2', $rules[3]['account']); + $this->assertEquals('testact3', $rules[4]['account']); + $this->assertEquals('earnie', $rules[4]['user']); + $this->assertTrue($rules[5]['rlistings']); - $this->assertEquals(ACL::WRITE, $rules[3]['mask']); - $this->assertEquals('testact2', $rules[3]['account']); - $this->assertEquals('testact3', $rules[4]['account']); - $this->assertEquals('earnie', $rules[4]['user']); - $this->assertTrue($rules[5]['rlistings']); + // Final canary: + $headers = $acl->headers(); + $read = $headers[ACL::HEADER_READ]; + $write = $headers[ACL::HEADER_WRITE]; - // Final canary: - $headers = $acl->headers(); - $read = $headers[ACL::HEADER_READ]; - $write = $headers[ACL::HEADER_WRITE]; + $this->assertEquals('.r:.example.com,.rlistings,.r:-*.evil.net', $read); + // Note that the spurious .rlistings was removed. + $this->assertEquals('testact2,testact3:earnie', $write); - $this->assertEquals('.r:.example.com,.rlistings,.r:-*.evil.net', $read); - // Note that the spurious .rlistings was removed. - $this->assertEquals('testact2,testact3:earnie', $write); + } - } + public function testIsNonPublic() + { + $acl = new ACL(); - public function testIsNonPublic() { - $acl = new ACL(); + $this->assertTrue($acl->isNonPublic()); - $this->assertTrue($acl->isNonPublic()); + $acl->addReferrer(ACL::READ, '*.evil.net'); + $this->assertFalse($acl->isNonPublic()); - $acl->addReferrer(ACL::READ, '*.evil.net'); - $this->assertFalse($acl->isNonPublic()); + $acl = ACL::makeNonPublic(); + $this->assertTrue($acl->isNonPublic()); + } - $acl = ACL::makeNonPublic(); - $this->assertTrue($acl->isNonPublic()); - } + public function testIsPublic() + { + $acl = new ACL(); - public function testIsPublic() { - $acl = new ACL(); + $this->assertFalse($acl->isPublic()); + $acl->allowListings(); + $acl->addReferrer(ACL::READ, '*'); - $this->assertFalse($acl->isPublic()); - $acl->allowListings(); - $acl->addReferrer(ACL::READ, '*'); + $this->assertTrue($acl->isPublic()); - $this->assertTrue($acl->isPublic()); + $acl->addAccount(ACL::WRITE, 'foo', 'bar'); + $this->assertTrue($acl->isPublic()); - $acl->addAccount(ACL::WRITE, 'foo', 'bar'); - $this->assertTrue($acl->isPublic()); - - $acl = ACL::makePublic(); - $this->assertTrue($acl->isPublic()); - } + $acl = ACL::makePublic(); + $this->assertTrue($acl->isPublic()); + } } diff --git a/test/Tests/AutoloaderTest.php b/test/Tests/AutoloaderTest.php index 66443ba..9cf1353 100644 --- a/test/Tests/AutoloaderTest.php +++ b/test/Tests/AutoloaderTest.php @@ -2,17 +2,17 @@ /* ============================================================================ (c) Copyright 2014 Hewlett-Packard Development Company, L.P. - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at - http://www.apache.org/licenses/LICENSE-2.0 + http://www.apache.org/licenses/LICENSE-2.0 - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. ============================================================================ */ /** * Unit tests for the Autoloader. @@ -22,25 +22,27 @@ namespace OpenStack\Tests; require_once 'src/OpenStack/Autoloader.php'; require_once 'test/TestCase.php'; -class AutoloaderTest extends \OpenStack\Tests\TestCase { +class AutoloaderTest extends \OpenStack\Tests\TestCase +{ + /** + * Test the BaseDir. + */ + public function testBasedir() + { + $basedir = \OpenStack\Autoloader::$basedir; + $this->assertRegExp('/OpenStack/', $basedir); + } - /** - * Test the BaseDir. - */ - public function testBasedir() { - $basedir = \OpenStack\Autoloader::$basedir; - $this->assertRegExp('/OpenStack/', $basedir); - } + /** + * Test the autoloader. + */ + public function testAutoloader() + { + \OpenStack\Autoloader::useAutoloader(); - /** - * Test the autoloader. - */ - public function testAutoloader() { - \OpenStack\Autoloader::useAutoloader(); + // If we can construct a class, we are okay. + $test = new \OpenStack\Exception("TEST"); - // If we can construct a class, we are okay. - $test = new \OpenStack\Exception("TEST"); - - $this->assertInstanceOf('\OpenStack\Exception', $test); - } + $this->assertInstanceOf('\OpenStack\Exception', $test); + } } diff --git a/test/Tests/BootstrapTest.php b/test/Tests/BootstrapTest.php index c0e719a..65ffd31 100644 --- a/test/Tests/BootstrapTest.php +++ b/test/Tests/BootstrapTest.php @@ -2,17 +2,17 @@ /* ============================================================================ (c) Copyright 2012-2014 Hewlett-Packard Development Company, L.P. - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at - http://www.apache.org/licenses/LICENSE-2.0 + http://www.apache.org/licenses/LICENSE-2.0 - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. ============================================================================ */ /** * Unit tests for the Bootstrap. @@ -21,12 +21,13 @@ namespace OpenStack\Tests; require_once 'test/TestCase.php'; -class BootstrapTest extends \OpenStack\Tests\TestCase { - - /** - * Canary test. - */ - public function testSettings() { - $this->assertTrue(!empty(self::$settings)); - } +class BootstrapTest extends \OpenStack\Tests\TestCase +{ + /** + * Canary test. + */ + public function testSettings() + { + $this->assertTrue(!empty(self::$settings)); + } } diff --git a/test/Tests/ContainerTest.php b/test/Tests/ContainerTest.php index 033b38d..5aeb8e1 100644 --- a/test/Tests/ContainerTest.php +++ b/test/Tests/ContainerTest.php @@ -2,17 +2,17 @@ /* ============================================================================ (c) Copyright 2012-2014 Hewlett-Packard Development Company, L.P. - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at - http://www.apache.org/licenses/LICENSE-2.0 + http://www.apache.org/licenses/LICENSE-2.0 - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. ============================================================================ */ /** * Unit tests for Containers. @@ -25,414 +25,423 @@ use \OpenStack\Storage\ObjectStorage\Container; use \OpenStack\Storage\ObjectStorage\Object; use \OpenStack\Storage\ObjectStorage\ACL; -class ContainerTest extends \OpenStack\Tests\TestCase { +class ContainerTest extends \OpenStack\Tests\TestCase +{ + const FILENAME = 'unit-test-dummy.txt'; + const FILESTR = 'This is a test.'; - const FILENAME = 'unit-test-dummy.txt'; - const FILESTR = 'This is a test.'; + // The factory functions (newFrom*) are tested in the + // ObjectStorage tests, as they are required there. + // Rather than build a Mock to achieve the same test here, + // we just don't test them again. - // The factory functions (newFrom*) are tested in the - // ObjectStorage tests, as they are required there. - // Rather than build a Mock to achieve the same test here, - // we just don't test them again. + public function testConstructor() + { + $container = new Container('foo'); + $this->assertEquals('foo', $container->name()); - public function testConstructor() { - $container = new Container('foo'); - $this->assertEquals('foo', $container->name()); - - // These will now cause the system to try to fetch a remote - // container. - //$this->assertEquals(0, $container->bytes()); - //$this->assertEquals(0, $container->count()); - } - - /** - * @expectedException \OpenStack\Exception - */ - public function testConstructorFailure() { - $container = new Container('foo'); - $this->assertEquals('foo', $container->name()); - - // These will now cause the system to try to fetch a remote - // container. This is a failure condition. - $this->assertEquals(0, $container->bytes()); - } - - public function testCountable() { - // Verify that the interface Countable is properly - // implemented. - - $mockJSON = array('count' => 5, 'bytes' => 128, 'name' => 'foo'); - - $container = Container::newFromJSON($mockJSON, 'fake', 'fake'); - - $this->assertEquals(5, count($container)); - - } - - const FNAME = 'testSave'; - const FCONTENT = 'This is a test.'; - const FTYPE = 'application/x-monkey-file'; - - public function testSave() { - - // Clean up anything left. - $this->destroyContainerFixture(); - - $container = $this->containerFixture(); - - $obj = new Object(self::FNAME, self::FCONTENT, self::FTYPE); - $obj->setMetadata(array('foo' => '1234')); - - $this->assertEquals(self::FCONTENT, $obj->content()); - - try { - $ret = $container->save($obj); - } - catch (\Exception $e) { - $this->destroyContainerFixture(); - throw $e; + // These will now cause the system to try to fetch a remote + // container. + //$this->assertEquals(0, $container->bytes()); + //$this->assertEquals(0, $container->count()); } - $this->assertTrue($ret); - } - - /** - * @depends testSave - */ - public function testRemoteObject() { - $container = $this->containerFixture(); - $object = $container->remoteObject(self::FNAME); - - $this->assertEquals(self::FNAME, $object->name()); - $this->assertEquals(self::FTYPE, $object->contentType()); - - $etag = md5(self::FCONTENT); - $this->assertEquals($etag, $object->eTag()); - - $md = $object->metadata(); - $this->assertEquals(1, count($md)); - - // Note that headers are normalized remotely to have initial - // caps. Since we have no way of knowing what the original - // metadata casing is, we leave it with initial caps. - $this->assertEquals('1234', $md['Foo']); - - $content = $object->content(); - $this->assertEquals(self::FCONTENT, $content); - - // Make sure I can do this twice (regression). - // Note that this SHOULD perform another request. - $this->assertEquals(self::FCONTENT, $object->content()); - - // Overwrite the copy: - $object->setContent('HI'); - $this->assertEquals('HI', $object->content()); - - // Make sure I can do this twice (regression check). - $this->assertEquals('HI', $object->content()); - } - - - /** - * @depends testRemoteObject - */ - public function testRefresh() { - $container = $this->containerFixture(); - $object = $container->remoteObject(self::FNAME); - - $content = (string) $object->content(); - $object->setContent('FOO'); - $this->assertEquals('FOO', $object->content()); - - $object->refresh(TRUE); - $this->assertEquals($content, (string) $object->content()); - - $object->refresh(FALSE); - $this->assertEquals($content, (string) $object->content()); - - } - - /** - * @depends testRemoteObject - */ - public function testObject() { - $container = $this->containerFixture(); - $object = $container->object(self::FNAME); - - $this->assertEquals(self::FNAME, $object->name()); - $this->assertEquals(self::FTYPE, $object->contentType()); - - $etag = md5(self::FCONTENT); - $this->assertEquals($etag, $object->eTag()); - - $md = $object->metadata(); - $this->assertEquals(1, count($md)); - - // Note that headers are normalized remotely to have initial - // caps. Since we have no way of knowing what the original - // metadata casing is, we leave it with initial caps. - $this->assertEquals('1234', $md['Foo']); - - $content = $object->content(); - - $this->assertEquals(self::FCONTENT, $content); - - // Overwrite the copy: - $object->setContent('HI'); - $this->assertEquals('HI', $object->content()); - - // Make sure this throws a 404. - try { - $foo = $container->object('no/such'); - } - catch (\OpenStack\Exception $e) { - $this->assertInstanceOf('\OpenStack\Transport\FileNotFoundException', $e); - } - } - - /** - * @depends testSave - */ - public function testObjects() { - $container = $this->containerFixture(); - $obj1 = new Object('a/' . self::FNAME, self::FCONTENT, self::FTYPE); - $obj2 = new Object('a/b/' . self::FNAME, self::FCONTENT, self::FTYPE); - - $container->save($obj1); - $container->save($obj2); - - // Now we have a container with three items. - $objects = $container->objects(); - - $this->assertEquals(3, count($objects)); - - $objects = $container->objects(1, 'a/' . self::FNAME); - - $this->assertEquals(1, count($objects)); - } - - /** - * @depends testObjects - */ - public function testGetIterator() { - - $container = $this->containerFixture(); - - $it = $container->getIterator(); - $this->assertInstanceOf('Traversable', $it); - - $i = 0; - foreach ($container as $item) { - ++$i; - } - $this->assertEquals(3, $i); - - } - - /** - * @depends testObjects - */ - public function testObjectsWithPrefix() { - $container = $this->containerFixture(); - - $objects = $container->objectsWithPrefix('a/'); - $this->assertEquals(2, count($objects)); - - foreach ($objects as $o) { - if ($o instanceof Object) { - $this->assertEquals('a/' . self::FNAME, $o->name()); - } - else { - $this->assertEquals('a/b/', $o->path()); - } - - } - - // Since we set the delimiter to ':' we will get back - // all of the objects in a/. This is because none of - // the objects contain ':' in their names. - $objects = $container->objectsWithPrefix('a/', ':'); - $this->assertEquals(2, count($objects)); - - foreach ($objects as $o) { - $this->assertInstanceOf('\OpenStack\Storage\ObjectStorage\Object', $o); - } - - // This should give us one file and one subdir. - $objects = $container->objectsWithPrefix('', '/'); - $this->assertEquals(2, count($objects)); - - foreach ($objects as $o) { - if ($o instanceof Object) { - $this->assertEquals(self::FNAME, $o->name()); - } - else { - $this->assertEquals('a/', $o->path()); - } - } - } - - /** - * @depends testObjects - */ - public function testObjectsWithPath() { - $container = $this->containerFixture(); - $objects = $container->objectsByPath('a/b/'); - - $this->assertEquals(1, count($objects)); - - $o = array_shift($objects); - $this->assertEquals('a/b/' . self::FNAME, $o->name()); - - /* - * The Open Stack documentation is unclear about how best to - * use paths. Experimentation suggests that if you rely on paths - * instead of prefixes, your best bet is to create directory - * markers. + /** + * @expectedException \OpenStack\Exception */ + public function testConstructorFailure() + { + $container = new Container('foo'); + $this->assertEquals('foo', $container->name()); - // Test subdir listings: - // This does not work (by design?) with Path. You have to use prefix - // or else create directory markers. - // $obj1 = new Object('a/aa/aaa/' . self::FNAME, self::FCONTENT, self::FTYPE); - // $container->save($obj1); - // $objects = $container->objectsByPath('a/aaa', '/'); - - // $this->assertEquals(1, count($objects), 'One subdir'); - - // $objects = $container->objectsByPath('a/'); - // throw new \Exception(print_r($objects, TRUE)); - // $this->assertEquals(2, count($objects)); - - // foreach ($objects as $o) { - // if ($o instanceof Object) { - // $this->assertEquals('a/' . self::FNAME, $o->name()); - // } - // else { - // $this->assertEquals('a/b/', $o->path()); - // } - // } - } - - /** - * @depends testRemoteObject - */ - public function testUpdateMetadata() { - $container = $this->containerFixture(); - $object = $container->remoteObject(self::FNAME); - - $md = $object->metadata(); - - $this->assertEquals('1234', $md['Foo']); - - $md['Foo'] = 456; - $md['Bar'] = 'bert'; - $object->setMetadata($md); - - $container->updateMetadata($object); - - $copy = $container->remoteObject(self::FNAME); - - $this->assertEquals('456', $md['Foo']); - $this->assertEquals('bert', $md['Bar']); - - // Now we need to canary test: - $this->assertEquals($object->contentType(), $copy->contentType()); - $this->assertEquals($object->contentLength(), $copy->contentLength()); - - - } - - /** - * @depends testRemoteObject - */ - public function testCopy() { - $container = $this->containerFixture(); - $object = $container->remoteObject(self::FNAME); - - $container->copy($object, 'FOO-1.txt'); - - $copy = $container->remoteObject('FOO-1.txt'); - - $this->assertEquals($object->contentType(), $copy->contentType()); - $this->assertEquals($object->etag(), $copy->etag()); - - $container->delete('foo-1.txt'); - - } - - /** - * @depends testCopy - */ - public function testCopyAcrossContainers() { - - // Create a new container. - $store = $this->objectStore(); - $cname = self::$settings['openstack.swift.container'] . 'COPY'; - if ($store->hasContainer($cname)) { - $this->eradicateContainer($cname); + // These will now cause the system to try to fetch a remote + // container. This is a failure condition. + $this->assertEquals(0, $container->bytes()); } - $store->createContainer($cname); - $newContainer = $store->container($cname); + public function testCountable() + { + // Verify that the interface Countable is properly + // implemented. - // Get teh old container and its object. - $container = $this->containerFixture(); - $object = $container->remoteObject(self::FNAME); + $mockJSON = array('count' => 5, 'bytes' => 128, 'name' => 'foo'); - $ret = $container->copy($object, 'foo-1.txt', $cname); + $container = Container::newFromJSON($mockJSON, 'fake', 'fake'); - $this->assertTrue($ret); + $this->assertEquals(5, count($container)); - $copy = $newContainer->remoteObject('foo-1.txt'); - - $this->assertEquals($object->etag(), $copy->etag()); - - $this->eradicateContainer($cname); - - } - - - /** - * @depends testSave - */ - public function testDelete() { - $container = $this->containerFixture(); - - $ret = $container->delete(self::FNAME); - - $fail = $container->delete('no_such_file.txt'); - - $this->destroyContainerFixture(); - $this->assertTrue($ret); - $this->assertFalse($fail); - - } - - /** - * @group public - */ - public function testAcl() { - $store = $this->objectStore(); - $cname = self::$settings['openstack.swift.container'] . 'PUBLIC'; - - if ($store->hasContainer($cname)) { - $store->deleteContainer($cname); } - $store->createContainer($cname, ACL::makePublic()); + const FNAME = 'testSave'; + const FCONTENT = 'This is a test.'; + const FTYPE = 'application/x-monkey-file'; + + public function testSave() + { + // Clean up anything left. + $this->destroyContainerFixture(); + + $container = $this->containerFixture(); + + $obj = new Object(self::FNAME, self::FCONTENT, self::FTYPE); + $obj->setMetadata(array('foo' => '1234')); + + $this->assertEquals(self::FCONTENT, $obj->content()); + + try { + $ret = $container->save($obj); + } catch (\Exception $e) { + $this->destroyContainerFixture(); + throw $e; + } + + $this->assertTrue($ret); + } + + /** + * @depends testSave + */ + public function testRemoteObject() + { + $container = $this->containerFixture(); + $object = $container->remoteObject(self::FNAME); + + $this->assertEquals(self::FNAME, $object->name()); + $this->assertEquals(self::FTYPE, $object->contentType()); + + $etag = md5(self::FCONTENT); + $this->assertEquals($etag, $object->eTag()); + + $md = $object->metadata(); + $this->assertEquals(1, count($md)); + + // Note that headers are normalized remotely to have initial + // caps. Since we have no way of knowing what the original + // metadata casing is, we leave it with initial caps. + $this->assertEquals('1234', $md['Foo']); + + $content = $object->content(); + $this->assertEquals(self::FCONTENT, $content); + + // Make sure I can do this twice (regression). + // Note that this SHOULD perform another request. + $this->assertEquals(self::FCONTENT, $object->content()); + + // Overwrite the copy: + $object->setContent('HI'); + $this->assertEquals('HI', $object->content()); + + // Make sure I can do this twice (regression check). + $this->assertEquals('HI', $object->content()); + } - $store->containers(); - $container = $store->container($cname); + /** + * @depends testRemoteObject + */ + public function testRefresh() + { + $container = $this->containerFixture(); + $object = $container->remoteObject(self::FNAME); - $acl = $container->acl(); + $content = (string) $object->content(); + $object->setContent('FOO'); + $this->assertEquals('FOO', $object->content()); - $this->assertInstanceOf('\OpenStack\Storage\ObjectStorage\ACL', $acl); - $this->assertTrue($acl->isPublic()); + $object->refresh(TRUE); + $this->assertEquals($content, (string) $object->content()); - $store->deleteContainer($cname); + $object->refresh(FALSE); + $this->assertEquals($content, (string) $object->content()); - } + } + + /** + * @depends testRemoteObject + */ + public function testObject() + { + $container = $this->containerFixture(); + $object = $container->object(self::FNAME); + + $this->assertEquals(self::FNAME, $object->name()); + $this->assertEquals(self::FTYPE, $object->contentType()); + + $etag = md5(self::FCONTENT); + $this->assertEquals($etag, $object->eTag()); + + $md = $object->metadata(); + $this->assertEquals(1, count($md)); + + // Note that headers are normalized remotely to have initial + // caps. Since we have no way of knowing what the original + // metadata casing is, we leave it with initial caps. + $this->assertEquals('1234', $md['Foo']); + + $content = $object->content(); + + $this->assertEquals(self::FCONTENT, $content); + + // Overwrite the copy: + $object->setContent('HI'); + $this->assertEquals('HI', $object->content()); + + // Make sure this throws a 404. + try { + $foo = $container->object('no/such'); + } catch (\OpenStack\Exception $e) { + $this->assertInstanceOf('\OpenStack\Transport\FileNotFoundException', $e); + } + } + + /** + * @depends testSave + */ + public function testObjects() + { + $container = $this->containerFixture(); + $obj1 = new Object('a/' . self::FNAME, self::FCONTENT, self::FTYPE); + $obj2 = new Object('a/b/' . self::FNAME, self::FCONTENT, self::FTYPE); + + $container->save($obj1); + $container->save($obj2); + + // Now we have a container with three items. + $objects = $container->objects(); + + $this->assertEquals(3, count($objects)); + + $objects = $container->objects(1, 'a/' . self::FNAME); + + $this->assertEquals(1, count($objects)); + } + + /** + * @depends testObjects + */ + public function testGetIterator() + { + $container = $this->containerFixture(); + + $it = $container->getIterator(); + $this->assertInstanceOf('Traversable', $it); + + $i = 0; + foreach ($container as $item) { + ++$i; + } + $this->assertEquals(3, $i); + + } + + /** + * @depends testObjects + */ + public function testObjectsWithPrefix() + { + $container = $this->containerFixture(); + + $objects = $container->objectsWithPrefix('a/'); + $this->assertEquals(2, count($objects)); + + foreach ($objects as $o) { + if ($o instanceof Object) { + $this->assertEquals('a/' . self::FNAME, $o->name()); + } else { + $this->assertEquals('a/b/', $o->path()); + } + + } + + // Since we set the delimiter to ':' we will get back + // all of the objects in a/. This is because none of + // the objects contain ':' in their names. + $objects = $container->objectsWithPrefix('a/', ':'); + $this->assertEquals(2, count($objects)); + + foreach ($objects as $o) { + $this->assertInstanceOf('\OpenStack\Storage\ObjectStorage\Object', $o); + } + + // This should give us one file and one subdir. + $objects = $container->objectsWithPrefix('', '/'); + $this->assertEquals(2, count($objects)); + + foreach ($objects as $o) { + if ($o instanceof Object) { + $this->assertEquals(self::FNAME, $o->name()); + } else { + $this->assertEquals('a/', $o->path()); + } + } + } + + /** + * @depends testObjects + */ + public function testObjectsWithPath() + { + $container = $this->containerFixture(); + $objects = $container->objectsByPath('a/b/'); + + $this->assertEquals(1, count($objects)); + + $o = array_shift($objects); + $this->assertEquals('a/b/' . self::FNAME, $o->name()); + + /* + * The Open Stack documentation is unclear about how best to + * use paths. Experimentation suggests that if you rely on paths + * instead of prefixes, your best bet is to create directory + * markers. + */ + + // Test subdir listings: + // This does not work (by design?) with Path. You have to use prefix + // or else create directory markers. + // $obj1 = new Object('a/aa/aaa/' . self::FNAME, self::FCONTENT, self::FTYPE); + // $container->save($obj1); + // $objects = $container->objectsByPath('a/aaa', '/'); + + // $this->assertEquals(1, count($objects), 'One subdir'); + + // $objects = $container->objectsByPath('a/'); + // throw new \Exception(print_r($objects, TRUE)); + // $this->assertEquals(2, count($objects)); + + // foreach ($objects as $o) { + // if ($o instanceof Object) { + // $this->assertEquals('a/' . self::FNAME, $o->name()); + // } + // else { + // $this->assertEquals('a/b/', $o->path()); + // } + // } + } + + /** + * @depends testRemoteObject + */ + public function testUpdateMetadata() + { + $container = $this->containerFixture(); + $object = $container->remoteObject(self::FNAME); + + $md = $object->metadata(); + + $this->assertEquals('1234', $md['Foo']); + + $md['Foo'] = 456; + $md['Bar'] = 'bert'; + $object->setMetadata($md); + + $container->updateMetadata($object); + + $copy = $container->remoteObject(self::FNAME); + + $this->assertEquals('456', $md['Foo']); + $this->assertEquals('bert', $md['Bar']); + + // Now we need to canary test: + $this->assertEquals($object->contentType(), $copy->contentType()); + $this->assertEquals($object->contentLength(), $copy->contentLength()); + + + } + + /** + * @depends testRemoteObject + */ + public function testCopy() + { + $container = $this->containerFixture(); + $object = $container->remoteObject(self::FNAME); + + $container->copy($object, 'FOO-1.txt'); + + $copy = $container->remoteObject('FOO-1.txt'); + + $this->assertEquals($object->contentType(), $copy->contentType()); + $this->assertEquals($object->etag(), $copy->etag()); + + $container->delete('foo-1.txt'); + + } + + /** + * @depends testCopy + */ + public function testCopyAcrossContainers() + { + // Create a new container. + $store = $this->objectStore(); + $cname = self::$settings['openstack.swift.container'] . 'COPY'; + if ($store->hasContainer($cname)) { + $this->eradicateContainer($cname); + } + + $store->createContainer($cname); + $newContainer = $store->container($cname); + + // Get teh old container and its object. + $container = $this->containerFixture(); + $object = $container->remoteObject(self::FNAME); + + $ret = $container->copy($object, 'foo-1.txt', $cname); + + $this->assertTrue($ret); + + $copy = $newContainer->remoteObject('foo-1.txt'); + + $this->assertEquals($object->etag(), $copy->etag()); + + $this->eradicateContainer($cname); + + } + + + /** + * @depends testSave + */ + public function testDelete() + { + $container = $this->containerFixture(); + + $ret = $container->delete(self::FNAME); + + $fail = $container->delete('no_such_file.txt'); + + $this->destroyContainerFixture(); + $this->assertTrue($ret); + $this->assertFalse($fail); + + } + + /** + * @group public + */ + public function testAcl() + { + $store = $this->objectStore(); + $cname = self::$settings['openstack.swift.container'] . 'PUBLIC'; + + if ($store->hasContainer($cname)) { + $store->deleteContainer($cname); + } + + $store->createContainer($cname, ACL::makePublic()); + + + $store->containers(); + $container = $store->container($cname); + + $acl = $container->acl(); + + $this->assertInstanceOf('\OpenStack\Storage\ObjectStorage\ACL', $acl); + $this->assertTrue($acl->isPublic()); + + $store->deleteContainer($cname); + + } } diff --git a/test/Tests/GuzzleClientTest.php b/test/Tests/GuzzleClientTest.php index 9368c95..92ca175 100644 --- a/test/Tests/GuzzleClientTest.php +++ b/test/Tests/GuzzleClientTest.php @@ -2,17 +2,17 @@ /* ============================================================================ (c) Copyright 2014 Hewlett-Packard Development Company, L.P. - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at - http://www.apache.org/licenses/LICENSE-2.0 + http://www.apache.org/licenses/LICENSE-2.0 - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. ============================================================================ */ namespace OpenStack\Tests; @@ -21,52 +21,55 @@ use OpenStack\Transport\GuzzleClient; require_once 'test/TestCase.php'; -class GuzzleClientTest extends \OpenStack\Tests\TestCase { +class GuzzleClientTest extends \OpenStack\Tests\TestCase +{ + /** + * Get the config from the test settings file and pass that into the client. + */ + public function buildClient() + { + $options = array(); + if (isset(self::$settings['transport.proxy'])) { + $options['proxy'] = self::$settings['transport.proxy']; + } + if (isset(self::$settings['transport.debug'])) { + $options['debug'] = self::$settings['transport.debug']; + } + if (isset(self::$settings['transport.ssl.verify'])) { + $options['ssl_verify'] = self::$settings['transport.ssl.verify']; + } + if (isset(self::$settings['transport.timeout'])) { + $options['timeout'] = self::$settings['transport.timeout']; + } - /** - * Get the config from the test settings file and pass that into the client. - */ - public function buildClient() { - $options = array(); - if (isset(self::$settings['transport.proxy'])) { - $options['proxy'] = self::$settings['transport.proxy']; - } - if (isset(self::$settings['transport.debug'])) { - $options['debug'] = self::$settings['transport.debug']; - } - if (isset(self::$settings['transport.ssl.verify'])) { - $options['ssl_verify'] = self::$settings['transport.ssl.verify']; - } - if (isset(self::$settings['transport.timeout'])) { - $options['timeout'] = self::$settings['transport.timeout']; + return new GuzzleClient($options); } - return new GuzzleClient($options); - } + public function testDoRequest() + { + $url = 'http://www.openstack.org'; + $method = 'GET'; - public function testDoRequest() { - $url = 'http://www.openstack.org'; - $method = 'GET'; + $client = $this->buildClient(); - $client = $this->buildClient(); + $this->assertInstanceOf('\OpenStack\Transport\GuzzleClient', $client); - $this->assertInstanceOf('\OpenStack\Transport\GuzzleClient', $client); + $response = $client->doRequest($url, $method); + $this->assertInstanceOf('\GuzzleHttp\Message\Response', $response); - $response = $client->doRequest($url, $method); - $this->assertInstanceOf('\GuzzleHttp\Message\Response', $response); + } - } + /** + * @depends testDoRequest + * @expectedException \OpenStack\Transport\FileNotFoundException + */ + public function testDoRequestException() + { + $url = 'http://www.openstack.org/this-does-no-exist'; + $method = 'GET'; - /** - * @depends testDoRequest - * @expectedException \OpenStack\Transport\FileNotFoundException - */ - public function testDoRequestException() { - $url = 'http://www.openstack.org/this-does-no-exist'; - $method = 'GET'; + $client = $this->buildClient(); + $client->doRequest($url, $method); + } - $client = $this->buildClient(); - $client->doRequest($url, $method); - } - -} \ No newline at end of file +} diff --git a/test/Tests/IdentityServicesTest.php b/test/Tests/IdentityServicesTest.php index 271fd1a..bb0079c 100644 --- a/test/Tests/IdentityServicesTest.php +++ b/test/Tests/IdentityServicesTest.php @@ -2,17 +2,17 @@ /* ============================================================================ (c) Copyright 2012-2014 Hewlett-Packard Development Company, L.P. - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at - http://www.apache.org/licenses/LICENSE-2.0 + http://www.apache.org/licenses/LICENSE-2.0 - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. ============================================================================ */ /** * Unit tests for IdentityService. @@ -24,414 +24,423 @@ require_once 'test/TestCase.php'; use \OpenStack\Services\IdentityService; use \OpenStack\Bootstrap; +class IdentityServicesTest extends \OpenStack\Tests\TestCase +{ + public function testConstructor() + { + $endpoint = self::conf('openstack.identity.url'); + $this->assertNotEmpty($endpoint); -class IdentityServiceTest extends \OpenStack\Tests\TestCase { + $service = new IdentityService($endpoint, $this->getTransportClient()); - public function testConstructor(){ - $endpoint = self::conf('openstack.identity.url'); - $this->assertNotEmpty($endpoint); + $this->assertInstanceOf('\OpenStack\Services\IdentityService', $service); - $service = new IdentityService($endpoint, $this->getTransportClient()); - - $this->assertInstanceOf('\OpenStack\Services\IdentityService', $service); - - return $service; - } - - /** - * @depends testConstructor - */ - public function testUrl() { - $endpoint = self::conf('openstack.identity.url'); - $service = new IdentityService($endpoint, $this->getTransportClient()); - - // If there is a trailing / we remove that from the endpoint. Our calls add - // the / back where appropriate. - $this->assertStringStartsWith(rtrim($endpoint, '/'), $service->url()); - - return $service; - } - - /** - * @depends testUrl - */ - public function testAuthenticate($service){ - - // Canary: Make sure all the required params are declared. - $settings = array( - 'openstack.identity.username', - 'openstack.identity.password', - 'openstack.identity.tenantId', - ); - foreach ($settings as $setting) { - $this->assertNotEmpty(self::conf($setting), "Required param: " . $setting); + return $service; } - // Test username/password auth. - $auth = array( - 'passwordCredentials' => array( - 'username' => self::conf('openstack.identity.username'), - 'password' => self::conf('openstack.identity.password'), - ), - 'tenantId' => self::conf('openstack.identity.tenantId'), - ); - $tok = $service->authenticate($auth); - $this->assertNotEmpty($tok); + /** + * @depends testConstructor + */ + public function testUrl() + { + $endpoint = self::conf('openstack.identity.url'); + $service = new IdentityService($endpoint, $this->getTransportClient()); - // We should get the same token if we request again. - $service = new IdentityService(self::conf('openstack.identity.url'), $this->getTransportClient()); - $tok2 = $service->authenticate($auth); - $this->assertEquals($tok, $tok2); + // If there is a trailing / we remove that from the endpoint. Our calls add + // the / back where appropriate. + $this->assertStringStartsWith(rtrim($endpoint, '/'), $service->url()); - // Again with no tenant ID. - $auth = array( - 'passwordCredentials' => array( - 'username' => self::conf('openstack.identity.username'), - 'password' => self::conf('openstack.identity.password'), - ), - //'tenantId' => self::conf('openstack.identity.tenantId'), - ); - $tok = $service->authenticate($auth); - $this->assertNotEmpty($tok); - } - - /** - * @depends testAuthenticate - */ - public function testAuthenticateAsUser() { - $service = new IdentityService(self::conf('openstack.identity.url'), $this->getTransportClient()); - - $user = self::conf('openstack.identity.username'); - $pass = self::conf('openstack.identity.password'); - $tenantId = self::conf('openstack.identity.tenantId'); - - $tok = $service->authenticateAsUser($user, $pass, $tenantId); - - $this->assertNotEmpty($tok); - - // Try again, this time with no tenant ID. - $tok2 = $service->authenticateAsUser($user, $pass); - $this->assertNotEmpty($tok2); - - $details = $service->tokenDetails(); - $this->assertFalse(isset($details['tenant'])); - - return $service; - } - - /** - * @depends testAuthenticateAsUser - */ - public function testToken($service) { - $this->assertNotEmpty($service->token()); - } - - /** - * @depends testAuthenticateAsUser - */ - public function testIsExpired($service) { - $this->assertFalse($service->isExpired()); - - $service2 = new IdentityService(self::conf('openstack.identity.url'), $this->getTransportClient()); - $this->assertTrue($service2->isExpired()); - } - - /** - * @depends testAuthenticateAsUser - */ - public function testTenantName() { - $user = self::conf('openstack.identity.username'); - $pass = self::conf('openstack.identity.password'); - $tenantName = self::conf('openstack.identity.tenantName'); - - $service = new IdentityService(self::conf('openstack.identity.url'), $this->getTransportClient()); - $this->assertNull($service->tenantName()); - - $service->authenticateAsUser($user, $pass); - $this->assertEmpty($service->tenantName()); - - $service = new IdentityService(self::conf('openstack.identity.url'), $this->getTransportClient()); - $ret = $service->authenticateAsUser($user, $pass, NULL, $tenantName); - $this->assertNotEmpty($service->tenantName()); - - $service = new IdentityService(self::conf('openstack.identity.url'), $this->getTransportClient()); - $this->assertNull($service->tenantName()); - } - - /** - * @depends testAuthenticateAsUser - */ - public function testTenantId() { - $user = self::conf('openstack.identity.username'); - $pass = self::conf('openstack.identity.password'); - $tenantId = self::conf('openstack.identity.tenantId'); - - $service = new IdentityService(self::conf('openstack.identity.url'), $this->getTransportClient()); - $this->assertNull($service->tenantId()); - - $service->authenticateAsUser($user, $pass); - $this->assertEmpty($service->tenantId()); - - $service = new IdentityService(self::conf('openstack.identity.url'), $this->getTransportClient()); - $service->authenticateAsUser($user, $pass, $tenantId); - $this->assertNotEmpty($service->tenantId()); - } - - /** - * @depends testAuthenticateAsUser - */ - public function testTokenDetails() { - $now = time(); - $user = self::conf('openstack.identity.username'); - $pass = self::conf('openstack.identity.password'); - $tenantId = self::conf('openstack.identity.tenantId'); - - $service = new IdentityService(self::conf('openstack.identity.url'), $this->getTransportClient()); - $service->authenticateAsUser($user, $pass); - - // Details for user auth. - $details = $service->tokenDetails(); - $this->assertNotEmpty($details['id']); - $this->assertFalse(isset($details['tenant'])); - - $ts = strtotime($details['expires']); - $this->assertGreaterThan($now, $ts); - - - // Test details for username auth. - $service = new IdentityService(self::conf('openstack.identity.url'), $this->getTransportClient()); - $service->authenticateAsUser($user, $pass, $tenantId); - - $details = $service->tokenDetails(); - - $expectUser = self::conf('openstack.identity.username'); - - $this->assertStringStartsWith($expectUser, $details['tenant']['name']); - $this->assertNotEmpty($details['id']); - $this->assertNotEmpty($details['tenant']['id']); - - $this->assertEquals($tenantId, $details['tenant']['id']); - - $ts = strtotime($details['expires']); - $this->assertGreaterThan($now, $ts); - } - - /** - * @depends testAuthenticateAsUser - */ - public function testServiceCatalog($service) { - $catalog = $service->serviceCatalog(); - - $this->assertGreaterThan(0, count($catalog)); - - $idService = NULL; - foreach ($catalog as $item) { - if ($item['type'] == 'identity') { - $idService = $item; - } + return $service; } - $this->assertEquals('Identity', $idService['name']); - $this->assertNotEmpty($idService['endpoints']); - $this->assertNotEmpty($idService['endpoints'][0]['publicURL']); + /** + * @depends testUrl + */ + public function testAuthenticate($service) + { + // Canary: Make sure all the required params are declared. + $settings = array( + 'openstack.identity.username', + 'openstack.identity.password', + 'openstack.identity.tenantId', + ); + foreach ($settings as $setting) { + $this->assertNotEmpty(self::conf($setting), "Required param: " . $setting); + } - // Test filters. - $justID = $service->serviceCatalog('identity'); - $this->assertEquals(1, count($justID)); + // Test username/password auth. + $auth = array( + 'passwordCredentials' => array( + 'username' => self::conf('openstack.identity.username'), + 'password' => self::conf('openstack.identity.password'), + ), + 'tenantId' => self::conf('openstack.identity.tenantId'), + ); + $tok = $service->authenticate($auth); + $this->assertNotEmpty($tok); - $idService = $justID[0]; - $this->assertEquals('Identity', $idService['name']); - $this->assertNotEmpty($idService['endpoints']); - $this->assertNotEmpty($idService['endpoints'][0]['publicURL']); + // We should get the same token if we request again. + $service = new IdentityService(self::conf('openstack.identity.url'), $this->getTransportClient()); + $tok2 = $service->authenticate($auth); + $this->assertEquals($tok, $tok2); - // Make sure a missed filter returns an empty set. - $expectEmpty = $service->serviceCatalog('no-such-servicename'); - $this->assertEmpty($expectEmpty); - } - - - /** - * @depends testAuthenticateAsUser - */ - public function testUser($service) { - $user = $service->user(); - - $this->assertEquals(self::conf('openstack.identity.username'), $user['name']); - $this->assertNotEmpty($user['roles']); - } - - /** - * @depends testAuthenticateAsUser - * @group serialize - */ - public function testSerialization($service) { - - $ser = serialize($service); - - $this->assertNotEmpty($ser); - - $again = unserialize($ser); - - $this->assertInstanceOf('\OpenStack\Services\IdentityService', $again); - - $this->assertEquals($service->tenantId(), $again->tenantId()); - $this->assertEquals($service->serviceCatalog(), $again->serviceCatalog()); - $this->assertEquals($service->tokenDetails(), $again->tokenDetails()); - $this->assertEquals($service->user(), $again->user()); - $this->assertFalse($again->isExpired()); - - $tenantId = $again->tenantId(); - - $newTok = $again->rescopeUsingTenantId($tenantId); - - $this->assertNotEmpty($newTok); - } - - /** - * @group tenant - */ - public function testTenants() { - $service = new IdentityService(self::conf('openstack.identity.url'), $this->getTransportClient()); - $service2 = new IdentityService(self::conf('openstack.identity.url'), $this->getTransportClient()); - $user = self::conf('openstack.identity.username'); - $pass = self::conf('openstack.identity.password'); - $tenantId = self::conf('openstack.identity.tenantId'); - $service->authenticateAsUser($user, $pass, $tenantId); - - - $tenants = $service2->tenants($service->token()); - - $this->assertGreaterThan(0, count($tenants)); - $this->assertNotEmpty($tenants[0]['name']); - $this->assertNotEmpty($tenants[0]['id']); - - $tenants = $service->tenants(); - $this->assertGreaterThan(0, count($tenants)); - $this->assertNotEmpty($tenants[0]['name']); - $this->assertNotEmpty($tenants[0]['id']); - - } - - /** - * @group tenant - * @depends testTenants - */ - function testRescope() { - $service = new IdentityService(self::conf('openstack.identity.url'), $this->getTransportClient()); - $user = self::conf('openstack.identity.username'); - $pass = self::conf('openstack.identity.password'); - $tenantId = self::conf('openstack.identity.tenantId'); - - // Authenticate without a tenant ID. - $token = $service->authenticateAsUser($user, $pass); - - $this->assertNotEmpty($token); - - $details = $service->tokenDetails(); - $this->assertFalse(isset($details['tenant'])); - - $service->rescopeUsingTenantId($tenantId); - - $details = $service->tokenDetails(); - $this->assertEquals($tenantId, $details['tenant']['id']); - - // Test unscoping - $service->rescopeUsingTenantId(''); - $details = $service->tokenDetails(); - $this->assertFalse(isset($details['tenant'])); - } - - /** - * @group tenant - * @depends testTenants - */ - function testRescopeByTenantName() { - $service = new IdentityService(self::conf('openstack.identity.url'), $this->getTransportClient()); - $user = self::conf('openstack.identity.username'); - $pass = self::conf('openstack.identity.password'); - $tenantName = self::conf('openstack.identity.tenantName'); - - // Authenticate without a tenant ID. - $token = $service->authenticateAsUser($user, $pass); - - $this->assertNotEmpty($token); - - $details = $service->tokenDetails(); - $this->assertFalse(isset($details['tenant'])); - - $service->rescopeUsingTenantName($tenantName); - - $details = $service->tokenDetails(); - $this->assertEquals($tenantName, $details['tenant']['name']); - - // Test unscoping - $service->rescope(''); - $details = $service->tokenDetails(); - $this->assertFalse(isset($details['tenant'])); - } - - /** - * Test the bootstrap identity factory. - * @depends testAuthenticateAsUser - */ - function testBootstrap() { - - // We need to save the config settings and reset the bootstrap to this. - // It does not remove the old settings. The means the identity fall through - // for different settings may not happen because of ordering. So, we cache - // and reset back to the default for each test. - $reset = Bootstrap::$config; - - // Test authenticating as a user. - $settings = array( - 'username' => self::conf('openstack.identity.username'), - 'password' => self::conf('openstack.identity.password'), - 'endpoint' => self::conf('openstack.identity.url'), - 'tenantid' => self::conf('openstack.identity.tenantId'), - 'transport' => self::conf('transport'), - 'transport.debug' => self::conf('transport.debug', FALSE), - 'transport.ssl_verify' => self::conf('transport.ssl', TRUE), - ); - if (self::conf('transport.timeout')) { - $setting['transport.timeout'] = self::conf('transport.timeout'); + // Again with no tenant ID. + $auth = array( + 'passwordCredentials' => array( + 'username' => self::conf('openstack.identity.username'), + 'password' => self::conf('openstack.identity.password'), + ), + //'tenantId' => self::conf('openstack.identity.tenantId'), + ); + $tok = $service->authenticate($auth); + $this->assertNotEmpty($tok); } - if (self::conf('transport.proxy')) { - $setting['transport.proxy'] = self::conf('transport.proxy'); + + /** + * @depends testAuthenticate + */ + public function testAuthenticateAsUser() + { + $service = new IdentityService(self::conf('openstack.identity.url'), $this->getTransportClient()); + + $user = self::conf('openstack.identity.username'); + $pass = self::conf('openstack.identity.password'); + $tenantId = self::conf('openstack.identity.tenantId'); + + $tok = $service->authenticateAsUser($user, $pass, $tenantId); + + $this->assertNotEmpty($tok); + + // Try again, this time with no tenant ID. + $tok2 = $service->authenticateAsUser($user, $pass); + $this->assertNotEmpty($tok2); + + $details = $service->tokenDetails(); + $this->assertFalse(isset($details['tenant'])); + + return $service; } - Bootstrap::setConfiguration($settings); - $is = Bootstrap::identity(TRUE); - $this->assertInstanceOf('\OpenStack\Services\IdentityService', $is); - - // Test getting a second instance from the cache. - $is2 = Bootstrap::identity(); - $this->assertEquals($is, $is2); - - // Test that forcing a refresh does so. - $is2 = Bootstrap::identity(TRUE); - $this->assertNotEquals($is, $is2); - - Bootstrap::$config = $reset; - - // Test with tenant name - $settings = array( - 'username' => self::conf('openstack.identity.username'), - 'password' => self::conf('openstack.identity.password'), - 'endpoint' => self::conf('openstack.identity.url'), - 'tenantname' => self::conf('openstack.identity.tenantName'), - 'transport' => self::conf('transport'), - 'transport.debug' => self::conf('transport.debug', FALSE), - 'transport.ssl_verify' => self::conf('transport.ssl', TRUE), - ); - if (self::conf('transport.timeout')) { - $setting['transport.timeout'] = self::conf('transport.timeout'); + /** + * @depends testAuthenticateAsUser + */ + public function testToken($service) + { + $this->assertNotEmpty($service->token()); } - if (self::conf('transport.proxy')) { - $setting['transport.proxy'] = self::conf('transport.proxy'); - } - Bootstrap::setConfiguration($settings); - $is = Bootstrap::identity(TRUE); - $this->assertInstanceOf('\OpenStack\Services\IdentityService', $is); - } + /** + * @depends testAuthenticateAsUser + */ + public function testIsExpired($service) + { + $this->assertFalse($service->isExpired()); + + $service2 = new IdentityService(self::conf('openstack.identity.url'), $this->getTransportClient()); + $this->assertTrue($service2->isExpired()); + } + + /** + * @depends testAuthenticateAsUser + */ + public function testTenantName() + { + $user = self::conf('openstack.identity.username'); + $pass = self::conf('openstack.identity.password'); + $tenantName = self::conf('openstack.identity.tenantName'); + + $service = new IdentityService(self::conf('openstack.identity.url'), $this->getTransportClient()); + $this->assertNull($service->tenantName()); + + $service->authenticateAsUser($user, $pass); + $this->assertEmpty($service->tenantName()); + + $service = new IdentityService(self::conf('openstack.identity.url'), $this->getTransportClient()); + $ret = $service->authenticateAsUser($user, $pass, NULL, $tenantName); + $this->assertNotEmpty($service->tenantName()); + + $service = new IdentityService(self::conf('openstack.identity.url'), $this->getTransportClient()); + $this->assertNull($service->tenantName()); + } + + /** + * @depends testAuthenticateAsUser + */ + public function testTenantId() + { + $user = self::conf('openstack.identity.username'); + $pass = self::conf('openstack.identity.password'); + $tenantId = self::conf('openstack.identity.tenantId'); + + $service = new IdentityService(self::conf('openstack.identity.url'), $this->getTransportClient()); + $this->assertNull($service->tenantId()); + + $service->authenticateAsUser($user, $pass); + $this->assertEmpty($service->tenantId()); + + $service = new IdentityService(self::conf('openstack.identity.url'), $this->getTransportClient()); + $service->authenticateAsUser($user, $pass, $tenantId); + $this->assertNotEmpty($service->tenantId()); + } + + /** + * @depends testAuthenticateAsUser + */ + public function testTokenDetails() + { + $now = time(); + $user = self::conf('openstack.identity.username'); + $pass = self::conf('openstack.identity.password'); + $tenantId = self::conf('openstack.identity.tenantId'); + + $service = new IdentityService(self::conf('openstack.identity.url'), $this->getTransportClient()); + $service->authenticateAsUser($user, $pass); + + // Details for user auth. + $details = $service->tokenDetails(); + $this->assertNotEmpty($details['id']); + $this->assertFalse(isset($details['tenant'])); + + $ts = strtotime($details['expires']); + $this->assertGreaterThan($now, $ts); + + // Test details for username auth. + $service = new IdentityService(self::conf('openstack.identity.url'), $this->getTransportClient()); + $service->authenticateAsUser($user, $pass, $tenantId); + + $details = $service->tokenDetails(); + + $expectUser = self::conf('openstack.identity.username'); + + $this->assertStringStartsWith($expectUser, $details['tenant']['name']); + $this->assertNotEmpty($details['id']); + $this->assertNotEmpty($details['tenant']['id']); + + $this->assertEquals($tenantId, $details['tenant']['id']); + + $ts = strtotime($details['expires']); + $this->assertGreaterThan($now, $ts); + } + + /** + * @depends testAuthenticateAsUser + */ + public function testServiceCatalog($service) + { + $catalog = $service->serviceCatalog(); + + $this->assertGreaterThan(0, count($catalog)); + + $idService = NULL; + foreach ($catalog as $item) { + if ($item['type'] == 'identity') { + $idService = $item; + } + } + + $this->assertEquals('Identity', $idService['name']); + $this->assertNotEmpty($idService['endpoints']); + $this->assertNotEmpty($idService['endpoints'][0]['publicURL']); + + // Test filters. + $justID = $service->serviceCatalog('identity'); + $this->assertEquals(1, count($justID)); + + $idService = $justID[0]; + $this->assertEquals('Identity', $idService['name']); + $this->assertNotEmpty($idService['endpoints']); + $this->assertNotEmpty($idService['endpoints'][0]['publicURL']); + + // Make sure a missed filter returns an empty set. + $expectEmpty = $service->serviceCatalog('no-such-servicename'); + $this->assertEmpty($expectEmpty); + } + + /** + * @depends testAuthenticateAsUser + */ + public function testUser($service) + { + $user = $service->user(); + + $this->assertEquals(self::conf('openstack.identity.username'), $user['name']); + $this->assertNotEmpty($user['roles']); + } + + /** + * @depends testAuthenticateAsUser + * @group serialize + */ + public function testSerialization($service) + { + $ser = serialize($service); + + $this->assertNotEmpty($ser); + + $again = unserialize($ser); + + $this->assertInstanceOf('\OpenStack\Services\IdentityService', $again); + + $this->assertEquals($service->tenantId(), $again->tenantId()); + $this->assertEquals($service->serviceCatalog(), $again->serviceCatalog()); + $this->assertEquals($service->tokenDetails(), $again->tokenDetails()); + $this->assertEquals($service->user(), $again->user()); + $this->assertFalse($again->isExpired()); + + $tenantId = $again->tenantId(); + + $newTok = $again->rescopeUsingTenantId($tenantId); + + $this->assertNotEmpty($newTok); + } + + /** + * @group tenant + */ + public function testTenants() + { + $service = new IdentityService(self::conf('openstack.identity.url'), $this->getTransportClient()); + $service2 = new IdentityService(self::conf('openstack.identity.url'), $this->getTransportClient()); + $user = self::conf('openstack.identity.username'); + $pass = self::conf('openstack.identity.password'); + $tenantId = self::conf('openstack.identity.tenantId'); + $service->authenticateAsUser($user, $pass, $tenantId); + + $tenants = $service2->tenants($service->token()); + + $this->assertGreaterThan(0, count($tenants)); + $this->assertNotEmpty($tenants[0]['name']); + $this->assertNotEmpty($tenants[0]['id']); + + $tenants = $service->tenants(); + $this->assertGreaterThan(0, count($tenants)); + $this->assertNotEmpty($tenants[0]['name']); + $this->assertNotEmpty($tenants[0]['id']); + + } + + /** + * @group tenant + * @depends testTenants + */ + public function testRescope() + { + $service = new IdentityService(self::conf('openstack.identity.url'), $this->getTransportClient()); + $user = self::conf('openstack.identity.username'); + $pass = self::conf('openstack.identity.password'); + $tenantId = self::conf('openstack.identity.tenantId'); + + // Authenticate without a tenant ID. + $token = $service->authenticateAsUser($user, $pass); + + $this->assertNotEmpty($token); + + $details = $service->tokenDetails(); + $this->assertFalse(isset($details['tenant'])); + + $service->rescopeUsingTenantId($tenantId); + + $details = $service->tokenDetails(); + $this->assertEquals($tenantId, $details['tenant']['id']); + + // Test unscoping + $service->rescopeUsingTenantId(''); + $details = $service->tokenDetails(); + $this->assertFalse(isset($details['tenant'])); + } + + /** + * @group tenant + * @depends testTenants + */ + public function testRescopeByTenantName() + { + $service = new IdentityService(self::conf('openstack.identity.url'), $this->getTransportClient()); + $user = self::conf('openstack.identity.username'); + $pass = self::conf('openstack.identity.password'); + $tenantName = self::conf('openstack.identity.tenantName'); + + // Authenticate without a tenant ID. + $token = $service->authenticateAsUser($user, $pass); + + $this->assertNotEmpty($token); + + $details = $service->tokenDetails(); + $this->assertFalse(isset($details['tenant'])); + + $service->rescopeUsingTenantName($tenantName); + + $details = $service->tokenDetails(); + $this->assertEquals($tenantName, $details['tenant']['name']); + + // Test unscoping + $service->rescope(''); + $details = $service->tokenDetails(); + $this->assertFalse(isset($details['tenant'])); + } + + /** + * Test the bootstrap identity factory. + * @depends testAuthenticateAsUser + */ + public function testBootstrap() + { + // We need to save the config settings and reset the bootstrap to this. + // It does not remove the old settings. The means the identity fall through + // for different settings may not happen because of ordering. So, we cache + // and reset back to the default for each test. + $reset = Bootstrap::$config; + + // Test authenticating as a user. + $settings = array( + 'username' => self::conf('openstack.identity.username'), + 'password' => self::conf('openstack.identity.password'), + 'endpoint' => self::conf('openstack.identity.url'), + 'tenantid' => self::conf('openstack.identity.tenantId'), + 'transport' => self::conf('transport'), + 'transport.debug' => self::conf('transport.debug', FALSE), + 'transport.ssl_verify' => self::conf('transport.ssl', TRUE), + ); + if (self::conf('transport.timeout')) { + $setting['transport.timeout'] = self::conf('transport.timeout'); + } + if (self::conf('transport.proxy')) { + $setting['transport.proxy'] = self::conf('transport.proxy'); + } + Bootstrap::setConfiguration($settings); + + $is = Bootstrap::identity(TRUE); + $this->assertInstanceOf('\OpenStack\Services\IdentityService', $is); + + // Test getting a second instance from the cache. + $is2 = Bootstrap::identity(); + $this->assertEquals($is, $is2); + + // Test that forcing a refresh does so. + $is2 = Bootstrap::identity(TRUE); + $this->assertNotEquals($is, $is2); + + Bootstrap::$config = $reset; + + // Test with tenant name + $settings = array( + 'username' => self::conf('openstack.identity.username'), + 'password' => self::conf('openstack.identity.password'), + 'endpoint' => self::conf('openstack.identity.url'), + 'tenantname' => self::conf('openstack.identity.tenantName'), + 'transport' => self::conf('transport'), + 'transport.debug' => self::conf('transport.debug', FALSE), + 'transport.ssl_verify' => self::conf('transport.ssl', TRUE), + ); + if (self::conf('transport.timeout')) { + $setting['transport.timeout'] = self::conf('transport.timeout'); + } + if (self::conf('transport.proxy')) { + $setting['transport.proxy'] = self::conf('transport.proxy'); + } + Bootstrap::setConfiguration($settings); + + $is = Bootstrap::identity(TRUE); + $this->assertInstanceOf('\OpenStack\Services\IdentityService', $is); + } } diff --git a/test/Tests/ObjectStorageTest.php b/test/Tests/ObjectStorageTest.php index b98b493..fb9fa0e 100644 --- a/test/Tests/ObjectStorageTest.php +++ b/test/Tests/ObjectStorageTest.php @@ -2,17 +2,17 @@ /* ============================================================================ (c) Copyright 2012-2014 Hewlett-Packard Development Company, L.P. - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at - http://www.apache.org/licenses/LICENSE-2.0 + http://www.apache.org/licenses/LICENSE-2.0 - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. ============================================================================ */ /** * Unit tests for ObjectStorage. @@ -24,281 +24,292 @@ require_once 'test/TestCase.php'; use \OpenStack\Storage\ObjectStorage\Object; use \OpenStack\Storage\ObjectStorage\ACL; - -class ObjectStorageTest extends \OpenStack\Tests\TestCase { - - /** - * Canary test. - */ - public function testSettings() { - $this->assertTrue(!empty(self::$settings)); - } - - /** - * Test Swift-based authentication. - * @group deprecated - */ - public function testSwiftAuthentication() { - - $ostore = $this->swiftAuth(); - - $this->assertInstanceOf('\OpenStack\Storage\ObjectStorage', $ostore); - $this->assertTrue(strlen($ostore->token()) > 0); - } - - /** - * @group auth - */ - public function testConstructor() { - $ident = $this->identity(); - - $services = $ident->serviceCatalog(\OpenStack\Storage\ObjectStorage::SERVICE_TYPE); - - if (empty($services)) { - throw new \Exception('No object-store service found.'); - } - - //$serviceURL = $services[0]['endpoints'][0]['adminURL']; - $serviceURL = $services[0]['endpoints'][0]['publicURL']; - - $ostore = new \OpenStack\Storage\ObjectStorage($ident->token(), $serviceURL, $this->getTransportClient()); - - $this->assertInstanceOf('\OpenStack\Storage\ObjectStorage', $ostore); - $this->assertTrue(strlen($ostore->token()) > 0); - - } - - public function testNewFromServiceCatalog() { - $ident = $this->identity(); - $tok = $ident->token(); - $cat = $ident->serviceCatalog(); - $ostore = \OpenStack\Storage\ObjectStorage::newFromServiceCatalog($cat, $tok, self::$settings['openstack.swift.region'], $this->getTransportClient()); - $this->assertInstanceOf('\OpenStack\Storage\ObjectStorage', $ostore); - $this->assertTrue(strlen($ostore->token()) > 0); - } - - public function testFailedNewFromServiceCatalog(){ - $ident = $this->identity(); - $tok = $ident->token(); - $cat = $ident->serviceCatalog(); - $ostore = \OpenStack\Storage\ObjectStorage::newFromServiceCatalog($cat, $tok, 'region-w.geo-99999.fake', $this->getTransportClient()); - $this->assertEmpty($ostore); - } - - public function testNewFromIdnetity() { - $ident = $this->identity(); - $ostore = \OpenStack\Storage\ObjectStorage::newFromIdentity($ident, self::$settings['openstack.swift.region'], $this->getTransportClient()); - $this->assertInstanceOf('\OpenStack\Storage\ObjectStorage', $ostore); - $this->assertTrue(strlen($ostore->token()) > 0); - } - - public function testNewFromIdentityAltRegion() { - $ident = $this->identity(); - $ostore = \OpenStack\Storage\ObjectStorage::newFromIdentity($ident, 'region-b.geo-1', $this->getTransportClient()); - $this->assertInstanceOf('\OpenStack\Storage\ObjectStorage', $ostore); - $this->assertTrue(strlen($ostore->token()) > 0); - - // Make sure the store is not the same as the default region. - $ostoreDefault = \OpenStack\Storage\ObjectStorage::newFromIdentity($ident, self::$settings['openstack.swift.region'], $this->getTransportClient()); - $this->assertNotEquals($ostore, $ostoreDefault); - } - - /** - * @group auth - * @ group acl - */ - public function testCreateContainer() { - $testCollection = self::$settings['openstack.swift.container']; - - $this->assertNotEmpty($testCollection, "Canary: container name must be in settings file."); - - $store = $this->objectStore();//swiftAuth(); - - $this->destroyContainerFixture(); - /* - if ($store->hasContainer($testCollection)) { - $store->deleteContainer($testCollection); - } +class ObjectStorageTest extends \OpenStack\Tests\TestCase +{ + /** + * Canary test. */ - - $md = array('Foo' => 1234); - - $ret = $store->createContainer($testCollection, NULL, $md); - $this->assertTrue($ret, "Create container"); - - } - - /** - * @group auth - * @depends testCreateContainer - */ - public function testAccountInfo () { - $store = $this->objectStore(); - - $info = $store->accountInfo(); - - $this->assertGreaterThan(0, $info['containers']); - $this->assertGreaterThanOrEqual(0, $info['bytes']); - $this->assertGreaterThanOrEqual(0, $info['objects']); - } - - /** - * @depends testCreateContainer - */ - public function testContainers() { - $store = $this->objectStore(); - $containers = $store->containers(); - - $this->assertNotEmpty($containers); - - //$first = array_shift($containers); - - $testCollection = self::conf('openstack.swift.container'); - $testContainer = $containers[$testCollection]; - $this->assertEquals($testCollection, $testContainer->name()); - $this->assertEquals(0, $testContainer->bytes()); - $this->assertEquals(0, $testContainer->count()); - - // Make sure we get back an ACL: - $this->assertInstanceOf('\OpenStack\Storage\ObjectStorage\ACL', $testContainer->acl()); - - } - - /** - * @depends testCreateContainer - */ - public function testContainer() { - $testCollection = self::$settings['openstack.swift.container']; - $store = $this->objectStore(); - - $container = $store->container($testCollection); - - $this->assertEquals(0, $container->bytes()); - $this->assertEquals(0, $container->count()); - $this->assertEquals($testCollection, $container->name()); - - $md = $container->metadata(); - $this->assertEquals(1, count($md)); - $this->assertEquals('1234', $md['Foo']); - } - - /** - * @depends testCreateContainer - */ - public function testHasContainer() { - $testCollection = self::$settings['openstack.swift.container']; - $store = $this->objectStore(); - - $this->assertTrue($store->hasContainer($testCollection)); - $this->assertFalse($store->hasContainer('nihil')); - } - - /** - * @depends testHasContainer - */ - public function testDeleteContainer() { - $testCollection = self::$settings['openstack.swift.container']; - - $store = $this->objectStore(); - //$ret = $store->createContainer($testCollection); - //$this->assertTrue($store->hasContainer($testCollection)); - - $ret = $store->deleteContainer($testCollection); - - $this->assertTrue($ret); - - // Now we try to delete a container that does not exist. - $ret = $store->deleteContainer('nihil'); - $this->assertFalse($ret); - } - - /** - * @expectedException \OpenStack\Storage\ObjectStorage\ContainerNotEmptyException - */ - public function testDeleteNonEmptyContainer() { - - $testCollection = self::$settings['openstack.swift.container']; - - $this->assertNotEmpty($testCollection); - - $store = $this->objectStore(); - $store->createContainer($testCollection); - - $container = $store->container($testCollection); - $container->save(new Object('test', 'test', 'text/plain')); - - try { - $ret = $store->deleteContainer($testCollection); - } - catch (\Exception $e) { - $container->delete('test'); - $store->deleteContainer($testCollection); - throw $e; + public function testSettings() + { + $this->assertTrue(!empty(self::$settings)); } - try { - $container->delete('test'); - } - // Skip 404s. - catch (\Exception $e) {} + /** + * Test Swift-based authentication. + * @group deprecated + */ + public function testSwiftAuthentication() + { + $ostore = $this->swiftAuth(); - $store->deleteContainer($testCollection); - } - - /** - * @depends testCreateContainer - * @group acl - */ - public function testCreateContainerPublic() { - $testCollection = self::$settings['openstack.swift.container'] . 'PUBLIC'; - $store = $this->objectStore(); - if ($store->hasContainer($testCollection)) { - $store->deleteContainer($testCollection); + $this->assertInstanceOf('\OpenStack\Storage\ObjectStorage', $ostore); + $this->assertTrue(strlen($ostore->token()) > 0); } - $ret = $store->createContainer($testCollection, ACL::makePublic()); - $container = $store->container($testCollection); + /** + * @group auth + */ + public function testConstructor() + { + $ident = $this->identity(); - // Now test that we can get the container contents. Since there is - // no content in the container, we use the format=xml to make sure - // we get some data back. - $url = $container->url() . '?format=xml'; + $services = $ident->serviceCatalog(\OpenStack\Storage\ObjectStorage::SERVICE_TYPE); - // Use CURL to get better debugging: - //$client = \OpenStack\Transport::instance(); - //$response = $client->doRequest($url, 'GET'); + if (empty($services)) { + throw new \Exception('No object-store service found.'); + } - $data = file_get_contents($url); - $this->assertNotEmpty($data, $url); + //$serviceURL = $services[0]['endpoints'][0]['adminURL']; + $serviceURL = $services[0]['endpoints'][0]['publicURL']; - $containers = $store->containers(); - //throw new \Exception(print_r($containers, TRUE)); + $ostore = new \OpenStack\Storage\ObjectStorage($ident->token(), $serviceURL, $this->getTransportClient()); - $store->deleteContainer($testCollection); - } + $this->assertInstanceOf('\OpenStack\Storage\ObjectStorage', $ostore); + $this->assertTrue(strlen($ostore->token()) > 0); - /** - * @depends testCreateContainerPublic - */ - public function testChangeContainerACL() { - $testCollection = self::$settings['openstack.swift.container'] . 'PUBLIC'; - $store = $this->objectStore(); - if ($store->hasContainer($testCollection)) { - $store->deleteContainer($testCollection); } - $ret = $store->createContainer($testCollection); + public function testNewFromServiceCatalog() + { + $ident = $this->identity(); + $tok = $ident->token(); + $cat = $ident->serviceCatalog(); + $ostore = \OpenStack\Storage\ObjectStorage::newFromServiceCatalog($cat, $tok, self::$settings['openstack.swift.region'], $this->getTransportClient()); + $this->assertInstanceOf('\OpenStack\Storage\ObjectStorage', $ostore); + $this->assertTrue(strlen($ostore->token()) > 0); + } - $acl = \OpenStack\Storage\ObjectStorage\ACL::makePublic(); - $ret = $store->changeContainerACL($testCollection, $acl); + public function testFailedNewFromServiceCatalog() + { + $ident = $this->identity(); + $tok = $ident->token(); + $cat = $ident->serviceCatalog(); + $ostore = \OpenStack\Storage\ObjectStorage::newFromServiceCatalog($cat, $tok, 'region-w.geo-99999.fake', $this->getTransportClient()); + $this->assertEmpty($ostore); + } - $this->assertFalse($ret); + public function testNewFromIdnetity() + { + $ident = $this->identity(); + $ostore = \OpenStack\Storage\ObjectStorage::newFromIdentity($ident, self::$settings['openstack.swift.region'], $this->getTransportClient()); + $this->assertInstanceOf('\OpenStack\Storage\ObjectStorage', $ostore); + $this->assertTrue(strlen($ostore->token()) > 0); + } - $container = $store->container($testCollection); - $url = $container->url() . '?format=xml'; - $data = file_get_contents($url); - $this->assertNotEmpty($data, $url); + public function testNewFromIdentityAltRegion() + { + $ident = $this->identity(); + $ostore = \OpenStack\Storage\ObjectStorage::newFromIdentity($ident, 'region-b.geo-1', $this->getTransportClient()); + $this->assertInstanceOf('\OpenStack\Storage\ObjectStorage', $ostore); + $this->assertTrue(strlen($ostore->token()) > 0); - $store->deleteContainer($testCollection); - } + // Make sure the store is not the same as the default region. + $ostoreDefault = \OpenStack\Storage\ObjectStorage::newFromIdentity($ident, self::$settings['openstack.swift.region'], $this->getTransportClient()); + $this->assertNotEquals($ostore, $ostoreDefault); + } + + /** + * @group auth + * @ group acl + */ + public function testCreateContainer() + { + $testCollection = self::$settings['openstack.swift.container']; + + $this->assertNotEmpty($testCollection, "Canary: container name must be in settings file."); + + $store = $this->objectStore();//swiftAuth(); + + $this->destroyContainerFixture(); + /* + if ($store->hasContainer($testCollection)) { + $store->deleteContainer($testCollection); + } + */ + + $md = array('Foo' => 1234); + + $ret = $store->createContainer($testCollection, NULL, $md); + $this->assertTrue($ret, "Create container"); + + } + + /** + * @group auth + * @depends testCreateContainer + */ + public function testAccountInfo() + { + $store = $this->objectStore(); + + $info = $store->accountInfo(); + + $this->assertGreaterThan(0, $info['containers']); + $this->assertGreaterThanOrEqual(0, $info['bytes']); + $this->assertGreaterThanOrEqual(0, $info['objects']); + } + + /** + * @depends testCreateContainer + */ + public function testContainers() + { + $store = $this->objectStore(); + $containers = $store->containers(); + + $this->assertNotEmpty($containers); + + //$first = array_shift($containers); + + $testCollection = self::conf('openstack.swift.container'); + $testContainer = $containers[$testCollection]; + $this->assertEquals($testCollection, $testContainer->name()); + $this->assertEquals(0, $testContainer->bytes()); + $this->assertEquals(0, $testContainer->count()); + + // Make sure we get back an ACL: + $this->assertInstanceOf('\OpenStack\Storage\ObjectStorage\ACL', $testContainer->acl()); + + } + + /** + * @depends testCreateContainer + */ + public function testContainer() + { + $testCollection = self::$settings['openstack.swift.container']; + $store = $this->objectStore(); + + $container = $store->container($testCollection); + + $this->assertEquals(0, $container->bytes()); + $this->assertEquals(0, $container->count()); + $this->assertEquals($testCollection, $container->name()); + + $md = $container->metadata(); + $this->assertEquals(1, count($md)); + $this->assertEquals('1234', $md['Foo']); + } + + /** + * @depends testCreateContainer + */ + public function testHasContainer() + { + $testCollection = self::$settings['openstack.swift.container']; + $store = $this->objectStore(); + + $this->assertTrue($store->hasContainer($testCollection)); + $this->assertFalse($store->hasContainer('nihil')); + } + + /** + * @depends testHasContainer + */ + public function testDeleteContainer() + { + $testCollection = self::$settings['openstack.swift.container']; + + $store = $this->objectStore(); + //$ret = $store->createContainer($testCollection); + //$this->assertTrue($store->hasContainer($testCollection)); + + $ret = $store->deleteContainer($testCollection); + + $this->assertTrue($ret); + + // Now we try to delete a container that does not exist. + $ret = $store->deleteContainer('nihil'); + $this->assertFalse($ret); + } + + /** + * @expectedException \OpenStack\Storage\ObjectStorage\ContainerNotEmptyException + */ + public function testDeleteNonEmptyContainer() + { + $testCollection = self::$settings['openstack.swift.container']; + + $this->assertNotEmpty($testCollection); + + $store = $this->objectStore(); + $store->createContainer($testCollection); + + $container = $store->container($testCollection); + $container->save(new Object('test', 'test', 'text/plain')); + + try { + $ret = $store->deleteContainer($testCollection); + } catch (\Exception $e) { + $container->delete('test'); + $store->deleteContainer($testCollection); + throw $e; + } + + try { + $container->delete('test'); + } + // Skip 404s. + catch (\Exception $e) {} + + $store->deleteContainer($testCollection); + } + + /** + * @depends testCreateContainer + * @group acl + */ + public function testCreateContainerPublic() + { + $testCollection = self::$settings['openstack.swift.container'] . 'PUBLIC'; + $store = $this->objectStore(); + if ($store->hasContainer($testCollection)) { + $store->deleteContainer($testCollection); + } + + $ret = $store->createContainer($testCollection, ACL::makePublic()); + $container = $store->container($testCollection); + + // Now test that we can get the container contents. Since there is + // no content in the container, we use the format=xml to make sure + // we get some data back. + $url = $container->url() . '?format=xml'; + + // Use CURL to get better debugging: + //$client = \OpenStack\Transport::instance(); + //$response = $client->doRequest($url, 'GET'); + + $data = file_get_contents($url); + $this->assertNotEmpty($data, $url); + + $containers = $store->containers(); + //throw new \Exception(print_r($containers, TRUE)); + + $store->deleteContainer($testCollection); + } + + /** + * @depends testCreateContainerPublic + */ + public function testChangeContainerACL() + { + $testCollection = self::$settings['openstack.swift.container'] . 'PUBLIC'; + $store = $this->objectStore(); + if ($store->hasContainer($testCollection)) { + $store->deleteContainer($testCollection); + } + $ret = $store->createContainer($testCollection); + + $acl = \OpenStack\Storage\ObjectStorage\ACL::makePublic(); + $ret = $store->changeContainerACL($testCollection, $acl); + + $this->assertFalse($ret); + + $container = $store->container($testCollection); + $url = $container->url() . '?format=xml'; + $data = file_get_contents($url); + $this->assertNotEmpty($data, $url); + + $store->deleteContainer($testCollection); + } } diff --git a/test/Tests/ObjectTest.php b/test/Tests/ObjectTest.php index 2e1450e..883d6c8 100644 --- a/test/Tests/ObjectTest.php +++ b/test/Tests/ObjectTest.php @@ -2,17 +2,17 @@ /* ============================================================================ (c) Copyright 2012-2014 Hewlett-Packard Development Company, L.P. - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at - http://www.apache.org/licenses/LICENSE-2.0 + http://www.apache.org/licenses/LICENSE-2.0 - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. ============================================================================ */ /** * Unit tests for ObjectStorage Object. @@ -23,123 +23,130 @@ require_once 'test/TestCase.php'; use \OpenStack\Storage\ObjectStorage\Object; -class ObjectTest extends \OpenStack\Tests\TestCase { +class ObjectTest extends \OpenStack\Tests\TestCase +{ + const FNAME = 'descartes.txt'; + const FCONTENT = 'Cogito ergo sum.'; + const FTYPE = 'text/plain; charset=ISO-8859-1'; - const FNAME = 'descartes.txt'; - const FCONTENT = 'Cogito ergo sum.'; - const FTYPE = 'text/plain; charset=ISO-8859-1'; + /** + * Set up a basic object fixture. + * + * This provides an Object initialized with the main constants defined + * for this class. Use this as a fixture to avoid repetition. + * + * @return Object An initialized object. + */ + public function basicObjectFixture() + { + $o = new Object(self::FNAME); + $o->setContent(self::FCONTENT, self::FTYPE); - /** - * Set up a basic object fixture. - * - * This provides an Object initialized with the main constants defined - * for this class. Use this as a fixture to avoid repetition. - * - * @return Object An initialized object. - */ - public function basicObjectFixture() { + return $o; + } - $o = new Object(self::FNAME); - $o->setContent(self::FCONTENT, self::FTYPE); + public function testConstructor() + { + $o = $this->basicObjectFixture(); - return $o; - } + $this->assertEquals(self::FNAME, $o->name()); - public function testConstructor() { - $o = $this->basicObjectFixture(); + $o = new Object('a', 'b', 'text/plain'); - $this->assertEquals(self::FNAME, $o->name()); + $this->assertEquals('a', $o->name()); + $this->assertEquals('b', $o->content()); + $this->assertEquals('text/plain', $o->contentType()); + } - $o = new Object('a', 'b', 'text/plain'); + public function testContentType() + { + // Don't use the fixture, we want to test content + // type in its raw state. + $o = new Object('foo.txt'); - $this->assertEquals('a', $o->name()); - $this->assertEquals('b', $o->content()); - $this->assertEquals('text/plain', $o->contentType()); - } + $this->assertEquals('application/octet-stream', $o->contentType()); - public function testContentType() { - // Don't use the fixture, we want to test content - // type in its raw state. - $o = new Object('foo.txt'); + $o->setContentType('text/plain; charset=UTF-8'); + $this->assertEquals('text/plain; charset=UTF-8', $o->contentType()); + } - $this->assertEquals('application/octet-stream', $o->contentType()); + public function testContent() + { + $o = $this->basicObjectFixture(); - $o->setContentType('text/plain; charset=UTF-8'); - $this->assertEquals('text/plain; charset=UTF-8', $o->contentType()); - } + $this->assertEquals(self::FCONTENT, $o->content()); - public function testContent() { - $o = $this->basicObjectFixture(); + // Test binary data. + $bin = sha1(self::FCONTENT, TRUE); + $o->setContent($bin, 'application/octet-stream'); - $this->assertEquals(self::FCONTENT, $o->content()); + $this->assertEquals($bin, $o->content()); + } - // Test binary data. - $bin = sha1(self::FCONTENT, TRUE); - $o->setContent($bin, 'application/octet-stream'); + public function testEtag() + { + $o = $this->basicObjectFixture(); + $md5 = md5(self::FCONTENT); - $this->assertEquals($bin, $o->content()); - } + $this->assertEquals($md5, $o->eTag()); + } - public function testEtag() { - $o = $this->basicObjectFixture(); - $md5 = md5(self::FCONTENT); + public function testIsChunked() + { + $o = $this->basicObjectFixture(); + $this->assertFalse($o->isChunked()); + } - $this->assertEquals($md5, $o->eTag()); - } + public function testContentLength() + { + $o = $this->basicObjectFixture(); + $this->assertEquals(strlen(self::FCONTENT), $o->contentLength()); - public function testIsChunked() { - $o = $this->basicObjectFixture(); - $this->assertFalse($o->isChunked()); - } + // Test on binary data. + $bin = sha1(self::FCONTENT, TRUE); - public function testContentLength() { - $o = $this->basicObjectFixture(); - $this->assertEquals(strlen(self::FCONTENT), $o->contentLength()); + $o->setContent($bin); + $this->assertFalse($o->contentLength() == 0); + $this->assertEquals(strlen($bin), $o->contentLength()); + } - // Test on binary data. - $bin = sha1(self::FCONTENT, TRUE); + public function testMetadata() + { + $md = array( + 'Immanuel' => 'Kant', + 'David' => 'Hume', + 'Gottfried' => 'Leibniz', + 'Jean-Jaques' => 'Rousseau', + ); - $o->setContent($bin); - $this->assertFalse($o->contentLength() == 0); - $this->assertEquals(strlen($bin), $o->contentLength()); - } + $o = $this->basicObjectFixture(); + $o->setMetadata($md); - public function testMetadata() { - $md = array( - 'Immanuel' => 'Kant', - 'David' => 'Hume', - 'Gottfried' => 'Leibniz', - 'Jean-Jaques' => 'Rousseau', - ); + $got = $o->metadata(); - $o = $this->basicObjectFixture(); - $o->setMetadata($md); + $this->assertEquals(4, count($got)); + $this->assertArrayHasKey('Immanuel', $got); + $this->assertEquals('Leibniz', $got['Gottfried']); - $got = $o->metadata(); + } - $this->assertEquals(4, count($got)); - $this->assertArrayHasKey('Immanuel', $got); - $this->assertEquals('Leibniz', $got['Gottfried']); + public function testAdditionalHeaders() + { + $o = $this->basicObjectFixture(); - } + $extra = array( + 'a' => 'b', + 'aaa' => 'bbb', + 'ccc' => 'bbb', + ); + $o->setAdditionalHeaders($extra); - public function testAdditionalHeaders() { - $o = $this->basicObjectFixture(); + $got = $o->additionalHeaders(); + $this->assertEquals(3, count($got)); - $extra = array( - 'a' => 'b', - 'aaa' => 'bbb', - 'ccc' => 'bbb', - ); - $o->setAdditionalHeaders($extra); + $o->removeHeaders(array('ccc')); - $got = $o->additionalHeaders(); - $this->assertEquals(3, count($got)); - - $o->removeHeaders(array('ccc')); - - - $got = $o->additionalHeaders(); - $this->assertEquals(2, count($got)); - } + $got = $o->additionalHeaders(); + $this->assertEquals(2, count($got)); + } } diff --git a/test/Tests/RemoteObjectTest.php b/test/Tests/RemoteObjectTest.php index bc9c811..0b4d87f 100644 --- a/test/Tests/RemoteObjectTest.php +++ b/test/Tests/RemoteObjectTest.php @@ -2,17 +2,17 @@ /* ============================================================================ (c) Copyright 2012-2014 Hewlett-Packard Development Company, L.P. - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at - http://www.apache.org/licenses/LICENSE-2.0 + http://www.apache.org/licenses/LICENSE-2.0 - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. ============================================================================ */ /** * Unit tests for ObjectStorage RemoteObject. @@ -25,291 +25,308 @@ use \OpenStack\Storage\ObjectStorage\RemoteObject; use \OpenStack\Storage\ObjectStorage\Object; use \OpenStack\Storage\ObjectStorage\Container; -class RemoteObjectTest extends \OpenStack\Tests\TestCase { - - const FNAME = 'RemoteObjectTest'; - //const FTYPE = 'text/plain; charset=UTF-8'; - const FTYPE = 'application/octet-stream; charset=UTF-8'; - const FCONTENT = 'Rah rah ah ah ah. Roma roma ma. Gaga oh la la.'; - const FMETA_NAME = 'Foo'; - const FMETA_VALUE = 'Bar'; - const FDISPOSITION = 'attachment; roma.gaga'; - const FENCODING = 'gzip'; - const FCORS_NAME = 'Access-Control-Max-Age'; - const FCORS_VALUE = '2000'; - - protected function createAnObject() { - $container = $this->containerFixture(); - - $object = new Object(self::FNAME, self::FCONTENT, self::FTYPE); - $object->setMetadata(array(self::FMETA_NAME => self::FMETA_VALUE)); - $object->setDisposition(self::FDISPOSITION); - $object->setEncoding(self::FENCODING); - $object->setAdditionalHeaders(array( - 'Access-Control-Allow-Origin' => 'http://example.com', - 'Access-control-allow-origin' => 'http://example.com', - )); - - // Need some headers that Swift actually stores and returns. This - // one does not seem to be returned ever. - //$object->setAdditionalHeaders(array(self::FCORS_NAME => self::FCORS_VALUE)); - - $container->save($object); - } - - public function testNewFromHeaders() { - // This is tested via the container. - - $this->destroyContainerFixture(); - $container = $this->containerFixture(); - $this->createAnObject(); - - $obj = $container->remoteObject(self::FNAME); - - $this->assertInstanceOf('\OpenStack\Storage\ObjectStorage\RemoteObject', $obj); - - return $obj; - } - /** - * @depends testNewFromHeaders - */ - public function testContentLength($obj) { - $len = strlen(self::FCONTENT); - - $this->assertEquals($len, $obj->contentLength()); - - return $obj; - } - - /** - * @depends testContentLength - */ - public function testContentType($obj) { - $this->assertEquals(self::FTYPE, $obj->contentType()); +class RemoteObjectTest extends \OpenStack\Tests\TestCase +{ + const FNAME = 'RemoteObjectTest'; + //const FTYPE = 'text/plain; charset=UTF-8'; + const FTYPE = 'application/octet-stream; charset=UTF-8'; + const FCONTENT = 'Rah rah ah ah ah. Roma roma ma. Gaga oh la la.'; + const FMETA_NAME = 'Foo'; + const FMETA_VALUE = 'Bar'; + const FDISPOSITION = 'attachment; roma.gaga'; + const FENCODING = 'gzip'; + const FCORS_NAME = 'Access-Control-Max-Age'; + const FCORS_VALUE = '2000'; + + protected function createAnObject() + { + $container = $this->containerFixture(); + + $object = new Object(self::FNAME, self::FCONTENT, self::FTYPE); + $object->setMetadata(array(self::FMETA_NAME => self::FMETA_VALUE)); + $object->setDisposition(self::FDISPOSITION); + $object->setEncoding(self::FENCODING); + $object->setAdditionalHeaders(array( + 'Access-Control-Allow-Origin' => 'http://example.com', + 'Access-control-allow-origin' => 'http://example.com', + )); + + // Need some headers that Swift actually stores and returns. This + // one does not seem to be returned ever. + //$object->setAdditionalHeaders(array(self::FCORS_NAME => self::FCORS_VALUE)); + + $container->save($object); + } + + public function testNewFromHeaders() + { + // This is tested via the container. + + $this->destroyContainerFixture(); + $container = $this->containerFixture(); + $this->createAnObject(); + + $obj = $container->remoteObject(self::FNAME); + + $this->assertInstanceOf('\OpenStack\Storage\ObjectStorage\RemoteObject', $obj); + + return $obj; + } + /** + * @depends testNewFromHeaders + */ + public function testContentLength($obj) + { + $len = strlen(self::FCONTENT); + + $this->assertEquals($len, $obj->contentLength()); + + return $obj; + } + + /** + * @depends testContentLength + */ + public function testContentType($obj) + { + $this->assertEquals(self::FTYPE, $obj->contentType()); + + return $obj; + } + + /** + * @depends testContentType + */ + public function testEtag($obj) + { + $hash = md5(self::FCONTENT); + + $this->assertEquals($hash, $obj->eTag()); + + return $obj; + } + + /** + * @depends testContentType + */ + public function testLastModified($obj) + { + $date = $obj->lastModified(); + + $this->assertTrue(is_int($date)); + $this->assertTrue($date > 0); + } + + /** + * @depends testNewFromHeaders + */ + public function testMetadata($obj) + { + $md = $obj->metadata(); + + $this->assertArrayHasKey(self::FMETA_NAME, $md); + $this->assertEquals(self::FMETA_VALUE, $md[self::FMETA_NAME]); + } + + /** + * @depends testNewFromHeaders + */ + public function testDisposition($obj) + { + $this->assertEquals(self::FDISPOSITION, $obj->disposition()); + } - return $obj; - } + /** + * @depends testNewFromHeaders + */ + public function testEncoding($obj) + { + $this->assertEquals(self::FENCODING, $obj->encoding()); + } - /** - * @depends testContentType - */ - public function testEtag($obj) { - $hash = md5(self::FCONTENT); + /** + * @depends testNewFromHeaders + */ + public function testHeaders($obj) + { + $headers = $obj->headers(); + $this->assertTrue(count($headers) > 1); - $this->assertEquals($hash, $obj->eTag()); + //fwrite(STDOUT, print_r($headers, TRUE)); - return $obj; - } + $this->assertNotEmpty($headers['Date']); - /** - * @depends testContentType - */ - public function testLastModified($obj) { - $date = $obj->lastModified(); + $obj->removeHeaders(array('Date')); - $this->assertTrue(is_int($date)); - $this->assertTrue($date > 0); - } + $headers = $obj->headers(); + $this->assertFalse(isset($headers['Date'])); - /** - * @depends testNewFromHeaders - */ - public function testMetadata($obj) { - $md = $obj->metadata(); + // Swift doesn't return CORS headers even though it is supposed to. + //$this->assertEquals(self::FCORS_VALUE, $headers[self::FCORS_NAME]); + } - $this->assertArrayHasKey(self::FMETA_NAME, $md); - $this->assertEquals(self::FMETA_VALUE, $md[self::FMETA_NAME]); - } + /** + * @depends testNewFromHeaders + */ + public function testUrl($obj) + { + $url = $obj->url(); - /** - * @depends testNewFromHeaders - */ - public function testDisposition($obj) { - $this->assertEquals(self::FDISPOSITION, $obj->disposition()); - } + $this->assertTrue(strpos($obj->url(), $obj->name())> 0); + } + /** + * @depends testNewFromHeaders + */ + public function testStream($obj) + { + $res = $obj->stream(); - /** - * @depends testNewFromHeaders - */ - public function testEncoding($obj) { - $this->assertEquals(self::FENCODING, $obj->encoding()); - } + $this->assertTrue(is_resource($res)); - /** - * @depends testNewFromHeaders - */ - public function testHeaders($obj) { - $headers = $obj->headers(); - $this->assertTrue(count($headers) > 1); + $res_md = stream_get_meta_data($res); + + $content = fread($res, $obj->contentLength()); + + fclose($res); - //fwrite(STDOUT, print_r($headers, TRUE)); + $this->assertEquals(self::FCONTENT, $content); - $this->assertNotEmpty($headers['Date']); + // Now repeat the tests, only with a local copy of the data. + // This allows us to test the local tempfile buffering. - $obj->removeHeaders(array('Date')); + $obj->setContent($content); - $headers = $obj->headers(); - $this->assertFalse(isset($headers['Date'])); + $res2 = $obj->stream(); + $res_md = stream_get_meta_data($res2); - // Swift doesn't return CORS headers even though it is supposed to. - //$this->assertEquals(self::FCORS_VALUE, $headers[self::FCORS_NAME]); - } + $this->assertEquals('PHP', $res_md['wrapper_type']); - /** - * @depends testNewFromHeaders - */ - public function testUrl($obj) { - $url = $obj->url(); + $content = fread($res2, $obj->contentLength()); - $this->assertTrue(strpos($obj->url(), $obj->name())> 0); - } - /** - * @depends testNewFromHeaders - */ - public function testStream($obj) { - $res = $obj->stream(); + fclose($res2); - $this->assertTrue(is_resource($res)); + $this->assertEquals(self::FCONTENT, $content); - $res_md = stream_get_meta_data($res); + // Finally, we redo the first part of the test to make sure that + // refreshing gets us a new copy: - $content = fread($res, $obj->contentLength()); + $res3 = $obj->stream(TRUE); + $res_md = stream_get_meta_data($res3); + $this->assertEquals('PHP', $res_md['wrapper_type']); + fclose($res3); - fclose($res); + return $obj; + } - $this->assertEquals(self::FCONTENT, $content); + // To avoid test tainting from testStream(), we start over. + public function testContent() + { + $container = $this->containerFixture(); + $obj = $container->object(self::FNAME); - // Now repeat the tests, only with a local copy of the data. - // This allows us to test the local tempfile buffering. + $content = $obj->content(); + $this->assertEquals(self::FCONTENT, $content); - $obj->setContent($content); + // Make sure remoteObject retrieves the same content. + $obj = $container->remoteObject(self::FNAME); + $content = $obj->content(); + $this->assertEquals(self::FCONTENT, $content); - $res2 = $obj->stream(); - $res_md = stream_get_meta_data($res2); + } - $this->assertEquals('PHP', $res_md['wrapper_type']); + /** + * @depends testStream + */ + public function testCaching() + { + $container = $this->containerFixture(); + $obj = $container->remoteObject(self::FNAME); - $content = fread($res2, $obj->contentLength()); + $this->assertFalse($obj->isCaching()); - fclose($res2); + $content = $obj->content(); - $this->assertEquals(self::FCONTENT, $content); + $res1 = $obj->stream(); + $md = stream_get_meta_data($res1); + $this->assertEquals('PHP', $md['wrapper_type']); - // Finally, we redo the first part of the test to make sure that - // refreshing gets us a new copy: + fclose($res1); - $res3 = $obj->stream(TRUE); - $res_md = stream_get_meta_data($res3); - $this->assertEquals('PHP', $res_md['wrapper_type']); - fclose($res3); + // Enable caching and retest. + $obj->setCaching(TRUE); + $this->assertTrue($obj->isCaching()); - return $obj; - } + // This will cache the content. + $content = $obj->content(); - // To avoid test tainting from testStream(), we start over. - public function testContent() { - $container = $this->containerFixture(); - $obj = $container->object(self::FNAME); + $res2 = $obj->stream(); + $md = stream_get_meta_data($res2); - $content = $obj->content(); - $this->assertEquals(self::FCONTENT, $content); + // If this is using the PHP version, it built content from the + // cached version. + $this->assertEquals('PHP', $md['wrapper_type']); - // Make sure remoteObject retrieves the same content. - $obj = $container->remoteObject(self::FNAME); - $content = $obj->content(); - $this->assertEquals(self::FCONTENT, $content); + fclose($res2); + } - } + /** + * @depends testNewFromHeaders + */ + public function testContentVerification($obj) + { + $this->assertTrue($obj->isVerifyingContent()); + $obj->setContentVerification(FALSE); + $this->assertFALSE($obj->isVerifyingContent()); + $obj->setContentVerification(TRUE); + } - /** - * @depends testStream - */ - public function testCaching() { - $container = $this->containerFixture(); - $obj = $container->remoteObject(self::FNAME); + /** + * @depends testCaching + */ + public function testIsDirty() + { + $container = $this->containerFixture(); + $obj = $container->remoteObject(self::FNAME); - $this->assertFalse($obj->isCaching()); + // THere is no content. Assert false. + $this->assertFalse($obj->isDirty()); - $content = $obj->content(); + $obj->setCaching(TRUE); + $obj->content(); - $res1 = $obj->stream(); - $md = stream_get_meta_data($res1); - $this->assertEquals('PHP', $md['wrapper_type']); + // THere is content, but it is unchanged. + $this->assertFalse($obj->isDirty()); - fclose($res1); + // Change content and retest. + $obj->setContent('foo'); - // Enable caching and retest. - $obj->setCaching(TRUE); - $this->assertTrue($obj->isCaching()); + $this->assertTrue($obj->isDirty()); + } - // This will cache the content. - $content = $obj->content(); + /** + * @depends testIsDirty + */ + public function testRefresh() + { + $container = $this->containerFixture(); + $obj = $container->remoteObject(self::FNAME); - $res2 = $obj->stream(); - $md = stream_get_meta_data($res2); + $obj->setContent('foo'); + $this->assertTrue($obj->isDirty()); - // If this is using the PHP version, it built content from the - // cached version. - $this->assertEquals('PHP', $md['wrapper_type']); + $obj->refresh(FALSE); + $this->assertFalse($obj->isDirty()); + $this->assertEquals(self::FCONTENT, $obj->content()); - fclose($res2); - } + $obj->setContent('foo'); + $this->assertTrue($obj->isDirty()); - /** - * @depends testNewFromHeaders - */ - public function testContentVerification($obj) { - $this->assertTrue($obj->isVerifyingContent()); - $obj->setContentVerification(FALSE); - $this->assertFALSE($obj->isVerifyingContent()); - $obj->setContentVerification(TRUE); - } + $obj->refresh(TRUE); + $this->assertFalse($obj->isDirty()); + $this->assertEquals(self::FCONTENT, $obj->content()); - /** - * @depends testCaching - */ - public function testIsDirty() { - $container = $this->containerFixture(); - $obj = $container->remoteObject(self::FNAME); + $this->destroyContainerFixture(); - // THere is no content. Assert false. - $this->assertFalse($obj->isDirty()); - - $obj->setCaching(TRUE); - $obj->content(); - - // THere is content, but it is unchanged. - $this->assertFalse($obj->isDirty()); - - // Change content and retest. - $obj->setContent('foo'); - - $this->assertTrue($obj->isDirty()); - } - - /** - * @depends testIsDirty - */ - public function testRefresh() { - $container = $this->containerFixture(); - $obj = $container->remoteObject(self::FNAME); - - $obj->setContent('foo'); - $this->assertTrue($obj->isDirty()); - - $obj->refresh(FALSE); - $this->assertFalse($obj->isDirty()); - $this->assertEquals(self::FCONTENT, $obj->content()); - - $obj->setContent('foo'); - $this->assertTrue($obj->isDirty()); - - $obj->refresh(TRUE); - $this->assertFalse($obj->isDirty()); - $this->assertEquals(self::FCONTENT, $obj->content()); - - $this->destroyContainerFixture(); - - } + } } diff --git a/test/Tests/StreamWrapperFSTest.php b/test/Tests/StreamWrapperFSTest.php index 2a52168..241bb55 100644 --- a/test/Tests/StreamWrapperFSTest.php +++ b/test/Tests/StreamWrapperFSTest.php @@ -2,17 +2,17 @@ /* ============================================================================ (c) Copyright 2012-2014 Hewlett-Packard Development Company, L.P. - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at - http://www.apache.org/licenses/LICENSE-2.0 + http://www.apache.org/licenses/LICENSE-2.0 - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. ============================================================================ */ /** * Unit tests for the stream wrapper file systema. @@ -24,621 +24,643 @@ require_once 'test/TestCase.php'; use \OpenStack\Storage\ObjectStorage\StreamWrapperFS; use \OpenStack\Storage\ObjectStorage\Container; use \OpenStack\Storage\ObjectStorage\Object; -use \OpenStack\Storage\ObjectStorage\ACL; /** * @group streamWrapper */ -class StreamWrapperFSTest extends \OpenStack\Tests\TestCase { +class StreamWrapperFSTest extends \OpenStack\Tests\TestCase +{ + const FNAME = 'streamTest.txt'; + const FTYPE = 'application/x-tuna-fish; charset=iso-8859-13'; - const FNAME = 'streamTest.txt'; - const FTYPE = 'application/x-tuna-fish; charset=iso-8859-13'; + /*public static function setUpBeforeClass() { + }*/ - /*public static function setUpBeforeClass() { - }*/ + /** + * Cleaning up the test container so we can reuse it for other tests. + */ + public static function tearDownAfterClass() + { + // First we get an identity + $user = self::conf('openstack.identity.username'); + $pass = self::conf('openstack.identity.password'); + $tenantId = self::conf('openstack.identity.tenantId'); + $url = self::conf('openstack.identity.url'); - /** - * Cleaning up the test container so we can reuse it for other tests. - */ - public static function tearDownAfterClass() { + $ident = new \OpenStack\Services\IdentityService($url, self::getTransportClient()); - // First we get an identity - $user = self::conf('openstack.identity.username'); - $pass = self::conf('openstack.identity.password'); - $tenantId = self::conf('openstack.identity.tenantId'); - $url = self::conf('openstack.identity.url'); + $token = $ident->authenticateAsUser($user, $pass, $tenantId); - $ident = new \OpenStack\Services\IdentityService($url, self::getTransportClient()); + // Then we need to get an instance of storage + $store = \OpenStack\Storage\ObjectStorage::newFromIdentity($ident, self::conf('openstack.swift.region'), self::getTransportClient()); - $token = $ident->authenticateAsUser($user, $pass, $tenantId); + // Delete the container and all the contents. + $cname = self::$settings['openstack.swift.container']; - // Then we need to get an instance of storage - $store = \OpenStack\Storage\ObjectStorage::newFromIdentity($ident, self::conf('openstack.swift.region'), self::getTransportClient()); + try { + $container = $store->container($cname); + } + // The container was never created. + catch (\OpenStack\Transport\FileNotFoundException $e) { + return; + } + foreach ($container as $object) { + try { + $container->delete($object->name()); + } catch (\Exception $e) {} + } - // Delete the container and all the contents. - $cname = self::$settings['openstack.swift.container']; - - try { - $container = $store->container($cname); - } - // The container was never created. - catch (\OpenStack\Transport\FileNotFoundException $e) { - return; + $store->deleteContainer($cname); } - foreach ($container as $object) { - try { - $container->delete($object->name()); - } - catch (\Exception $e) {} + protected function newUrl($objectName) + { + $scheme = StreamWrapperFS::DEFAULT_SCHEME; + $cname = self::$settings['openstack.swift.container']; + $cname = urlencode($cname); + + $objectParts = explode('/', $objectName); + for ($i = 0; $i < count($objectParts); ++$i) { + $objectParts[$i] = urlencode($objectParts[$i]); + } + $objectName = implode('/', $objectParts); + + $url = $scheme . '://' . $cname . '/' . $objectName; + + return $url; } - $store->deleteContainer($cname); - } + /** + * This assumes auth has already been done. + */ + protected function basicSwiftContext($add = array(), $scheme = NULL) + { + $cname = self::$settings['openstack.swift.container']; - protected function newUrl($objectName) { - $scheme = StreamWrapperFS::DEFAULT_SCHEME; - $cname = self::$settings['openstack.swift.container']; - $cname = urlencode($cname); + if (empty($scheme)) { + $scheme = StreamWrapperFS::DEFAULT_SCHEME; + } - $objectParts = explode('/', $objectName); - for ($i = 0; $i < count($objectParts); ++$i) { - $objectParts[$i] = urlencode($objectParts[$i]); - } - $objectName = implode('/', $objectParts); + if (empty(self::$ostore)) { + throw new \Exception('OStore is gone.'); + } - $url = $scheme . '://' . $cname . '/' . $objectName; + $params = $add + array( + 'token' => $this->objectStore()->token(), + 'swift_endpoint' => $this->objectStore()->url(), + 'content_type' => self::FTYPE, + 'transport_client' => $this->getTransportClient(), + ); + $cxt = array($scheme => $params); - return $url; - } - - /** - * This assumes auth has already been done. - */ - protected function basicSwiftContext($add = array(), $scheme = NULL) { - $cname = self::$settings['openstack.swift.container']; - - if (empty($scheme)) { - $scheme = StreamWrapperFS::DEFAULT_SCHEME; + return stream_context_create($cxt); } - if (empty(self::$ostore)) { - throw new \Exception('OStore is gone.'); + /** + * This performs authentication via context. + * + * UPDATE: This now users IdentityService instead of deprecated + * swauth. + */ + protected function authSwiftContext($add = array(), $scheme = NULL) + { + $cname = self::$settings['openstack.swift.container']; + $username = self::$settings['openstack.identity.username']; + $password = self::$settings['openstack.identity.password']; + $tenant = self::$settings['openstack.identity.tenantId']; + $baseURL = self::$settings['openstack.identity.url']; + + if (empty($scheme)) { + $scheme = StreamWrapperFS::DEFAULT_SCHEME; + } + + $params = $add + array( + 'username' => $username, + 'password' => $password, + 'endpoint' => $baseURL, + 'tenantid' => $tenant, + 'content_type' => self::FTYPE, + 'transport_client' => $this->getTransportClient(), + ); + $cxt = array($scheme => $params); + + return stream_context_create($cxt); + } - $params = $add + array( - 'token' => $this->objectStore()->token(), - 'swift_endpoint' => $this->objectStore()->url(), - 'content_type' => self::FTYPE, - 'transport_client' => $this->getTransportClient(), - ); - $cxt = array($scheme => $params); + /** + * Add additional params to the config. + * + * This allows us to insert credentials into the + * bootstrap config, which in turn allows us to run + * high-level context-less functions like + * file_get_contents(), stat(), and is_file(). + */ + protected function addBootstrapConfig() + { + $opts = array( + 'username' => self::$settings['openstack.identity.username'], + 'password' => self::$settings['openstack.identity.password'], + 'endpoint' => self::$settings['openstack.identity.url'], + 'tenantid' => self::$settings['openstack.identity.tenantId'], + 'token' => $this->objectStore()->token(), + 'swift_endpoint' => $this->objectStore()->url(), + ); + \OpenStack\Bootstrap::setConfiguration($opts); - return stream_context_create($cxt); - } - - /** - * This performs authentication via context. - * - * UPDATE: This now users IdentityService instead of deprecated - * swauth. - */ - protected function authSwiftContext($add = array(), $scheme = NULL) { - $cname = self::$settings['openstack.swift.container']; - $username = self::$settings['openstack.identity.username']; - $password = self::$settings['openstack.identity.password']; - $tenant = self::$settings['openstack.identity.tenantId']; - $baseURL = self::$settings['openstack.identity.url']; - - if (empty($scheme)) { - $scheme = StreamWrapperFS::DEFAULT_SCHEME; } - $params = $add + array( - 'username' => $username, - 'password' => $password, - 'endpoint' => $baseURL, - 'tenantid' => $tenant, - 'content_type' => self::FTYPE, - 'transport_client' => $this->getTransportClient(), - ); - $cxt = array($scheme => $params); + // Canary. There are UTF-8 encoding issues in stream wrappers. + public function testStreamContext() + { + // Clear old values. + \OpenStack\Bootstrap::setConfiguration(array( + 'token' => NULL, + )); - return stream_context_create($cxt); + $cxt = $this->authSwiftContext(); + $array = stream_context_get_options($cxt); - } + $opts = $array['swiftfs']; + $endpoint = self::conf('openstack.identity.url'); - - /** - * Add additional params to the config. - * - * This allows us to insert credentials into the - * bootstrap config, which in turn allows us to run - * high-level context-less functions like - * file_get_contents(), stat(), and is_file(). - */ - protected function addBootstrapConfig() { - $opts = array( - 'username' => self::$settings['openstack.identity.username'], - 'password' => self::$settings['openstack.identity.password'], - 'endpoint' => self::$settings['openstack.identity.url'], - 'tenantid' => self::$settings['openstack.identity.tenantId'], - 'token' => $this->objectStore()->token(), - 'swift_endpoint' => $this->objectStore()->url(), - ); - \OpenStack\Bootstrap::setConfiguration($opts); - - } - - // Canary. There are UTF-8 encoding issues in stream wrappers. - public function testStreamContext() { - // Clear old values. - \OpenStack\Bootstrap::setConfiguration(array( - 'token' => NULL, - )); - - $cxt = $this->authSwiftContext(); - $array = stream_context_get_options($cxt); - - $opts = $array['swiftfs']; - $endpoint = self::conf('openstack.identity.url'); - - $this->assertEquals($endpoint, $opts['endpoint'], 'A UTF-8 encoding issue.'); - } - - /** - * @depends testStreamContext - */ - public function testRegister() { - // Canary - $this->assertNotEmpty(StreamWrapperFS::DEFAULT_SCHEME); - - $klass = '\OpenStack\Storage\ObjectStorage\StreamWrapperFS'; - stream_wrapper_register(StreamWrapperFS::DEFAULT_SCHEME, $klass); - - $wrappers = stream_get_wrappers(); - - $this->assertContains(StreamWrapperFS::DEFAULT_SCHEME, $wrappers); - } - - /** - * @depends testRegister - */ - public function testOpenFailureWithoutContext() { - $url = $this->newUrl('foo→/bar.txt'); - $ret = @fopen($url, 'r'); - - $this->assertFalse($ret); - } - - /** - * @depends testRegister - */ - public function testOpen() { - $cname = self::$settings['openstack.swift.container']; - - // Create a fresh container. - $this->eradicateContainer($cname); - $this->containerFixture(); - - // Simple write test. - $oUrl = $this->newUrl('foo→/test.csv'); - - $res = fopen($oUrl, 'nope', FALSE, $this->authSwiftContext()); - - $this->assertTrue(is_resource($res)); - - $md = stream_get_meta_data($res); - $wrapper = $md['wrapper_data']; - - fclose($res); - - // Now we test the same, but re-using the auth token: - $cxt = $this->basicSwiftContext(array('token' => $wrapper->token())); - - $res = fopen($oUrl, 'nope', FALSE, $cxt); - - $this->assertTrue(is_resource($res)); - - fclose($res); - - } - - /** - * @depends testOpen - */ - public function testOpenFailureWithRead() { - $url = $this->newUrl(__FUNCTION__); - $res = @fopen($url, 'r', FALSE, $this->basicSwiftContext()); - - $this->assertFalse($res); - - } - - // DO we need to test other modes? - - /** - * @depends testOpen - */ - public function testOpenCreateMode() { - $url = $this->newUrl(self::FNAME); - $res = fopen($url, 'c+', FALSE, $this->basicSwiftContext()); - $this->assertTrue(is_resource($res)); - //fclose($res); - - return $res; - } - - /** - * @depends testOpenCreateMode - */ - public function testTell($res) { - // Sould be at the beginning of the buffer. - $this->assertEquals(0, ftell($res)); - - return $res; - } - - /** - * @depends testTell - */ - public function testWrite($res) { - $str = 'To be is to be the value of a bound variable. -- Quine'; - fwrite($res, $str); - $this->assertGreaterThan(0, ftell($res)); - - return $res; - } - - /** - * @depends testWrite - */ - public function testStat($res) { - $stat = fstat($res); - - $this->assertGreaterThan(0, $stat['size']); - - return $res; - } - - /** - * @depends testStat - */ - public function testSeek($res) { - $then = ftell($res); - rewind($res); - - $now = ftell($res); - - // $now should be 0 - $this->assertLessThan($then, $now); - $this->assertEquals(0, $now); - - fseek($res, 0, SEEK_END); - $final = ftell($res); - - $this->assertEquals($then, $final); - - return $res; - - } - - /** - * @depends testSeek - */ - public function testEof($res) { - rewind($res); - - $this->assertEquals(0, ftell($res)); - - $this->assertFalse(feof($res)); - - fseek($res, 0, SEEK_END); - $this->assertGreaterThan(0, ftell($res)); - - $read = fread($res, 8192); - - $this->assertEmpty($read); - - $this->assertTrue(feof($res)); - - return $res; - } - - /** - * @depends testEof - */ - public function testFlush($res) { - - $stat1 = fstat($res); - - fflush($res); - - // Grab a copy of the object. - $url = $this->newUrl(self::FNAME); - $newObj = fopen($url, 'r', FALSE, $this->basicSwiftContext()); - - $stat2 = fstat($newObj); - - $this->assertEquals($stat1['size'], $stat2['size']); - - return $res; - } - - /** - * @depends testFlush - */ - public function testStreamGetMetadata($res) { - // Grab a copy of the object. - $url = $this->newUrl(self::FNAME); - $newObj = fopen($url, 'r', FALSE, $this->basicSwiftContext()); - - $md = stream_get_meta_data($newObj); - //throw new \Exception(print_r($md, true)); - $obj = $md['wrapper_data']->object(); - - $this->assertInstanceOf('\OpenStack\Storage\ObjectStorage\RemoteObject', $obj); - - $this->assertEquals(self::FTYPE, $obj->contentType()); - - } - - /** - * @depends testFlush - */ - public function testClose($res) { - $this->assertTrue(is_resource($res)); - fwrite($res, '~~~~'); - //throw new \Exception(stream_get_contents($res)); - fflush($res); - - // This is occasionally generating seemingly - // spurious PHP errors about Bootstrap::$config. - fclose($res); - - $url = $this->newUrl(self::FNAME); - $res2 = fopen($url, 'r', FALSE, $this->basicSwiftContext()); - $this->assertTrue(is_resource($res2)); - - $contents = stream_get_contents($res2); - fclose($res2); - $this->assertRegExp('/~{4}$/', $contents); - - } - - /** - * @depends testClose - */ - public function testCast() { - $url = $this->newUrl(self::FNAME); - $res = fopen($url, 'r', FALSE, $this->basicSwiftContext()); - - $read = array($res); - $write = array(); - $except = array(); - $num_changed = stream_select($read, $write, $except, 0); - $this->assertGreaterThan(0, $num_changed); - } - - /** - * @depends testClose - */ - public function testUrlStat(){ - // Add context to the bootstrap config. - $this->addBootstrapConfig(); - - $url = $this->newUrl(self::FNAME); - - $ret = stat($url); - - // Check that the array looks right. - $this->assertEquals(26, count($ret)); - $this->assertEquals(0, $ret[3]); - $this->assertEquals($ret[2], $ret['mode']); - - $this->assertTrue(file_exists($url)); - $this->assertTrue(is_readable($url)); - $this->assertTrue(is_writeable($url)); - $this->assertFalse(is_link($url)); - $this->assertGreaterThan(0, filemtime($url)); - $this->assertGreaterThan(5, filesize($url)); - - $perm = fileperms($url); - - // Assert that this is a file. Objects are - // *always* marked as files. - $this->assertEquals(0x8000, $perm & 0x8000); - - // Assert writeable by owner. - $this->assertEquals(0x0080, $perm & 0x0080); - - // Assert not world writable. - $this->assertEquals(0, $perm & 0x0002); - - $contents = file_get_contents($url); - $this->assertGreaterThan(5, strlen($contents)); - - $fsCopy = '/tmp/hpcloud-copy-test.txt'; - copy($url, $fsCopy, $this->basicSwiftContext()); - $this->assertTrue(file_exists($fsCopy)); - unlink($fsCopy); - } - - /** - * @depends testFlush - */ - public function testUnlink(){ - $url = $this->newUrl(self::FNAME); - $cxt = $this->basicSwiftContext(); - - $ret = unlink($url, $cxt); - $this->assertTrue($ret); - - $ret2 = unlink($url, $cxt); - $this->assertFalse($ret2); - } - - public function testSetOption() { - $url = $this->newUrl('fake.foo'); - $fake = fopen($url, 'nope', FALSE, $this->basicSwiftContext()); - - $this->assertTrue(stream_set_blocking($fake, 1)); - - // Returns 0 on success. - $this->assertEquals(0, stream_set_write_buffer($fake, 8192)); - - // Cant set a timeout on a tmp storage: - $this->assertFalse(stream_set_timeout($fake, 10)); - - fclose($fake); - } - - /** - * @depends testUnlink - */ - public function testRename(){ - $url = $this->newUrl('rename.foo'); - $fake = fopen($url, 'w+', FALSE, $this->basicSwiftContext()); - fwrite($fake, 'test'); - fclose($fake); - - $this->assertTrue(file_exists($url)); - - $url2 = $this->newUrl('rename.txt'); - - rename($url, $url2, $this->basicSwiftContext()); - - $this->assertTrue(file_exists($url2)); - $this->assertFalse(file_exists($url)); - - unlink($url2, $this->basicSwiftContext()); - } - - /** - * @depends testUnlink - */ - public function testOpenDir() { - $urls = array('test1.txt', 'foo/test2.txt', 'foo/test3.txt', 'bar/test4.txt'); - foreach ($urls as $base) { - $url = $this->newUrl($base); - $f = fopen($url, 'c+', FALSE, $this->basicSwiftContext()); - fwrite($f, 'Test.'); - fclose($f); + $this->assertEquals($endpoint, $opts['endpoint'], 'A UTF-8 encoding issue.'); } - $dirUrl = $this->newUrl(''); - $dir = opendir($dirUrl, $this->basicSwiftContext()); + /** + * @depends testStreamContext + */ + public function testRegister() + { + // Canary + $this->assertNotEmpty(StreamWrapperFS::DEFAULT_SCHEME); - $this->assertTrue(is_resource($dir)); + $klass = '\OpenStack\Storage\ObjectStorage\StreamWrapperFS'; + stream_wrapper_register(StreamWrapperFS::DEFAULT_SCHEME, $klass); - return $dir; + $wrappers = stream_get_wrappers(); - } - - /** - * @depends testOpenDir - */ - public function testReaddir($dir){ - // Order should be newest to oldest. - $expects = array('bar/', 'foo/', 'test1.txt'); - - $buffer = array(); - while (($entry = readdir($dir)) !== FALSE) { - $should_be = array_shift($expects); - $this->assertEquals($should_be, $entry); + $this->assertContains(StreamWrapperFS::DEFAULT_SCHEME, $wrappers); } - $this->assertFalse(readdir($dir)); - return $dir; - } - /** - * @depends testReaddir - */ - public function testRewindDir($dir){ - $this->assertFalse(readdir($dir)); - rewinddir($dir); - $this->assertEquals('bar/', readdir($dir)); - return $dir; - } + /** + * @depends testRegister + */ + public function testOpenFailureWithoutContext() + { + $url = $this->newUrl('foo→/bar.txt'); + $ret = @fopen($url, 'r'); - /** - * @depends testRewindDir - */ - public function testCloseDir($dir) { - $this->assertTrue(is_resource($dir)); - closedir($dir); + $this->assertFalse($ret); + } - // There is a bug in PHP where a - // resource buffer is not getting cleared. - // So this might return a value even though - // the underlying stream is cleared. - //$this->assertFalse(readdir($dir)); - } + /** + * @depends testRegister + */ + public function testOpen() + { + $cname = self::$settings['openstack.swift.container']; - /** - * @depends testCloseDir - */ - public function testOpenSubdir() { + // Create a fresh container. + $this->eradicateContainer($cname); + $this->containerFixture(); - // Opening foo we should find test2.txt and test3.txt. - $url = $this->newUrl('foo/'); - $dir = opendir($url, $this->basicSwiftContext()); + // Simple write test. + $oUrl = $this->newUrl('foo→/test.csv'); - // I don't know why, but these are always returned in - // lexical order. - $this->assertEquals('test2.txt', readdir($dir)); - $this->assertEquals('test3.txt', readdir($dir)); + $res = fopen($oUrl, 'nope', FALSE, $this->authSwiftContext()); - $array = scandir($url, -1, $this->basicSwiftContext()); - $this->assertEquals(2, count($array)); - $this->assertEquals('test3.txt', $array[0]); + $this->assertTrue(is_resource($res)); - } + $md = stream_get_meta_data($res); + $wrapper = $md['wrapper_data']; - /** - * @depends testReaddir - */ - public function testIsdir($dir) { + fclose($res); - // Object names are pathy. If objects exist starting with this path we can - // consider the directory to exist. - $url = $this->newUrl('baz/'); - $this->assertFALSE(is_dir($url)); + // Now we test the same, but re-using the auth token: + $cxt = $this->basicSwiftContext(array('token' => $wrapper->token())); - $url = $this->newUrl('foo/'); - $this->assertTRUE(is_dir($url)); + $res = fopen($oUrl, 'nope', FALSE, $cxt); - } + $this->assertTrue(is_resource($res)); - /** - * @depends testReaddir - */ - public function testMkdir() { + fclose($res); - // Object names are pathy. If no object names start with the a path we can - // consider mkdir passed. If object names exist we should fail mkdir. - $url = $this->newUrl('baz/'); - $this->assertTrue(mkdir($url, 0700, TRUE, $this->basicSwiftContext())); + } - // Test the case for an existing directory. - $url = $this->newUrl('foo/'); - $this->assertFalse(mkdir($url, 0700, TRUE, $this->basicSwiftContext())); - } + /** + * @depends testOpen + */ + public function testOpenFailureWithRead() + { + $url = $this->newUrl(__FUNCTION__); + $res = @fopen($url, 'r', FALSE, $this->basicSwiftContext()); - /** - * @depends testReaddir - */ - public function testRmdir() { + $this->assertFalse($res); - // Object names are pathy. If no object names start with the a path we can - // consider rmdir passed. If object names exist we should fail rmdir. - $url = $this->newUrl('baz/'); - $this->assertTrue(rmdir($url, $this->basicSwiftContext())); + } - // Test the case for an existing directory. - $url = $this->newUrl('foo/'); - $this->assertFalse(rmdir($url, $this->basicSwiftContext())); - } + // DO we need to test other modes? + + /** + * @depends testOpen + */ + public function testOpenCreateMode() + { + $url = $this->newUrl(self::FNAME); + $res = fopen($url, 'c+', FALSE, $this->basicSwiftContext()); + $this->assertTrue(is_resource($res)); + //fclose($res); + return $res; + } + + /** + * @depends testOpenCreateMode + */ + public function testTell($res) + { + // Sould be at the beginning of the buffer. + $this->assertEquals(0, ftell($res)); + + return $res; + } + + /** + * @depends testTell + */ + public function testWrite($res) + { + $str = 'To be is to be the value of a bound variable. -- Quine'; + fwrite($res, $str); + $this->assertGreaterThan(0, ftell($res)); + + return $res; + } + + /** + * @depends testWrite + */ + public function testStat($res) + { + $stat = fstat($res); + + $this->assertGreaterThan(0, $stat['size']); + + return $res; + } + + /** + * @depends testStat + */ + public function testSeek($res) + { + $then = ftell($res); + rewind($res); + + $now = ftell($res); + + // $now should be 0 + $this->assertLessThan($then, $now); + $this->assertEquals(0, $now); + + fseek($res, 0, SEEK_END); + $final = ftell($res); + + $this->assertEquals($then, $final); + + return $res; + + } + + /** + * @depends testSeek + */ + public function testEof($res) + { + rewind($res); + + $this->assertEquals(0, ftell($res)); + + $this->assertFalse(feof($res)); + + fseek($res, 0, SEEK_END); + $this->assertGreaterThan(0, ftell($res)); + + $read = fread($res, 8192); + + $this->assertEmpty($read); + + $this->assertTrue(feof($res)); + + return $res; + } + + /** + * @depends testEof + */ + public function testFlush($res) + { + $stat1 = fstat($res); + + fflush($res); + + // Grab a copy of the object. + $url = $this->newUrl(self::FNAME); + $newObj = fopen($url, 'r', FALSE, $this->basicSwiftContext()); + + $stat2 = fstat($newObj); + + $this->assertEquals($stat1['size'], $stat2['size']); + + return $res; + } + + /** + * @depends testFlush + */ + public function testStreamGetMetadata($res) + { + // Grab a copy of the object. + $url = $this->newUrl(self::FNAME); + $newObj = fopen($url, 'r', FALSE, $this->basicSwiftContext()); + + $md = stream_get_meta_data($newObj); + //throw new \Exception(print_r($md, true)); + $obj = $md['wrapper_data']->object(); + + $this->assertInstanceOf('\OpenStack\Storage\ObjectStorage\RemoteObject', $obj); + + $this->assertEquals(self::FTYPE, $obj->contentType()); + + } + + /** + * @depends testFlush + */ + public function testClose($res) + { + $this->assertTrue(is_resource($res)); + fwrite($res, '~~~~'); + //throw new \Exception(stream_get_contents($res)); + fflush($res); + + // This is occasionally generating seemingly + // spurious PHP errors about Bootstrap::$config. + fclose($res); + + $url = $this->newUrl(self::FNAME); + $res2 = fopen($url, 'r', FALSE, $this->basicSwiftContext()); + $this->assertTrue(is_resource($res2)); + + $contents = stream_get_contents($res2); + fclose($res2); + $this->assertRegExp('/~{4}$/', $contents); + + } + + /** + * @depends testClose + */ + public function testCast() + { + $url = $this->newUrl(self::FNAME); + $res = fopen($url, 'r', FALSE, $this->basicSwiftContext()); + + $read = array($res); + $write = array(); + $except = array(); + $num_changed = stream_select($read, $write, $except, 0); + $this->assertGreaterThan(0, $num_changed); + } + + /** + * @depends testClose + */ + public function testUrlStat() + { + // Add context to the bootstrap config. + $this->addBootstrapConfig(); + + $url = $this->newUrl(self::FNAME); + + $ret = stat($url); + + // Check that the array looks right. + $this->assertEquals(26, count($ret)); + $this->assertEquals(0, $ret[3]); + $this->assertEquals($ret[2], $ret['mode']); + + $this->assertTrue(file_exists($url)); + $this->assertTrue(is_readable($url)); + $this->assertTrue(is_writeable($url)); + $this->assertFalse(is_link($url)); + $this->assertGreaterThan(0, filemtime($url)); + $this->assertGreaterThan(5, filesize($url)); + + $perm = fileperms($url); + + // Assert that this is a file. Objects are + // *always* marked as files. + $this->assertEquals(0x8000, $perm & 0x8000); + + // Assert writeable by owner. + $this->assertEquals(0x0080, $perm & 0x0080); + + // Assert not world writable. + $this->assertEquals(0, $perm & 0x0002); + + $contents = file_get_contents($url); + $this->assertGreaterThan(5, strlen($contents)); + + $fsCopy = '/tmp/hpcloud-copy-test.txt'; + copy($url, $fsCopy, $this->basicSwiftContext()); + $this->assertTrue(file_exists($fsCopy)); + unlink($fsCopy); + } + + /** + * @depends testFlush + */ + public function testUnlink() + { + $url = $this->newUrl(self::FNAME); + $cxt = $this->basicSwiftContext(); + + $ret = unlink($url, $cxt); + $this->assertTrue($ret); + + $ret2 = unlink($url, $cxt); + $this->assertFalse($ret2); + } + + public function testSetOption() + { + $url = $this->newUrl('fake.foo'); + $fake = fopen($url, 'nope', FALSE, $this->basicSwiftContext()); + + $this->assertTrue(stream_set_blocking($fake, 1)); + + // Returns 0 on success. + $this->assertEquals(0, stream_set_write_buffer($fake, 8192)); + + // Cant set a timeout on a tmp storage: + $this->assertFalse(stream_set_timeout($fake, 10)); + + fclose($fake); + } + + /** + * @depends testUnlink + */ + public function testRename() + { + $url = $this->newUrl('rename.foo'); + $fake = fopen($url, 'w+', FALSE, $this->basicSwiftContext()); + fwrite($fake, 'test'); + fclose($fake); + + $this->assertTrue(file_exists($url)); + + $url2 = $this->newUrl('rename.txt'); + + rename($url, $url2, $this->basicSwiftContext()); + + $this->assertTrue(file_exists($url2)); + $this->assertFalse(file_exists($url)); + + unlink($url2, $this->basicSwiftContext()); + } + + /** + * @depends testUnlink + */ + public function testOpenDir() + { + $urls = array('test1.txt', 'foo/test2.txt', 'foo/test3.txt', 'bar/test4.txt'); + foreach ($urls as $base) { + $url = $this->newUrl($base); + $f = fopen($url, 'c+', FALSE, $this->basicSwiftContext()); + fwrite($f, 'Test.'); + fclose($f); + } + + $dirUrl = $this->newUrl(''); + $dir = opendir($dirUrl, $this->basicSwiftContext()); + + $this->assertTrue(is_resource($dir)); + + return $dir; + + } + + /** + * @depends testOpenDir + */ + public function testReaddir($dir) + { + // Order should be newest to oldest. + $expects = array('bar/', 'foo/', 'test1.txt'); + + $buffer = array(); + while (($entry = readdir($dir)) !== FALSE) { + $should_be = array_shift($expects); + $this->assertEquals($should_be, $entry); + } + $this->assertFalse(readdir($dir)); + + return $dir; + } + /** + * @depends testReaddir + */ + public function testRewindDir($dir) + { + $this->assertFalse(readdir($dir)); + rewinddir($dir); + $this->assertEquals('bar/', readdir($dir)); + + return $dir; + } + + /** + * @depends testRewindDir + */ + public function testCloseDir($dir) + { + $this->assertTrue(is_resource($dir)); + closedir($dir); + + // There is a bug in PHP where a + // resource buffer is not getting cleared. + // So this might return a value even though + // the underlying stream is cleared. + //$this->assertFalse(readdir($dir)); + } + + /** + * @depends testCloseDir + */ + public function testOpenSubdir() + { + // Opening foo we should find test2.txt and test3.txt. + $url = $this->newUrl('foo/'); + $dir = opendir($url, $this->basicSwiftContext()); + + // I don't know why, but these are always returned in + // lexical order. + $this->assertEquals('test2.txt', readdir($dir)); + $this->assertEquals('test3.txt', readdir($dir)); + + $array = scandir($url, -1, $this->basicSwiftContext()); + $this->assertEquals(2, count($array)); + $this->assertEquals('test3.txt', $array[0]); + + } + + /** + * @depends testReaddir + */ + public function testIsdir($dir) + { + // Object names are pathy. If objects exist starting with this path we can + // consider the directory to exist. + $url = $this->newUrl('baz/'); + $this->assertFALSE(is_dir($url)); + + $url = $this->newUrl('foo/'); + $this->assertTRUE(is_dir($url)); + + } + + /** + * @depends testReaddir + */ + public function testMkdir() + { + // Object names are pathy. If no object names start with the a path we can + // consider mkdir passed. If object names exist we should fail mkdir. + $url = $this->newUrl('baz/'); + $this->assertTrue(mkdir($url, 0700, TRUE, $this->basicSwiftContext())); + + // Test the case for an existing directory. + $url = $this->newUrl('foo/'); + $this->assertFalse(mkdir($url, 0700, TRUE, $this->basicSwiftContext())); + } + + /** + * @depends testReaddir + */ + public function testRmdir() + { + // Object names are pathy. If no object names start with the a path we can + // consider rmdir passed. If object names exist we should fail rmdir. + $url = $this->newUrl('baz/'); + $this->assertTrue(rmdir($url, $this->basicSwiftContext())); + + // Test the case for an existing directory. + $url = $this->newUrl('foo/'); + $this->assertFalse(rmdir($url, $this->basicSwiftContext())); + } } diff --git a/test/Tests/StreamWrapperTest.php b/test/Tests/StreamWrapperTest.php index 0d52512..7776fc3 100644 --- a/test/Tests/StreamWrapperTest.php +++ b/test/Tests/StreamWrapperTest.php @@ -2,17 +2,17 @@ /* ============================================================================ (c) Copyright 2012-2014 Hewlett-Packard Development Company, L.P. - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at - http://www.apache.org/licenses/LICENSE-2.0 + http://www.apache.org/licenses/LICENSE-2.0 - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. ============================================================================ */ /** * Unit tests for the stream wrapper. @@ -24,574 +24,596 @@ require_once 'test/TestCase.php'; use \OpenStack\Storage\ObjectStorage\StreamWrapper; use \OpenStack\Storage\ObjectStorage\Container; use \OpenStack\Storage\ObjectStorage\Object; -use \OpenStack\Storage\ObjectStorage\ACL; /** * @group streamWrapper */ -class StreamWrapperTest extends \OpenStack\Tests\TestCase { +class StreamWrapperTest extends \OpenStack\Tests\TestCase +{ + const FNAME = 'streamTest.txt'; + const FTYPE = 'application/x-tuna-fish; charset=iso-8859-13'; - const FNAME = 'streamTest.txt'; - const FTYPE = 'application/x-tuna-fish; charset=iso-8859-13'; + /** + * Cleaning up the test container so we can reuse it for other tests. + */ + public static function tearDownAfterClass() + { + // First we get an identity + $user = self::conf('openstack.identity.username'); + $pass = self::conf('openstack.identity.password'); + $tenantId = self::conf('openstack.identity.tenantId'); + $url = self::conf('openstack.identity.url'); - /** - * Cleaning up the test container so we can reuse it for other tests. - */ - public static function tearDownAfterClass() { + $ident = new \OpenStack\Services\IdentityService($url, self::getTransportClient()); - // First we get an identity - $user = self::conf('openstack.identity.username'); - $pass = self::conf('openstack.identity.password'); - $tenantId = self::conf('openstack.identity.tenantId'); - $url = self::conf('openstack.identity.url'); + $token = $ident->authenticateAsUser($user, $pass, $tenantId); - $ident = new \OpenStack\Services\IdentityService($url, self::getTransportClient()); + // Then we need to get an instance of storage + $store = \OpenStack\Storage\ObjectStorage::newFromIdentity($ident, self::conf('openstack.swift.region'), self::getTransportClient()); - $token = $ident->authenticateAsUser($user, $pass, $tenantId); + // Delete the container and all the contents. + $cname = self::$settings['openstack.swift.container']; - // Then we need to get an instance of storage - $store = \OpenStack\Storage\ObjectStorage::newFromIdentity($ident, self::conf('openstack.swift.region'), self::getTransportClient()); + try { + $container = $store->container($cname); + } + // The container was never created. + catch (\OpenStack\Transport\FileNotFoundException $e) { + return; + } + foreach ($container as $object) { + try { + $container->delete($object->name()); + } catch (\Exception $e) {} + } - // Delete the container and all the contents. - $cname = self::$settings['openstack.swift.container']; - - try { - $container = $store->container($cname); - } - // The container was never created. - catch (\OpenStack\Transport\FileNotFoundException $e) { - return; + $store->deleteContainer($cname); } - foreach ($container as $object) { - try { - $container->delete($object->name()); - } - catch (\Exception $e) {} + protected function newUrl($objectName) + { + $scheme = StreamWrapper::DEFAULT_SCHEME; + $cname = self::$settings['openstack.swift.container']; + $cname = urlencode($cname); + + $objectParts = explode('/', $objectName); + for ($i = 0; $i < count($objectParts); ++$i) { + $objectParts[$i] = urlencode($objectParts[$i]); + } + $objectName = implode('/', $objectParts); + + $url = $scheme . '://' . $cname . '/' . $objectName; + + return $url; } - $store->deleteContainer($cname); - } + /** + * This assumes auth has already been done. + */ + protected function basicSwiftContext($add = array(), $scheme = NULL) + { + $cname = self::$settings['openstack.swift.container']; - protected function newUrl($objectName) { - $scheme = StreamWrapper::DEFAULT_SCHEME; - $cname = self::$settings['openstack.swift.container']; - $cname = urlencode($cname); + if (empty($scheme)) { + $scheme = StreamWrapper::DEFAULT_SCHEME; + } - $objectParts = explode('/', $objectName); - for ($i = 0; $i < count($objectParts); ++$i) { - $objectParts[$i] = urlencode($objectParts[$i]); - } - $objectName = implode('/', $objectParts); + if (empty(self::$ostore)) { + throw new \Exception('OStore is gone.'); + } - $url = $scheme . '://' . $cname . '/' . $objectName; + $params = $add + array( + 'token' => $this->objectStore()->token(), + 'swift_endpoint' => $this->objectStore()->url(), + 'content_type' => self::FTYPE, + 'transport_client' => $this->getTransportClient(), + ); + $cxt = array($scheme => $params); - return $url; - } - - /** - * This assumes auth has already been done. - */ - protected function basicSwiftContext($add = array(), $scheme = NULL) { - $cname = self::$settings['openstack.swift.container']; - - if (empty($scheme)) { - $scheme = StreamWrapper::DEFAULT_SCHEME; + return stream_context_create($cxt); } - if (empty(self::$ostore)) { - throw new \Exception('OStore is gone.'); + /** + * This performs authentication via context. + */ + protected function authSwiftContext($add = array(), $scheme = NULL) + { + $cname = self::$settings['openstack.swift.container']; + $username = self::$settings['openstack.identity.username']; + $password = self::$settings['openstack.identity.password']; + $tenant = self::$settings['openstack.identity.tenantId']; + $baseURL = self::$settings['openstack.identity.url']; + + if (empty($scheme)) { + $scheme = StreamWrapper::DEFAULT_SCHEME; + } + + $params = $add + array( + 'username' => $username, + 'password' => $password, + 'endpoint' => $baseURL, + 'tenantid' => $tenant, + 'content_type' => self::FTYPE, + 'transport_client' => $this->getTransportClient(), + ); + $cxt = array($scheme => $params); + + return stream_context_create($cxt); + } - $params = $add + array( - 'token' => $this->objectStore()->token(), - 'swift_endpoint' => $this->objectStore()->url(), - 'content_type' => self::FTYPE, - 'transport_client' => $this->getTransportClient(), - ); - $cxt = array($scheme => $params); + /** + * Add additional params to the config. + * + * This allows us to insert credentials into the + * bootstrap config, which in turn allows us to run + * high-level context-less functions like + * file_get_contents(), stat(), and is_file(). + */ + protected function addBootstrapConfig() + { + $opts = array( + 'username' => self::$settings['openstack.identity.username'], + 'password' => self::$settings['openstack.identity.password'], + 'endpoint' => self::$settings['openstack.identity.url'], + 'tenantid' => self::$settings['openstack.identity.tenantId'], + 'token' => $this->objectStore()->token(), + 'swift_endpoint' => $this->objectStore()->url(), + ); + \OpenStack\Bootstrap::setConfiguration($opts); - return stream_context_create($cxt); - } - - /** - * This performs authentication via context. - */ - protected function authSwiftContext($add = array(), $scheme = NULL) { - $cname = self::$settings['openstack.swift.container']; - $username = self::$settings['openstack.identity.username']; - $password = self::$settings['openstack.identity.password']; - $tenant = self::$settings['openstack.identity.tenantId']; - $baseURL = self::$settings['openstack.identity.url']; - - if (empty($scheme)) { - $scheme = StreamWrapper::DEFAULT_SCHEME; } - $params = $add + array( - 'username' => $username, - 'password' => $password, - 'endpoint' => $baseURL, - 'tenantid' => $tenant, - 'content_type' => self::FTYPE, - 'transport_client' => $this->getTransportClient(), - ); - $cxt = array($scheme => $params); - - return stream_context_create($cxt); - - } - - - /** - * Add additional params to the config. - * - * This allows us to insert credentials into the - * bootstrap config, which in turn allows us to run - * high-level context-less functions like - * file_get_contents(), stat(), and is_file(). - */ - protected function addBootstrapConfig() { - $opts = array( - 'username' => self::$settings['openstack.identity.username'], - 'password' => self::$settings['openstack.identity.password'], - 'endpoint' => self::$settings['openstack.identity.url'], - 'tenantid' => self::$settings['openstack.identity.tenantId'], - 'token' => $this->objectStore()->token(), - 'swift_endpoint' => $this->objectStore()->url(), - ); - \OpenStack\Bootstrap::setConfiguration($opts); - - } - - // Canary. There are UTF-8 encoding issues in stream wrappers. - public function testStreamContext() { - // Reset this in case something else left its - // auth token lying around. - \OpenStack\Bootstrap::setConfiguration(array( - 'token' => NULL, - )); - $cxt = $this->authSwiftContext(); - $array = stream_context_get_options($cxt); - - $opts = $array['swift']; - $endpoint = self::conf('openstack.identity.url'); - - $this->assertEquals($endpoint, $opts['endpoint'], 'A UTF-8 encoding issue.'); - } - - /** - * @depends testStreamContext - */ - public function testRegister() { - // Canary - $this->assertNotEmpty(StreamWrapper::DEFAULT_SCHEME); - - $klass = '\OpenStack\Storage\ObjectStorage\StreamWrapper'; - stream_wrapper_register(StreamWrapper::DEFAULT_SCHEME, $klass); - - $wrappers = stream_get_wrappers(); - - $this->assertContains(StreamWrapper::DEFAULT_SCHEME, $wrappers); - } - - /** - * @depends testRegister - */ - public function testOpenFailureWithoutContext() { - $cname = self::$settings['openstack.swift.container']; - - // Create a fresh container. - $this->eradicateContainer($cname); - $this->containerFixture(); - - $url = $this->newUrl('foo→/bar.txt'); - $ret = @fopen($url, 'r'); - - $this->assertFalse($ret); - } - - /** - * @depends testRegister - */ - public function testOpen() { - $cname = self::$settings['openstack.swift.container']; - - // Create a fresh container. - $this->eradicateContainer($cname); - $this->containerFixture(); - - // Simple write test. - $oUrl = $this->newUrl('foo→/test.csv'); - - $res = fopen($oUrl, 'nope', FALSE, $this->authSwiftContext()); - - $this->assertTrue(is_resource($res)); - - $md = stream_get_meta_data($res); - $wrapper = $md['wrapper_data']; - - fclose($res); - - // Now we test the same, but re-using the auth token: - $cxt = $this->basicSwiftContext(array('token' => $wrapper->token())); - $res = fopen($oUrl, 'nope', FALSE, $cxt); - - $this->assertTrue(is_resource($res)); - - fclose($res); - - } - - /** - * @depends testOpen - */ - public function testOpenFailureWithRead() { - $url = $this->newUrl(__FUNCTION__); - $res = @fopen($url, 'r', FALSE, $this->basicSwiftContext()); - - $this->assertFalse($res); - - } - - // DO we need to test other modes? - - /** - * @depends testOpen - */ - public function testOpenCreateMode() { - $url = $this->newUrl(self::FNAME); - $res = fopen($url, 'c+', FALSE, $this->basicSwiftContext()); - $this->assertTrue(is_resource($res)); - //fclose($res); - - return $res; - } - - /** - * @depends testOpenCreateMode - */ - public function testTell($res) { - // Sould be at the beginning of the buffer. - $this->assertEquals(0, ftell($res)); - - return $res; - } - - /** - * @depends testTell - */ - public function testWrite($res) { - $str = 'To be is to be the value of a bound variable. -- Quine'; - fwrite($res, $str); - $this->assertGreaterThan(0, ftell($res)); - - return $res; - } - - /** - * @depends testWrite - */ - public function testStat($res) { - $stat = fstat($res); - - $this->assertGreaterThan(0, $stat['size']); - - return $res; - } - - /** - * @depends testStat - */ - public function testSeek($res) { - $then = ftell($res); - rewind($res); - - $now = ftell($res); - - // $now should be 0 - $this->assertLessThan($then, $now); - $this->assertEquals(0, $now); - - fseek($res, 0, SEEK_END); - $final = ftell($res); - - $this->assertEquals($then, $final); - - return $res; - - } - - /** - * @depends testSeek - */ - public function testEof($res) { - rewind($res); - - $this->assertEquals(0, ftell($res)); - - $this->assertFalse(feof($res)); - - fseek($res, 0, SEEK_END); - $this->assertGreaterThan(0, ftell($res)); - - $read = fread($res, 8192); - - $this->assertEmpty($read); - - $this->assertTrue(feof($res)); - - return $res; - } - - /** - * @depends testEof - */ - public function testFlush($res) { - - $stat1 = fstat($res); - - fflush($res); - - // Grab a copy of the object. - $url = $this->newUrl(self::FNAME); - $newObj = fopen($url, 'r', FALSE, $this->basicSwiftContext()); - - $stat2 = fstat($newObj); - - $this->assertEquals($stat1['size'], $stat2['size']); - - return $res; - } - - /** - * @depends testFlush - */ - public function testStreamGetMetadata($res) { - // Grab a copy of the object. - $url = $this->newUrl(self::FNAME); - $newObj = fopen($url, 'r', FALSE, $this->basicSwiftContext()); - - $md = stream_get_meta_data($newObj); - //throw new \Exception(print_r($md, true)); - $obj = $md['wrapper_data']->object(); - - $this->assertInstanceOf('\OpenStack\Storage\ObjectStorage\RemoteObject', $obj); - - $this->assertEquals(self::FTYPE, $obj->contentType()); - - } - - /** - * @depends testFlush - */ - public function testClose($res) { - $this->assertTrue(is_resource($res)); - fwrite($res, '~~~~'); - //throw new \Exception(stream_get_contents($res)); - fflush($res); - - // This is occasionally generating seemingly - // spurious PHP errors about Bootstrap::$config. - fclose($res); - - $url = $this->newUrl(self::FNAME); - $res2 = fopen($url, 'r', FALSE, $this->basicSwiftContext()); - $this->assertTrue(is_resource($res2)); - - $contents = stream_get_contents($res2); - fclose($res2); - $this->assertRegExp('/~{4}$/', $contents); - - } - - /** - * @depends testClose - */ - public function testCast() { - $url = $this->newUrl(self::FNAME); - $res = fopen($url, 'r', FALSE, $this->basicSwiftContext()); - - $read = array($res); - $write = array(); - $except = array(); - $num_changed = stream_select($read, $write, $except, 0); - $this->assertGreaterThan(0, $num_changed); - } - - /** - * @depends testClose - */ - public function testUrlStat(){ - // Add context to the bootstrap config. - $this->addBootstrapConfig(); - - $url = $this->newUrl(self::FNAME); - - $ret = stat($url); - - // Check that the array looks right. - $this->assertEquals(26, count($ret)); - $this->assertEquals(0, $ret[3]); - $this->assertEquals($ret[2], $ret['mode']); - - $this->assertTrue(file_exists($url)); - $this->assertTrue(is_readable($url)); - $this->assertTrue(is_writeable($url)); - $this->assertFalse(is_link($url)); - $this->assertGreaterThan(0, filemtime($url)); - $this->assertGreaterThan(5, filesize($url)); - - $perm = fileperms($url); - - // Assert that this is a file. Objects are - // *always* marked as files. - $this->assertEquals(0x8000, $perm & 0x8000); - - // Assert writeable by owner. - $this->assertEquals(0x0080, $perm & 0x0080); - - // Assert not world writable. - $this->assertEquals(0, $perm & 0x0002); - - $contents = file_get_contents($url); - $this->assertGreaterThan(5, strlen($contents)); - - $fsCopy = '/tmp/hpcloud-copy-test.txt'; - copy($url, $fsCopy, $this->basicSwiftContext()); - $this->assertTrue(file_exists($fsCopy)); - unlink($fsCopy); - } - - /** - * @depends testFlush - */ - public function testUnlink(){ - $url = $this->newUrl(self::FNAME); - $cxt = $this->basicSwiftContext(); - - $ret = unlink($url, $cxt); - $this->assertTrue($ret); - - $ret2 = unlink($url, $cxt); - $this->assertFalse($ret2); - } - - public function testSetOption() { - $url = $this->newUrl('fake.foo'); - $fake = fopen($url, 'nope', FALSE, $this->basicSwiftContext()); - - $this->assertTrue(stream_set_blocking($fake, 1)); - - // Returns 0 on success. - $this->assertEquals(0, stream_set_write_buffer($fake, 8192)); - - // Cant set a timeout on a tmp storage: - $this->assertFalse(stream_set_timeout($fake, 10)); - - fclose($fake); - } - - /** - * @depends testUnlink - */ - public function testRename(){ - $url = $this->newUrl('rename.foo'); - $fake = fopen($url, 'w+', FALSE, $this->basicSwiftContext()); - fwrite($fake, 'test'); - fclose($fake); - - $this->assertTrue(file_exists($url)); - - $url2 = $this->newUrl('rename.txt'); - - rename($url, $url2, $this->basicSwiftContext()); - - $this->assertTrue(file_exists($url2)); - $this->assertFalse(file_exists($url)); - - unlink($url2, $this->basicSwiftContext()); - } - - /** - * @depends testUnlink - */ - public function testOpenDir() { - $urls = array('test1.txt', 'foo/test2.txt', 'foo/test3.txt', 'bar/test4.txt'); - foreach ($urls as $base) { - $url = $this->newUrl($base); - $f = fopen($url, 'c+', FALSE, $this->basicSwiftContext()); - fwrite($f, 'Test.'); - fclose($f); + // Canary. There are UTF-8 encoding issues in stream wrappers. + public function testStreamContext() + { + // Reset this in case something else left its + // auth token lying around. + \OpenStack\Bootstrap::setConfiguration(array( + 'token' => NULL, + )); + $cxt = $this->authSwiftContext(); + $array = stream_context_get_options($cxt); + + $opts = $array['swift']; + $endpoint = self::conf('openstack.identity.url'); + + $this->assertEquals($endpoint, $opts['endpoint'], 'A UTF-8 encoding issue.'); } - $dirUrl = $this->newUrl(''); - $dir = opendir($dirUrl, $this->basicSwiftContext()); + /** + * @depends testStreamContext + */ + public function testRegister() + { + // Canary + $this->assertNotEmpty(StreamWrapper::DEFAULT_SCHEME); - $this->assertTrue(is_resource($dir)); + $klass = '\OpenStack\Storage\ObjectStorage\StreamWrapper'; + stream_wrapper_register(StreamWrapper::DEFAULT_SCHEME, $klass); - return $dir; + $wrappers = stream_get_wrappers(); - } - - /** - * @depends testOpenDir - */ - public function testReaddir($dir){ - // Order should be newest to oldest. - $expects = array('bar/', 'foo/', 'test1.txt'); - - $buffer = array(); - while (($entry = readdir($dir)) !== FALSE) { - $should_be = array_shift($expects); - $this->assertEquals($should_be, $entry); + $this->assertContains(StreamWrapper::DEFAULT_SCHEME, $wrappers); } - $this->assertFalse(readdir($dir)); - return $dir; - } - /** - * @depends testReaddir - */ - public function testRewindDir($dir){ - $this->assertFalse(readdir($dir)); - rewinddir($dir); - $this->assertEquals('bar/', readdir($dir)); - return $dir; - } + /** + * @depends testRegister + */ + public function testOpenFailureWithoutContext() + { + $cname = self::$settings['openstack.swift.container']; - /** - * @depends testRewindDir - */ - public function testCloseDir($dir) { - $this->assertTrue(is_resource($dir)); - closedir($dir); + // Create a fresh container. + $this->eradicateContainer($cname); + $this->containerFixture(); - // There is a bug in PHP where a - // resource buffer is not getting cleared. - // So this might return a value even though - // the underlying stream is cleared. - //$this->assertFalse(readdir($dir)); - } + $url = $this->newUrl('foo→/bar.txt'); + $ret = @fopen($url, 'r'); - /** - * @depends testCloseDir - */ - public function testOpenSubdir() { + $this->assertFalse($ret); + } - // Opening foo we should find test2.txt and test3.txt. - $url = $this->newUrl('foo/'); - $dir = opendir($url, $this->basicSwiftContext()); + /** + * @depends testRegister + */ + public function testOpen() + { + $cname = self::$settings['openstack.swift.container']; - // I don't know why, but these are always returned in - // lexical order. - $this->assertEquals('test2.txt', readdir($dir)); - $this->assertEquals('test3.txt', readdir($dir)); + // Create a fresh container. + $this->eradicateContainer($cname); + $this->containerFixture(); - $array = scandir($url, -1, $this->basicSwiftContext()); - $this->assertEquals(2, count($array)); - $this->assertEquals('test3.txt', $array[0]); + // Simple write test. + $oUrl = $this->newUrl('foo→/test.csv'); - } + $res = fopen($oUrl, 'nope', FALSE, $this->authSwiftContext()); + + $this->assertTrue(is_resource($res)); + + $md = stream_get_meta_data($res); + $wrapper = $md['wrapper_data']; + + fclose($res); + + // Now we test the same, but re-using the auth token: + $cxt = $this->basicSwiftContext(array('token' => $wrapper->token())); + $res = fopen($oUrl, 'nope', FALSE, $cxt); + + $this->assertTrue(is_resource($res)); + + fclose($res); + + } + + /** + * @depends testOpen + */ + public function testOpenFailureWithRead() + { + $url = $this->newUrl(__FUNCTION__); + $res = @fopen($url, 'r', FALSE, $this->basicSwiftContext()); + + $this->assertFalse($res); + + } + + // DO we need to test other modes? + + /** + * @depends testOpen + */ + public function testOpenCreateMode() + { + $url = $this->newUrl(self::FNAME); + $res = fopen($url, 'c+', FALSE, $this->basicSwiftContext()); + $this->assertTrue(is_resource($res)); + //fclose($res); + return $res; + } + + /** + * @depends testOpenCreateMode + */ + public function testTell($res) + { + // Sould be at the beginning of the buffer. + $this->assertEquals(0, ftell($res)); + + return $res; + } + + /** + * @depends testTell + */ + public function testWrite($res) + { + $str = 'To be is to be the value of a bound variable. -- Quine'; + fwrite($res, $str); + $this->assertGreaterThan(0, ftell($res)); + + return $res; + } + + /** + * @depends testWrite + */ + public function testStat($res) + { + $stat = fstat($res); + + $this->assertGreaterThan(0, $stat['size']); + + return $res; + } + + /** + * @depends testStat + */ + public function testSeek($res) + { + $then = ftell($res); + rewind($res); + + $now = ftell($res); + + // $now should be 0 + $this->assertLessThan($then, $now); + $this->assertEquals(0, $now); + + fseek($res, 0, SEEK_END); + $final = ftell($res); + + $this->assertEquals($then, $final); + + return $res; + + } + + /** + * @depends testSeek + */ + public function testEof($res) + { + rewind($res); + + $this->assertEquals(0, ftell($res)); + + $this->assertFalse(feof($res)); + + fseek($res, 0, SEEK_END); + $this->assertGreaterThan(0, ftell($res)); + + $read = fread($res, 8192); + + $this->assertEmpty($read); + + $this->assertTrue(feof($res)); + + return $res; + } + + /** + * @depends testEof + */ + public function testFlush($res) + { + $stat1 = fstat($res); + + fflush($res); + + // Grab a copy of the object. + $url = $this->newUrl(self::FNAME); + $newObj = fopen($url, 'r', FALSE, $this->basicSwiftContext()); + + $stat2 = fstat($newObj); + + $this->assertEquals($stat1['size'], $stat2['size']); + + return $res; + } + + /** + * @depends testFlush + */ + public function testStreamGetMetadata($res) + { + // Grab a copy of the object. + $url = $this->newUrl(self::FNAME); + $newObj = fopen($url, 'r', FALSE, $this->basicSwiftContext()); + + $md = stream_get_meta_data($newObj); + //throw new \Exception(print_r($md, true)); + $obj = $md['wrapper_data']->object(); + + $this->assertInstanceOf('\OpenStack\Storage\ObjectStorage\RemoteObject', $obj); + + $this->assertEquals(self::FTYPE, $obj->contentType()); + + } + + /** + * @depends testFlush + */ + public function testClose($res) + { + $this->assertTrue(is_resource($res)); + fwrite($res, '~~~~'); + //throw new \Exception(stream_get_contents($res)); + fflush($res); + + // This is occasionally generating seemingly + // spurious PHP errors about Bootstrap::$config. + fclose($res); + + $url = $this->newUrl(self::FNAME); + $res2 = fopen($url, 'r', FALSE, $this->basicSwiftContext()); + $this->assertTrue(is_resource($res2)); + + $contents = stream_get_contents($res2); + fclose($res2); + $this->assertRegExp('/~{4}$/', $contents); + + } + + /** + * @depends testClose + */ + public function testCast() + { + $url = $this->newUrl(self::FNAME); + $res = fopen($url, 'r', FALSE, $this->basicSwiftContext()); + + $read = array($res); + $write = array(); + $except = array(); + $num_changed = stream_select($read, $write, $except, 0); + $this->assertGreaterThan(0, $num_changed); + } + + /** + * @depends testClose + */ + public function testUrlStat() + { + // Add context to the bootstrap config. + $this->addBootstrapConfig(); + + $url = $this->newUrl(self::FNAME); + + $ret = stat($url); + + // Check that the array looks right. + $this->assertEquals(26, count($ret)); + $this->assertEquals(0, $ret[3]); + $this->assertEquals($ret[2], $ret['mode']); + + $this->assertTrue(file_exists($url)); + $this->assertTrue(is_readable($url)); + $this->assertTrue(is_writeable($url)); + $this->assertFalse(is_link($url)); + $this->assertGreaterThan(0, filemtime($url)); + $this->assertGreaterThan(5, filesize($url)); + + $perm = fileperms($url); + + // Assert that this is a file. Objects are + // *always* marked as files. + $this->assertEquals(0x8000, $perm & 0x8000); + + // Assert writeable by owner. + $this->assertEquals(0x0080, $perm & 0x0080); + + // Assert not world writable. + $this->assertEquals(0, $perm & 0x0002); + + $contents = file_get_contents($url); + $this->assertGreaterThan(5, strlen($contents)); + + $fsCopy = '/tmp/hpcloud-copy-test.txt'; + copy($url, $fsCopy, $this->basicSwiftContext()); + $this->assertTrue(file_exists($fsCopy)); + unlink($fsCopy); + } + + /** + * @depends testFlush + */ + public function testUnlink() + { + $url = $this->newUrl(self::FNAME); + $cxt = $this->basicSwiftContext(); + + $ret = unlink($url, $cxt); + $this->assertTrue($ret); + + $ret2 = unlink($url, $cxt); + $this->assertFalse($ret2); + } + + public function testSetOption() + { + $url = $this->newUrl('fake.foo'); + $fake = fopen($url, 'nope', FALSE, $this->basicSwiftContext()); + + $this->assertTrue(stream_set_blocking($fake, 1)); + + // Returns 0 on success. + $this->assertEquals(0, stream_set_write_buffer($fake, 8192)); + + // Cant set a timeout on a tmp storage: + $this->assertFalse(stream_set_timeout($fake, 10)); + + fclose($fake); + } + + /** + * @depends testUnlink + */ + public function testRename() + { + $url = $this->newUrl('rename.foo'); + $fake = fopen($url, 'w+', FALSE, $this->basicSwiftContext()); + fwrite($fake, 'test'); + fclose($fake); + + $this->assertTrue(file_exists($url)); + + $url2 = $this->newUrl('rename.txt'); + + rename($url, $url2, $this->basicSwiftContext()); + + $this->assertTrue(file_exists($url2)); + $this->assertFalse(file_exists($url)); + + unlink($url2, $this->basicSwiftContext()); + } + + /** + * @depends testUnlink + */ + public function testOpenDir() + { + $urls = array('test1.txt', 'foo/test2.txt', 'foo/test3.txt', 'bar/test4.txt'); + foreach ($urls as $base) { + $url = $this->newUrl($base); + $f = fopen($url, 'c+', FALSE, $this->basicSwiftContext()); + fwrite($f, 'Test.'); + fclose($f); + } + + $dirUrl = $this->newUrl(''); + $dir = opendir($dirUrl, $this->basicSwiftContext()); + + $this->assertTrue(is_resource($dir)); + + return $dir; + + } + + /** + * @depends testOpenDir + */ + public function testReaddir($dir) + { + // Order should be newest to oldest. + $expects = array('bar/', 'foo/', 'test1.txt'); + + $buffer = array(); + while (($entry = readdir($dir)) !== FALSE) { + $should_be = array_shift($expects); + $this->assertEquals($should_be, $entry); + } + $this->assertFalse(readdir($dir)); + + return $dir; + } + /** + * @depends testReaddir + */ + public function testRewindDir($dir) + { + $this->assertFalse(readdir($dir)); + rewinddir($dir); + $this->assertEquals('bar/', readdir($dir)); + + return $dir; + } + + /** + * @depends testRewindDir + */ + public function testCloseDir($dir) + { + $this->assertTrue(is_resource($dir)); + closedir($dir); + + // There is a bug in PHP where a + // resource buffer is not getting cleared. + // So this might return a value even though + // the underlying stream is cleared. + //$this->assertFalse(readdir($dir)); + } + + /** + * @depends testCloseDir + */ + public function testOpenSubdir() + { + // Opening foo we should find test2.txt and test3.txt. + $url = $this->newUrl('foo/'); + $dir = opendir($url, $this->basicSwiftContext()); + + // I don't know why, but these are always returned in + // lexical order. + $this->assertEquals('test2.txt', readdir($dir)); + $this->assertEquals('test3.txt', readdir($dir)); + + $array = scandir($url, -1, $this->basicSwiftContext()); + $this->assertEquals(2, count($array)); + $this->assertEquals('test3.txt', $array[0]); + + } } diff --git a/test/bootstrap_curl.php b/test/bootstrap_curl.php index 78f8b9a..4adaf3d 100644 --- a/test/bootstrap_curl.php +++ b/test/bootstrap_curl.php @@ -2,22 +2,22 @@ /* ============================================================================ (c) Copyright 2012-2014 Hewlett-Packard Development Company, L.P. - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at - http://www.apache.org/licenses/LICENSE-2.0 + http://www.apache.org/licenses/LICENSE-2.0 - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. ============================================================================ */ /** * Helpers for testing using the CurlTransport. */ $bootstrap_settings = array( - 'transport' => '\OpenStack\Transport\CURLTransport', + 'transport' => '\OpenStack\Transport\CURLTransport', ); diff --git a/test/bootstrap_phpstream.php b/test/bootstrap_phpstream.php index 007f66a..7b0f249 100644 --- a/test/bootstrap_phpstream.php +++ b/test/bootstrap_phpstream.php @@ -2,22 +2,22 @@ /* ============================================================================ (c) Copyright 2012-2014 Hewlett-Packard Development Company, L.P. - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at - http://www.apache.org/licenses/LICENSE-2.0 + http://www.apache.org/licenses/LICENSE-2.0 - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. ============================================================================ */ /** * Helpers for testing using the CurlTransport. */ $bootstrap_settings = array( - 'transport' => '\OpenStack\Transport\PHPStreamTransport', + 'transport' => '\OpenStack\Transport\PHPStreamTransport', );