diff --git a/.travis.yml b/.travis.yml index bdd5f76af..78dd0003f 100644 --- a/.travis.yml +++ b/.travis.yml @@ -3,7 +3,6 @@ php: - "5.6" - "5.5" - "5.4" - - "5.3" - hhvm sudo: false @@ -33,4 +32,4 @@ notifications: - shaunak.kashyap@rackspace.com env: - - secure: "bdrUeYb3nSGgBB+QtDZxUHVPw6B/wjb3KXLm8TgonWrQm4GPeWKK29qhmDnFZmQjwQPfuebe7wAk1ZxGoZKbEiELVpJJ+8XYVOt6W/6V53H31JL6FqiIE5+7qBwDe+9ziveM6GcTXHT1GI5mUeACIbeBDPZaNubIJH3U6MPim64=" \ No newline at end of file + - secure: "bdrUeYb3nSGgBB+QtDZxUHVPw6B/wjb3KXLm8TgonWrQm4GPeWKK29qhmDnFZmQjwQPfuebe7wAk1ZxGoZKbEiELVpJJ+8XYVOt6W/6V53H31JL6FqiIE5+7qBwDe+9ziveM6GcTXHT1GI5mUeACIbeBDPZaNubIJH3U6MPim64=" diff --git a/README.md b/README.md index 6031cf4ac..5f4d75da5 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ PHP SDK for OpenStack/Rackspace APIs [![Latest Stable Version](https://poser.pugx.org/rackspace/php-opencloud/v/stable.png)](https://packagist.org/packages/rackspace/php-opencloud) [![Travis CI](https://secure.travis-ci.org/rackspace/php-opencloud.png)](https://travis-ci.org/rackspace/php-opencloud) [![Total Downloads](https://poser.pugx.org/rackspace/php-opencloud/downloads.png)](https://packagist.org/packages/rackspace/php-opencloud) -For SDKs in different languages, see http://developer.rackspace.com. +Our official documentation is now available on http://docs.php-opencloud.com. For SDKs in different languages, see http://developer.rackspace.com. The PHP SDK should work with most OpenStack-based cloud deployments, though it specifically targets the Rackspace public cloud. In @@ -16,9 +16,11 @@ so that you can still use the SDK with a pure OpenStack instance Requirements ------------ -* PHP >=5.3.3 +* PHP >=5.4 * cURL extension for PHP +**Note**: Since PHP 5.3 has reached [end of life](http://php.net/eol.php) and is no longer officially supported, we are moving to 5.4 as a minimum requirement. If upgrading is not an option and you still need a stable version of the SDK for 5.3, please follow [this guide](http://docs.php-opencloud.com/en/latest/using-php-5.3). + Installation ------------ You must install this library through Composer: diff --git a/composer.json b/composer.json index 693e986ec..d22c56524 100644 --- a/composer.json +++ b/composer.json @@ -26,7 +26,7 @@ } }, "require": { - "php" : ">=5.3.3", + "php" : ">=5.4", "guzzle/guzzle" : "~3.8", "psr/log": "~1.0" }, @@ -35,6 +35,6 @@ "satooshi/php-coveralls": "0.6.*@dev", "jakub-onderka/php-parallel-lint": "0.*", "fabpot/php-cs-fixer": "1.0.*@dev", - "apigen/apigen": "~2.8" + "apigen/apigen": "~4.0" } } diff --git a/doc/index.rst b/doc/index.rst index 4994f0efb..3c47091cc 100644 --- a/doc/index.rst +++ b/doc/index.rst @@ -30,6 +30,10 @@ Read the :doc:`getting-started-with-openstack` or :doc:`getting-started-with-rackspace` to help you get started with basic Compute operations. +.. note:: + + If you are running PHP 5.3, please see our :doc:`using-php-5.3` guide. + Services -------- diff --git a/doc/services/common/service-args.rst b/doc/services/common/service-args.rst index c1cc754b8..128986d91 100644 --- a/doc/services/common/service-args.rst +++ b/doc/services/common/service-args.rst @@ -3,9 +3,9 @@ default will be provided if you pass in ``null``. * ``{region}`` is the region the service will operate in. For Rackspace - users, you can select one of the following from the `supported regions page - `_. + users, you can select one of the following from the :doc:`supported regions page + `. -* ``{urlType}`` is the `type of URL `_ to use, depending on which +* ``{urlType}`` is the :doc:`type of URL ` to use, depending on which endpoints your catalog provides. If omitted, it will default to the public network. diff --git a/doc/services/dns/domains.rst b/doc/services/dns/domains.rst index 0a7b83b96..b6c60290c 100644 --- a/doc/services/dns/domains.rst +++ b/doc/services/dns/domains.rst @@ -268,8 +268,8 @@ Modify domain Only the TTL, email address and comment attributes of a domain can be modified. Records cannot be added, modified, or removed through this API operation - you -will need to use the `add records `__, `modify records -`__ or `remove records `__ +will need to use the `add records `__, `modify records +`__ or `remove records `__ operations respectively. .. code-block:: php diff --git a/doc/services/object-store/migrating-containers.rst b/doc/services/object-store/migrating-containers.rst index 22adb839b..da983af3e 100644 --- a/doc/services/object-store/migrating-containers.rst +++ b/doc/services/object-store/migrating-containers.rst @@ -68,12 +68,12 @@ You can access all this functionality by executing: It's advisable to do this process in a Cloud Server in one of the two -regions you're migrating to/from. This allows you to use ``privateURL`` +regions you're migrating to/from. This allows you to use ``internalURL`` as the third argument in the ``objectStoreService`` methods like this: .. code-block:: php - $client->objectStoreService('cloudFiles', 'IAD', 'privateURL'); + $client->objectStoreService('cloudFiles', 'IAD', 'internalURL'); This will ensure that traffic between your server and your new IAD diff --git a/doc/services/object-store/objects.rst b/doc/services/object-store/objects.rst index 0eb0ea836..e23d6d4cb 100644 --- a/doc/services/object-store/objects.rst +++ b/doc/services/object-store/objects.rst @@ -177,6 +177,42 @@ docs `_ +List over 10,000 objects +------------------------ + +To retrieve more than 10,000 objects (the default limit), you'll need to use +the built-in paging which uses a 'marker' parameter to fetch the next page of data. + +.. code-block:: php + + $containerObjects = array(); + $marker = ''; + + while ($marker !== null) { + $params = array( + 'marker' => $marker, + ); + + $objects = $container->objectList($params); + $total = $objects->count(); + $count = 0; + + if ($total == 0) { + break; + } + + foreach ($objects as $object) { + /** @var $object OpenCloud\ObjectStore\Resource\DataObject **/ + $containerObjects[] = $object->getName(); + $count++; + + $marker = ($count == $total) ? $object->getName() : null; + } + } + +`Get the executable PHP script for this example `_ + + Get object ---------- @@ -204,8 +240,8 @@ Get file name .. code-block:: php - /** @param $container OpenCloud\ObjectStore\Resource\Container */ - $container = $object->getContainer(); + /** @param $name string */ + $name = $object->getName(); Get file size @@ -326,6 +362,41 @@ the name of the object inside the container that does not exist yet. `Get the executable PHP script for this example `_ +Symlinking to this object from another location +----------------------------------------------- + +To create a symlink to this file in another location you need to specify +a string-based source + +.. code-block:: php + + $object->createSymlinkFrom('/container_2/new_object_name'); + +Where ``container_2`` is the name of the container, and ``new_object_name`` is +the name of the object inside the container that either does not exist yet or +is an empty file. + +`Get the executable PHP script for this example `_ + + +Setting this object to symlink to another location +-------------------------------------------------- + +To set this file to symlink to another location you need to specify +a string-based destination + +.. code-block:: php + + $object->createSymlinkTo('/container_2/new_object_name'); + +Where ``container_2`` is the name of the container, and ``new_object_name`` is +the name of the object inside the container. + +The object must be an empty file. + +`Get the executable PHP script for this example `_ + + Get object metadata ------------------- diff --git a/doc/using-php-5.3.rst b/doc/using-php-5.3.rst new file mode 100644 index 000000000..8790710f2 --- /dev/null +++ b/doc/using-php-5.3.rst @@ -0,0 +1,20 @@ +Using the SDK with PHP v5.3 +=========================== + +Since PHP 5.3 has entered EOL and no longer receives security updates, we have bumped the minimum requirement to 5.4. Using 5.3 is still possible, however, but you will need to use an older stable version of the SDK. There are two ways to do this. + +The first way is by requiring it through the command line: + +.. code-block:: bash + + composer require rackspace/php-opencloud:1.12 + +The second way is by updating your composer.json file, and specifying the appropriate version of the SDK: + +.. code-block:: json + + "require": { + "rackspace/php-opencloud": "~1.12" + } + +Note that **1.12** is the last minor release supporting PHP 5.3. Version 1.13 and above has shifted to PHP 5.4. diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 000000000..154fd158d --- /dev/null +++ b/docs/README.md @@ -0,0 +1,3 @@ +# Documentation + +Our official docs are hosted on http://docs.php-opencloud.com. diff --git a/lib/OpenCloud/Common/Constants/Header.php b/lib/OpenCloud/Common/Constants/Header.php index c0246f71f..5de04bc2a 100644 --- a/lib/OpenCloud/Common/Constants/Header.php +++ b/lib/OpenCloud/Common/Constants/Header.php @@ -70,4 +70,5 @@ class Header const USER_AGENT = 'User-Agent'; const VARY = 'Vary'; const VIA = 'Via'; + const X_OBJECT_MANIFEST = 'X-Object-Manifest'; } diff --git a/lib/OpenCloud/Common/Exceptions/DomainNotFoundException.php b/lib/OpenCloud/Common/Exceptions/DomainNotFoundException.php new file mode 100644 index 000000000..07ddfb608 --- /dev/null +++ b/lib/OpenCloud/Common/Exceptions/DomainNotFoundException.php @@ -0,0 +1,22 @@ + 'extendedStatus', 'OS-EXT-STS:task_state' => 'taskStatus', 'OS-EXT-STS:power_state' => 'powerStatus', + 'OS-EXT-AZ:availability_zone' => 'availabilityZone' ); /** @@ -729,6 +736,12 @@ protected function createJson() $server->user_data = $this->user_data; } + // Availability zone + if (!empty($this->availabilityZone)) { + $this->checkExtension('OS-EXT-AZ'); + $server->availability_zone = $this->availabilityZone; + } + return (object) array('server' => $server); } diff --git a/lib/OpenCloud/Compute/Service.php b/lib/OpenCloud/Compute/Service.php index d34f6101a..37d66a036 100644 --- a/lib/OpenCloud/Compute/Service.php +++ b/lib/OpenCloud/Compute/Service.php @@ -81,7 +81,7 @@ public function __construct(Client $client, $type = null, $name = null, $region * * @api * @param string $id - if specified, the server with the ID is retrieved - * @returns Resource\Server object + * @return Resource\Server object */ public function server($id = null) { @@ -102,7 +102,7 @@ public function server($id = null) * not having all the information you need. * @param array $filter - a set of key/value pairs that is passed to the * servers list for filtering - * @returns \OpenCloud\Common\Collection + * @return \OpenCloud\Common\Collection\PaginatedIterator */ public function serverList($details = true, array $filter = array()) { @@ -128,7 +128,7 @@ public function network($id = null) * * @api * @param array $filter array of filter key/value pairs - * @return \OpenCloud\Common\Collection + * @return \OpenCloud\Common\Collection\PaginatedIterator */ public function networkList($filter = array()) { diff --git a/lib/OpenCloud/DNS/Resource/Domain.php b/lib/OpenCloud/DNS/Resource/Domain.php index cdd8ef28e..a103677f5 100644 --- a/lib/OpenCloud/DNS/Resource/Domain.php +++ b/lib/OpenCloud/DNS/Resource/Domain.php @@ -76,7 +76,7 @@ public function record($info = null) * returns a Collection of Record objects * * @param array $filter query-string parameters - * @return \OpenCloud\Collection + * @return OpenCloud\DNS\Collection\DnsIterator */ public function recordList($filter = array()) { @@ -107,6 +107,7 @@ public function subdomain($info = array()) * * @param array $filter key/value pairs for query string parameters * return \OpenCloud\Collection + * @return OpenCloud\DNS\Collection\DnsIterator */ public function subdomainList($filter = array()) { diff --git a/lib/OpenCloud/DNS/Service.php b/lib/OpenCloud/DNS/Service.php index cafb4ad94..651353e14 100644 --- a/lib/OpenCloud/DNS/Service.php +++ b/lib/OpenCloud/DNS/Service.php @@ -19,6 +19,7 @@ use OpenCloud\Common\Http\Message\Formatter; use OpenCloud\Common\Service\CatalogService; +use OpenCloud\Common\Exceptions\DomainNotFoundException; use OpenCloud\DNS\Collection\DnsIterator; use OpenCloud\DNS\Resource\AsyncResponse; use OpenCloud\DNS\Resource\Domain; @@ -55,11 +56,29 @@ public function domain($info = null) return $this->resource('Domain', $info); } + /** + * Returns a domain, given a domain name + * + * @param string $domainName Domain name + * @return Domain the domain object + */ + public function domainByName($domainName) + { + $domainList = $this->domainList(array("name" => $domainName)); + + if (count($domainList) != 1) { + throw new DomainNotFoundException(); + } + + return $this->resource('Domain', $domainList[0]); + } + + /** * Returns a collection of domains * * @param array $filter key/value pairs to use as query strings - * @return \OpenCloud\Common\Collection + * @return OpenCloud\DNS\Collection\DnsIterator */ public function domainList($filter = array()) { @@ -85,7 +104,7 @@ public function ptrRecord($info = null) * * @param \OpenCloud\Compute\Resource\Server $server the server for which to * retrieve the PTR records - * @return \OpenCloud\Common\Collection + * @return OpenCloud\DNS\Collection\DnsIterator */ public function ptrRecordList(HasPtrRecordsInterface $parent) { @@ -181,6 +200,13 @@ public function limitTypes() return $body->limitTypes; } + /** + * List asynchronous responses' statuses. + * @see http://docs.rackspace.com/cdns/api/v1.0/cdns-devguide/content/viewing_status_all_asynch_jobs.html + * + * @param array $query Any query parameters. Optional. + * @return OpenCloud\DNS\Collection\DnsIterator + */ public function listAsyncJobs(array $query = array()) { $url = clone $this->getUrl(); diff --git a/lib/OpenCloud/ObjectStore/AbstractService.php b/lib/OpenCloud/ObjectStore/AbstractService.php index 8f92874f8..6094d7780 100644 --- a/lib/OpenCloud/ObjectStore/AbstractService.php +++ b/lib/OpenCloud/ObjectStore/AbstractService.php @@ -30,7 +30,9 @@ abstract class AbstractService extends CatalogService const MAX_OBJECT_SIZE = 5102410241025; /** - * @return Resource\Account + * Returns the Object Store account associated with the service. + * + * @return Resource\Account Object Store account */ public function getAccount() { diff --git a/lib/OpenCloud/ObjectStore/CDNService.php b/lib/OpenCloud/ObjectStore/CDNService.php index d45fc494d..0092941e0 100644 --- a/lib/OpenCloud/ObjectStore/CDNService.php +++ b/lib/OpenCloud/ObjectStore/CDNService.php @@ -31,8 +31,12 @@ class CDNService extends AbstractService /** * List CDN-enabled containers. * - * @param array $filter - * @return \OpenCloud\Common\Collection\PaginatedIterator + * @param array $filter Array of filter options such as: + * + * * `limit`: number of results to limit the list to. Optional. + * * `marker`: name of container after which to start the list. Optional. + * * `end_marker`: name of container before which to end the list. Optional. + * @return \OpenCloud\Common\Collection\PaginatedIterator Iterator to list of CDN-enabled containers */ public function listContainers(array $filter = array()) { @@ -40,6 +44,12 @@ public function listContainers(array $filter = array()) return $this->resourceList('CDNContainer', $this->getUrl(null, $filter), $this); } + /** + * Return an existing CDN-enabled container. + * + * @param \stdClass $data Data to initialize container. + * @return CDNContainer CDN-enabled Container + */ public function cdnContainer($data) { $container = new CDNContainer($this, $data); diff --git a/lib/OpenCloud/ObjectStore/Exception/ObjectNotEmptyException.php b/lib/OpenCloud/ObjectStore/Exception/ObjectNotEmptyException.php new file mode 100644 index 000000000..6bf75f2f9 --- /dev/null +++ b/lib/OpenCloud/ObjectStore/Exception/ObjectNotEmptyException.php @@ -0,0 +1,35 @@ +name = $name; + + return $e; + } +} diff --git a/lib/OpenCloud/ObjectStore/Resource/AbstractContainer.php b/lib/OpenCloud/ObjectStore/Resource/AbstractContainer.php index a36320652..56cd1c2fa 100644 --- a/lib/OpenCloud/ObjectStore/Resource/AbstractContainer.php +++ b/lib/OpenCloud/ObjectStore/Resource/AbstractContainer.php @@ -50,13 +50,28 @@ public function __construct(ServiceInterface $service, $data = null) $this->populate($data); } + /** + * Return the transaction ID for an HTTP API operation. Useful for debugging. + * + * @return string Transaction ID + */ public function getTransId() { return $this->metadata->getProperty(HeaderConst::TRANS_ID); } + /** + * Returns whether this container is CDN-enabled or not. + * + * @return boolean true if this container is CDN-enabled; false, otherwise. + */ abstract public function isCdnEnabled(); + /** + * Returns whether this container has log retention enabled or not. + * + * @return boolean true if this container has log retention enabled; false, otherwise. + */ public function hasLogRetention() { if ($this instanceof CDNContainer) { @@ -66,11 +81,23 @@ public function hasLogRetention() } } + /** + * For internal use only + * + * @return string Name of the primary key field for this resource + */ public function primaryKeyField() { return 'name'; } + /** + * For internal use only + * + * @param string $path Path to add to URL. Optional. + * @param array $params Query parameters to add to URL. Optional. + * @return Url URL of this container + path + query parameters. + */ public function getUrl($path = null, array $params = array()) { if (strlen($this->getName()) == 0) { @@ -91,7 +118,7 @@ protected function createRefreshRequest() * This method will enable your CDN-enabled container to serve out HTML content like a website. * * @param $indexPage The data object name (i.e. a .html file) that will serve as the main index page. - * @return \Guzzle\Http\Message\Response + * @return \Guzzle\Http\Message\Response The HTTP response for this API operation. */ public function setStaticIndexPage($page) { @@ -110,7 +137,7 @@ public function setStaticIndexPage($page) * Set the default error page for your static site. * * @param $name The data object name (i.e. a .html file) that will serve as the main error page. - * @return \Guzzle\Http\Message\Response + * @return \Guzzle\Http\Message\Response The HTTP response for this operation. */ public function setStaticErrorPage($page) { diff --git a/lib/OpenCloud/ObjectStore/Resource/AbstractResource.php b/lib/OpenCloud/ObjectStore/Resource/AbstractResource.php index e2d494ede..37722e93d 100644 --- a/lib/OpenCloud/ObjectStore/Resource/AbstractResource.php +++ b/lib/OpenCloud/ObjectStore/Resource/AbstractResource.php @@ -45,16 +45,31 @@ public function __construct(ServiceInterface $service) $this->metadata = new $this->metadataClass; } + /** + * For internal use only. + * + * @return Service The ObjectStore service associated with this ObjectStore resource. + */ public function getService() { return $this->service; } + /** + * For internal use only. + * + * @return Service The CDN version of the ObjectStore service associated with this ObjectStore resource. + */ public function getCdnService() { return $this->service->getCDNService(); } + /** + * For internal use only. + * + * @return Client The HTTP client associated with the associated ObjectStore service. + */ public function getClient() { return $this->service->getClient(); @@ -63,9 +78,11 @@ public function getClient() /** * Factory method that allows for easy instantiation from a Response object. * - * @param Response $response - * @param ServiceInterface $service - * @return static + * For internal use only. + * + * @param Response $response HTTP response from an API operation. + * @param ServiceInterface $service The ObjectStore service to associate with this ObjectStore resource object. + * @return AbstractResource A concrete sub-class of `AbstractResource`. */ public static function fromResponse(Response $response, ServiceInterface $service) { @@ -81,8 +98,10 @@ public static function fromResponse(Response $response, ServiceInterface $servic /** * Trim headers of their resource-specific prefixes. * - * @param $headers - * @return array + * For internal use only. + * + * @param array $headers Headers as returned from an HTTP response + * @return array Trimmed headers */ public static function trimHeaders($headers) { @@ -121,8 +140,8 @@ protected static function stripPrefix($header) /** * Prepend/stock the header names with a resource-specific prefix. * - * @param array $headers - * @return array + * @param array $headers Headers to use on ObjectStore resource. + * @return array Headers returned with appropriate prefix as expected by ObjectStore service. */ public static function stockHeaders(array $headers) { @@ -147,11 +166,12 @@ public static function stockHeaders(array $headers) } /** - * Set the metadata (local-only) for this object. + * Set the metadata (local-only) for this object. You must call saveMetadata + * to actually persist the metadata using the ObjectStore service. * - * @param $data - * @param bool $constructFromResponse - * @return $this + * @param array $data Object/container metadata key/value pair array. + * @param bool $constructFromResponse Whether the metadata key/value pairs were obtiained from an HTTP response of an ObjectStore API operation. + * @return AbstractResource This object, with metadata set. */ public function setMetadata($data, $constructFromResponse = false) { @@ -167,7 +187,9 @@ public function setMetadata($data, $constructFromResponse = false) } /** - * @return \OpenCloud\Common\Metadata + * Returns metadata for this object. + * + * @return \OpenCloud\Common\Metadata Metadata set on this object. */ public function getMetadata() { @@ -180,7 +202,7 @@ public function getMetadata() * @param array $metadata The array of values you want to set as metadata * @param bool $stockPrefix Whether to prepend each array key with the metadata-specific prefix. For objects, this * would be X-Object-Meta-Foo => Bar - * @return mixed + * @return Response HTTP response from API operation. */ public function saveMetadata(array $metadata, $stockPrefix = true) { @@ -192,7 +214,7 @@ public function saveMetadata(array $metadata, $stockPrefix = true) /** * Retrieve metadata from the API. This method will then set and return this value. * - * @return \OpenCloud\Common\Metadata + * @return \OpenCloud\Common\Metadata Metadata returned from the ObjectStore service for this object/container. */ public function retrieveMetadata() { @@ -208,8 +230,8 @@ public function retrieveMetadata() /** * To delete or unset a particular metadata item. * - * @param $key - * @return mixed + * @param $key Metadata key to unset + * @return Response HTTP response returned from API operation to unset metadata item. */ public function unsetMetadataItem($key) { @@ -224,10 +246,12 @@ public function unsetMetadataItem($key) } /** - * Append a particular array of values to the existing metadata. Analogous to a merge. + * Append a particular array of values to the existing metadata. Analogous + * to a merge. You must call to actually persist the metadata using the + * ObjectStore service. * - * @param array $values - * @return array + * @param array $values The array of values you want to append to metadata. + * @return array Metadata, after `$values` are appended. */ public function appendToMetadata(array $values) { diff --git a/lib/OpenCloud/ObjectStore/Resource/Container.php b/lib/OpenCloud/ObjectStore/Resource/Container.php index 0f7b0df08..57b8ce34a 100644 --- a/lib/OpenCloud/ObjectStore/Resource/Container.php +++ b/lib/OpenCloud/ObjectStore/Resource/Container.php @@ -323,23 +323,6 @@ public function refresh($id = null, $url = null) { $headers = $this->createRefreshRequest()->send()->getHeaders(); $this->setMetadata($headers, true); - - try { - if (null !== ($cdnService = $this->getService()->getCDNService())) { - $cdn = new CDNContainer($cdnService); - $cdn->setName($this->name); - - $response = $cdn->createRefreshRequest()->send(); - - if ($response->isSuccessful()) { - $this->cdn = $cdn; - $this->cdn->setMetadata($response->getHeaders(), true); - } - } else { - $this->cdn = null; - } - } catch (ClientErrorResponseException $e) { - } } /** @@ -597,6 +580,30 @@ public function uploadDirectory($path) public function isCdnEnabled() { + // If CDN object is not already populated, try to populate it. + if (null === $this->cdn) { + $this->refreshCdnObject(); + } return ($this->cdn instanceof CDNContainer) && $this->cdn->isCdnEnabled(); } + + protected function refreshCdnObject() + { + try { + if (null !== ($cdnService = $this->getService()->getCDNService())) { + $cdn = new CDNContainer($cdnService); + $cdn->setName($this->name); + + $response = $cdn->createRefreshRequest()->send(); + + if ($response->isSuccessful()) { + $this->cdn = $cdn; + $this->cdn->setMetadata($response->getHeaders(), true); + } + } else { + $this->cdn = null; + } + } catch (ClientErrorResponseException $e) { + } + } } diff --git a/lib/OpenCloud/ObjectStore/Resource/DataObject.php b/lib/OpenCloud/ObjectStore/Resource/DataObject.php index 5877431b8..de302b4cd 100644 --- a/lib/OpenCloud/ObjectStore/Resource/DataObject.php +++ b/lib/OpenCloud/ObjectStore/Resource/DataObject.php @@ -24,6 +24,7 @@ use OpenCloud\Common\Exceptions; use OpenCloud\Common\Lang; use OpenCloud\ObjectStore\Constants\UrlType; +use OpenCloud\ObjectStore\Exception\ObjectNotEmptyException; /** * Objects are the basic storage entities in Cloud Files. They represent the @@ -76,6 +77,11 @@ class DataObject extends AbstractResource * @var string Etag. */ protected $etag; + + /** + * @var string Manifest. Can be null so we use false to mean unset. + */ + protected $manifest = false; /** * Also need to set Container parent and handle pseudo-directories. @@ -139,7 +145,9 @@ public function populateFromResponse(Response $response) ->setContentType((string) $headers[HeaderConst::CONTENT_TYPE]) ->setLastModified((string) $headers[HeaderConst::LAST_MODIFIED]) ->setContentLength((string) $headers[HeaderConst::CONTENT_LENGTH]) - ->setEtag((string) $headers[HeaderConst::ETAG]); + ->setEtag((string) $headers[HeaderConst::ETAG]) + // do not cast to a string to allow for null (i.e. no header) + ->setManifest($headers[HeaderConst::X_OBJECT_MANIFEST]); } public function refresh() @@ -293,6 +301,26 @@ public function getEtag() { return $this->etag ? : $this->content->getContentMd5(); } + + /** + * @param string $manifest Path (`container/object') to set as the value to X-Object-Manifest + * @return $this + */ + protected function setManifest($manifest) + { + $this->manifest = $manifest; + + return $this; + } + + /** + * @return null|string Path (`container/object') from X-Object-Manifest header or null if the header does not exist + */ + public function getManifest() + { + // only make a request if manifest has not been set (is false) + return $this->manifest !== false ? $this->manifest : $this->getManifestHeader(); + } public function setLastModified($lastModified) { @@ -327,10 +355,11 @@ public function update($params = array()) // merge specific properties with metadata $metadata += array( - HeaderConst::CONTENT_TYPE => $this->contentType, - HeaderConst::LAST_MODIFIED => $this->lastModified, - HeaderConst::CONTENT_LENGTH => $this->contentLength, - HeaderConst::ETAG => $this->etag + HeaderConst::CONTENT_TYPE => $this->contentType, + HeaderConst::LAST_MODIFIED => $this->lastModified, + HeaderConst::CONTENT_LENGTH => $this->contentLength, + HeaderConst::ETAG => $this->etag, + HeaderConst::X_OBJECT_MANIFEST => $this->manifest ); return $this->container->uploadObject($this->name, $this->content, $metadata); @@ -354,6 +383,68 @@ public function delete($params = array()) { return $this->getService()->getClient()->delete($this->getUrl())->send(); } + + /** + * Create a symlink to another named object from this object. Requires this object to be empty. + * + * @param string $destination Path (`container/object') of other object to symlink this object to + * @return \Guzzle\Http\Message\Response The response + * @throws \OpenCloud\Common\Exceptions\NoNameError if a destination name is not provided + * @throws \OpenCloud\ObjectStore\Exception\ObjectNotEmptyException if $this is not an empty object + */ + public function createSymlinkTo($destination) + { + if (!$this->name) { + throw new Exceptions\NoNameError(Lang::translate('Object has no name')); + } + + if ($this->getContentLength()) { + throw new ObjectNotEmptyException($this->getContainer()->getName() . '/' . $this->getName()); + } + + $response = $this->getService() + ->getClient() + ->createRequest('PUT', $this->getUrl(), array( + HeaderConst::X_OBJECT_MANIFEST => (string) $destination + )) + ->send(); + + if ($response->getStatusCode() == 201) { + $this->setManifest($source); + } + + return $response; + } + + /** + * Create a symlink to this object from another named object. Requires the other object to either not exist or be empty. + * + * @param string $source Path (`container/object') of other object to symlink this object from + * @return DataObject The symlinked object + * @throws \OpenCloud\Common\Exceptions\NoNameError if a source name is not provided + * @throws \OpenCloud\ObjectStore\Exception\ObjectNotEmptyException if object already exists and is not empty + */ + public function createSymlinkFrom($source) + { + if (!strlen($source)) { + throw new Exceptions\NoNameError(Lang::translate('Object has no name')); + } + + // Use ltrim to remove leading slash from source + list($containerName, $resourceName) = explode("/", ltrim($source, '/'), 2); + $container = $this->getService()->getContainer($containerName); + + if ($container->objectExists($resourceName)) { + $object = $container->getPartialObject($source); + if ($object->getContentLength() > 0) { + throw new ObjectNotEmptyException($source); + } + } + + return $container->uploadObject($resourceName, 'data', array( + HeaderConst::X_OBJECT_MANIFEST => (string) $this->getUrl() + )); + } /** * Get a temporary URL for this object. @@ -449,4 +540,21 @@ protected static function headerIsValidMetadata($header) return preg_match($pattern, $header); } + + /** + * @return null|string + */ + protected function getManifestHeader() + { + $response = $this->getService() + ->getClient() + ->head($this->getUrl()) + ->send(); + + $manifest = $response->getHeader(HeaderConst::X_OBJECT_MANIFEST); + + $this->setManifest($manifest); + + return $manifest; + } } diff --git a/lib/OpenCloud/ObjectStore/Service.php b/lib/OpenCloud/ObjectStore/Service.php index f0e1c3dc9..babd764dd 100644 --- a/lib/OpenCloud/ObjectStore/Service.php +++ b/lib/OpenCloud/ObjectStore/Service.php @@ -59,7 +59,9 @@ public function __construct(Client $client, $type = null, $name = null, $region } /** - * @return CDNService + * Return the CDN version of the ObjectStore service. + * + * @return CDNService CDN version of the ObjectStore service */ public function getCdnService() { @@ -69,8 +71,12 @@ public function getCdnService() /** * List all available containers. * - * @param array $filter - * @return \OpenCloud\Common\Collection\PaginatedIterator + * @param array $filter Array of filter options such as: + * + * * `limit`: number of results to limit the list to. Optional. + * * `marker`: name of container after which to start the list. Optional. + * * `end_marker`: name of container before which to end the list. Optional. + * @return \OpenCloud\Common\Collection\PaginatedIterator Iterator to list of containers */ public function listContainers(array $filter = array()) { @@ -79,8 +85,10 @@ public function listContainers(array $filter = array()) } /** - * @param $data - * @return Container + * Return a new or existing (if name is specified) container. + * + * @param \stdClass $data Data to initialize container. Optional. + * @return Container Container */ public function getContainer($data = null) { @@ -90,9 +98,9 @@ public function getContainer($data = null) /** * Create a container for this service. * - * @param $name The name of the container + * @param string $name The name of the container * @param array $metadata Additional (optional) metadata to associate with the container - * @return bool|static + * @return bool|Container Newly-created Container upon success; false, otherwise */ public function createContainer($name, array $metadata = array()) { @@ -114,9 +122,9 @@ public function createContainer($name, array $metadata = array()) /** * Check the validity of a potential container name. * - * @param $name - * @return bool - * @throws \OpenCloud\Common\Exceptions\InvalidArgumentError + * @param string $name Name of container + * @return bool True if container name is valid + * @throws \OpenCloud\Common\Exceptions\InvalidArgumentError if container name is invalid */ public function checkContainerName($name) { @@ -145,12 +153,12 @@ public function checkContainerName($name) * will be ignored. You can create up to 1,000 new containers per extraction request. Also note that only regular * files will be uploaded. Empty directories, symlinks, and so on, will not be uploaded. * - * @param $path The path to the archive being extracted - * @param $archive The contents of the archive (either string or stream) + * @param string $path The path to the archive being extracted + * @param string|stream $archive The contents of the archive (either string or stream) * @param string $archiveType The type of archive you're using {@see \OpenCloud\ObjectStore\Constants\UrlType} - * @return \Guzzle\Http\Message\Response - * @throws Exception\BulkOperationException - * @throws \OpenCloud\Common\Exceptions\InvalidArgumentError + * @return \Guzzle\Http\Message\Response HTTP response from API + * @throws \OpenCloud\Common\Exceptions\InvalidArgumentError if specifed `$archiveType` is invalid + * @throws Exception\BulkOperationException if there are errors with the bulk extract */ public function bulkExtract($path = '', $archive, $archiveType = UrlType::TAR_GZ) { @@ -199,17 +207,17 @@ public function bulkDelete(array $paths) * and sent as individual requests. * * @param array $paths The objects you want to delete. Each path needs - * be formatted as /{containerName}/{objectName}. If - * you are deleting object_1 and object_2 from the - * photos_container, the array will be: + * be formatted as `/{containerName}/{objectName}`. If + * you are deleting `object_1` and `object_2` from the + * `photos_container`, the array will be: * * array( * '/photos_container/object_1', * '/photos_container/object_2' * ) * - * @return array The array of responses from the API - * @throws Exception\BulkOperationException + * @return array[Guzzle\Http\Message\Response] HTTP responses from the API + * @throws Exception\BulkOperationException if the bulk delete operation fails */ public function batchDelete(array $paths) { @@ -257,7 +265,12 @@ private function executeBatchDeleteRequest(array $paths) * * @param Container $old Where you're moving files from * @param Container $new Where you're moving files to - * @return array Of PUT responses + * @param array $options Options to configure the migration. Optional. Available options are: + * + * * `read.batchLimit`: Number of files to read at a time from `$old` container. Optional; default = 1000. + * * `write.batchLimit`: Number of files to write at a time to `$new` container. Optional; default = 1000. + * * `read.pageLimit`: Number of filenames to read at a time from `$old` container. Optional; default = 10000. + * @return array[Guzzle\Http\Message\Response] HTTP responses from the API */ public function migrateContainer(Container $old, Container $new, array $options = array()) { diff --git a/lib/OpenCloud/Version.php b/lib/OpenCloud/Version.php index 47aafabf1..b38917918 100644 --- a/lib/OpenCloud/Version.php +++ b/lib/OpenCloud/Version.php @@ -27,7 +27,7 @@ */ class Version { - const VERSION = '1.12.2'; + const VERSION = '1.13.0'; /** * @return string Indicate current SDK version. diff --git a/samples/Compute/create_server_with_availability_zone.php b/samples/Compute/create_server_with_availability_zone.php new file mode 100644 index 000000000..cf4b1f4bc --- /dev/null +++ b/samples/Compute/create_server_with_availability_zone.php @@ -0,0 +1,48 @@ + '{username}', + 'password' => '{password}', +)); + +// 2. Create Compute service +$service = $client->computeService('nova', '{region}'); + +// 3. Get empty server +$server = $service->server(); + +// 4. Create the server. If you do not know what imageId or flavorId to use, +// please run the list_flavors.php and list_images.php scripts. +try { + $response = $server->create(array( + 'name' => '{serverName}', + 'imageId' => '{imageId}', + 'flavorId' => '{flavorId}', + 'availabilityZone' => '{availabilityZone}' + )); +} catch (BadResponseException $e) { + echo $e->getResponse(); +} diff --git a/samples/ObjectStore/list-objects-over-10000.php b/samples/ObjectStore/list-objects-over-10000.php new file mode 100644 index 000000000..ec349e88d --- /dev/null +++ b/samples/ObjectStore/list-objects-over-10000.php @@ -0,0 +1,62 @@ + '{username}', + 'apiKey' => '{apiKey}', +)); + +// 2. Obtain an Object Store service object from the client. +$objectStoreService = $client->objectStoreService(null, '{region}'); + +// 3. Get container. +$container = $objectStoreService->getContainer('{containerName}'); + +// 4. Get the list of objects +$objects = $container->objectList(); + +// 5. Create a list of all objects in the container +$containerObjects = array(); +$marker = ''; + +while ($marker !== null) { + $params = array( + 'marker' => $marker, + ); + + $objects = $container->objectList($params); + $total = $objects->count(); + $count = 0; + + if ($total == 0) { + break; + } + + foreach ($objects as $object) { + /** @var $object OpenCloud\ObjectStore\Resource\DataObject **/ + $containerObjects[] = $object->getName(); + $count++; + + $marker = ($count == $total) ? $object->getName() : null; + } +} diff --git a/samples/ObjectStore/symlink-object.php b/samples/ObjectStore/symlink-object.php new file mode 100644 index 000000000..f5c4bc129 --- /dev/null +++ b/samples/ObjectStore/symlink-object.php @@ -0,0 +1,45 @@ + '{username}', + 'apiKey' => '{apiKey}', +)); + +// 2. Obtain an Object Store service object from the client. +$objectStoreService = $client->objectStoreService(null, '{region}'); + +// 3. Get container. +$container = $objectStoreService->getContainer('{sourceContainerName}'); + +// 4. Get object. +$object = $container->getObject('{objectName}'); + +// 5. Create another container for object's copy. +$objectStoreService->createContainer('{destinationContainerName}'); + +// 6. Symlink to object from another container object. {objectName} must either not exist or be an empty file. +$object->createSymlinkFrom('{destinationContainerName}/{objectName}'); + +// 7. Symlink from object to another container object. $object must be an empty file. +$object->createSymlinkTo('{destinationContainerName}/{objectName}'); diff --git a/tests/OpenCloud/Tests/Compute/Resource/ServerTest.php b/tests/OpenCloud/Tests/Compute/Resource/ServerTest.php index 580e77e65..13ed80a35 100644 --- a/tests/OpenCloud/Tests/Compute/Resource/ServerTest.php +++ b/tests/OpenCloud/Tests/Compute/Resource/ServerTest.php @@ -403,4 +403,13 @@ public function test_Stop() $this->addMockSubscriber(new \Guzzle\Http\Message\Response(202)); $this->assertEquals(202, $this->server->stop()->getStatusCode()); } + + public function test_Create_Availability_Zone() + { + $new = new PublicServer($this->service); + $new->setAvailabilityZone('AZ1'); + $obj = $new->CreateJson(); + + $this->assertEquals('AZ1', $obj->server->availability_zone); + } } diff --git a/tests/OpenCloud/Tests/Compute/_response/Extensions.resp b/tests/OpenCloud/Tests/Compute/_response/Extensions.resp index 68c63da10..8474a182c 100644 --- a/tests/OpenCloud/Tests/Compute/_response/Extensions.resp +++ b/tests/OpenCloud/Tests/Compute/_response/Extensions.resp @@ -15,6 +15,14 @@ Server: Jetty(8.0.y.z-SNAPSHOT) "alias": "OS-DCF", "description": "Disk Management Extension." }, + { + "updated": "2013-01-30T00:00:00Z", + "name": "ExtendedAvailabilityZone", + "links": [], + "namespace": "http://docs.openstack.org/compute/ext/extended_availability_zone/api/v2", + "alias": "OS-EXT-AZ", + "description": "Extended Availability Zone support." + }, { "updated": "2013-02-19T00:00:00Z", "name": "ImageSize", diff --git a/tests/OpenCloud/Tests/DNS/ServiceTest.php b/tests/OpenCloud/Tests/DNS/ServiceTest.php index 73da7403e..9503e0409 100644 --- a/tests/OpenCloud/Tests/DNS/ServiceTest.php +++ b/tests/OpenCloud/Tests/DNS/ServiceTest.php @@ -40,6 +40,24 @@ public function testDomain() $this->assertInstanceOf('OpenCloud\DNS\Resource\Domain', $this->service->domain()); } + public function testDomainByName() + { + $this->addMockSubscriber($this->makeResponse('{"domains":[{"name":"region2.example.net","id":2725352,"updated":"2011-06-23T20:21:06.000+0000","created":"2011-06-23T19:24:27.000+0000"}],"totalEntries":114}', 200)); + $domain = $this->service->domainByName("region2.example.net"); + + $this->assertInstanceOf('OpenCloud\DNS\Resource\Domain', $domain); + $this->assertEquals("region2.example.net", $domain->getName()); + } + + /** + * @expectedException OpenCloud\Common\Exceptions\DomainNotFoundException + */ + public function testDomainByNameWhenDomainNotFound() + { + $this->addMockSubscriber($this->makeResponse('{"domains":[],"totalEntries":114}', 200)); + $domain = $this->service->domainByName("region2.example.net"); + } + /** * @mockFile Domain_List */ diff --git a/tests/OpenCloud/Tests/ObjectStore/ObjectStoreTestCase.php b/tests/OpenCloud/Tests/ObjectStore/ObjectStoreTestCase.php index 985f7830c..3c3c995c4 100644 --- a/tests/OpenCloud/Tests/ObjectStore/ObjectStoreTestCase.php +++ b/tests/OpenCloud/Tests/ObjectStore/ObjectStoreTestCase.php @@ -42,7 +42,12 @@ public function setupObjects() $this->addMockSubscriber($response1); - $response2 = new Response(204, array( + $this->container = $this->service->getContainer('foo'); + } + + protected function setupCdnContainerMockResponse() + { + $response = new Response(204, array( 'X-Cdn-Ssl-Uri' => 'https://83c49b9a2f7ad18250b3-346eb45fd42c58ca13011d659bfc1ac1.ssl.cf0.rackcdn.com', 'X-Ttl' => '259200', 'X-Cdn-Uri' => 'http://081e40d3ee1cec5f77bf-346eb45fd42c58ca13011d659bfc1ac1.r49.cf0.rackcdn.com', @@ -52,8 +57,6 @@ public function setupObjects() 'X-Trans-Id' => 'tx82a6752e00424edb9c46fa2573132e2c' )); - $this->addMockSubscriber($response2); - - $this->container = $this->service->getContainer('foo'); + $this->addMockSubscriber($response); } } diff --git a/tests/OpenCloud/Tests/ObjectStore/Resource/ContainerTest.php b/tests/OpenCloud/Tests/ObjectStore/Resource/ContainerTest.php index b5d2bc602..c5b191249 100644 --- a/tests/OpenCloud/Tests/ObjectStore/Resource/ContainerTest.php +++ b/tests/OpenCloud/Tests/ObjectStore/Resource/ContainerTest.php @@ -51,6 +51,8 @@ public function test_Get_Container() $this->assertEquals('3846773', $container->getBytesUsed()); $this->assertFalse($container->hasLogRetention()); + $this->setupCdnContainerMockResponse(); + $cdn = $container->getCdn(); $this->assertInstanceOf('OpenCloud\ObjectStore\Resource\CDNContainer', $cdn); $this->assertEquals('tx82a6752e00424edb9c46fa2573132e2c', $cdn->getTransId()); @@ -115,13 +117,13 @@ public function test_Delete_NonEmpty_Container() $this->addMockSubscriber($this->makeResponse('[]', 409)); $container->delete(); } + public function test_Object_List() { $container = $this->container; $this->addMockSubscriber($this->makeResponse('[{"name":"test_obj_1","hash":"4281c348eaf83e70ddce0e07221c3d28","bytes":14,"content_type":"application\/octet-stream","last_modified":"2009-02-03T05:26:32.612278"},{"name":"test_obj_2","hash":"b039efe731ad111bc1b0ef221c3849d0","bytes":64,"content_type":"application\/octet-stream","last_modified":"2009-02-03T05:26:32.612278"}]', 200)); - $list = $container->objectList(); $this->assertInstanceOf(self::COLLECTION_CLASS, $list); $this->assertEquals('test_obj_1', $list->first()->getName()); @@ -141,6 +143,8 @@ public function test_Misc_Operations() $container->disableLogging() ); + $this->setupCdnContainerMockResponse(); + $this->assertInstanceOf( 'Guzzle\Http\Message\Response', $container->getCdn()->setStaticIndexPage('index.html') diff --git a/tests/OpenCloud/Tests/ObjectStore/Resource/DataObjectTest.php b/tests/OpenCloud/Tests/ObjectStore/Resource/DataObjectTest.php index 8193c267d..4bebaf346 100644 --- a/tests/OpenCloud/Tests/ObjectStore/Resource/DataObjectTest.php +++ b/tests/OpenCloud/Tests/ObjectStore/Resource/DataObjectTest.php @@ -17,7 +17,11 @@ namespace OpenCloud\Tests\ObjectStore\Resource; +use Guzzle\Http\Message\Response; +use OpenCloud\Common\Constants\Header; use OpenCloud\ObjectStore\Constants\UrlType; +use OpenCloud\ObjectStore\Exception\ObjectNotEmptyException; +use OpenCloud\Tests\MockSubscriber; use OpenCloud\Tests\ObjectStore\ObjectStoreTestCase; class DataObjectTest extends ObjectStoreTestCase @@ -86,6 +90,7 @@ public function test_Temp_Url_Fails_With_Incorrect_Method() public function test_Purge() { $object = $this->container->dataObject('foobar'); + $this->setupCdnContainerMockResponse(); $this->assertInstanceOf( 'Guzzle\Http\Message\Response', $object->purge('test@example.com') @@ -96,9 +101,88 @@ public function test_Public_Urls() { $object = $this->container->dataObject('foobar'); + $this->setupCdnContainerMockResponse(); $this->assertNotNull($object->getPublicUrl()); $this->assertNotNull($object->getPublicUrl(UrlType::SSL)); $this->assertNotNull($object->getPublicUrl(UrlType::STREAMING)); $this->assertNotNull($object->getPublicUrl(UrlType::IOS_STREAMING)); } + + public function test_Symlink_To() + { + $targetName = 'new_container/new_object'; + $this->addMockSubscriber(new Response(200, array(Header::X_OBJECT_MANIFEST => $targetName))); + $object = $this->container->dataObject('foobar'); + $this->assertInstanceOf('Guzzle\Http\Message\Response', $object->createSymlinkTo($targetName)); + $this->assertEquals($targetName, $object->getManifest()); + } + + /** + * @expectedException OpenCloud\Common\Exceptions\NoNameError + */ + public function test_Symlink_To_Fails_With_NoName() + { + $object = $this->container->dataObject()->createSymlinkTo(null); + } + + /** + * @expectedException OpenCloud\ObjectStore\Exception\ObjectNotEmptyException + */ + public function test_Symlink_To_Fails_With_NotEmpty() + { + $this->addMockSubscriber(new Response(200, array(Header::CONTENT_LENGTH => 100))); + $object = $this->container->dataObject('foobar')->createSymlinkTo('new_container/new_object'); + } + + public function test_Symlink_From() + { + $symlinkName = 'new_container/new_object'; + + // We have to fill the mock response queue to properly get the correct X-Object-Manifest header + // Container\dataObject( ) + // - Container\refresh( ) + $this->addMockSubscriber(new Response(200)); + // DataObject\createSymlinkFrom( ) + // - Container\createRefreshRequest( ) + $this->addMockSubscriber(new Response(200)); + // - CDNContainer\createRefreshRequest( ) + $this->addMockSubscriber(new Response(200)); + // - Container\objectExists( ) + $this->addMockSubscriber(new Response(200)); + // - Container\getPartialObject( ) + $this->addMockSubscriber(new Response(200)); + // - Container\uploadObject( ) + $this->addMockSubscriber(new Response(200, array(Header::X_OBJECT_MANIFEST => $symlinkName))); + + $object = $this->container->dataObject('foobar')->createSymlinkFrom($symlinkName); + $this->assertInstanceOf('OpenCloud\ObjectStore\Resource\DataObject', $object); + } + + /** + * @expectedException OpenCloud\Common\Exceptions\NoNameError + */ + public function test_Symlink_From_Fails_With_NoName() + { + $object = $this->container->dataObject()->createSymlinkFrom(null); + } + + /** + * @expectedException OpenCloud\ObjectStore\Exception\ObjectNotEmptyException + */ + public function test_Symlink_From_Fails_With_NotEmpty() + { + // We have to fill the mock response queue to properly get the correct Content-Length header + // Container\dataObject( ) + // - Container\refresh( ) + $this->addMockSubscriber(new Response(200)); + // DataObject\createSymlinkFrom( ) + // - Container\createRefreshRequest( ) + $this->addMockSubscriber(new Response(200)); + // - Container\objectExists( ) + $this->addMockSubscriber(new Response(200)); + // - Container\getPartialObject( ) + $this->addMockSubscriber(new Response(200, array(Header::CONTENT_LENGTH => 100))); + + $object = $this->container->dataObject('foobar')->createSymlinkFrom('new_container/new_object'); + } }