Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 35 additions & 0 deletions doc/services/object-store/objects.rst
Original file line number Diff line number Diff line change
Expand Up @@ -326,6 +326,41 @@ the name of the object inside the container that does not exist yet.
`Get the executable PHP script for this example <https://github.com/rackspace/php-opencloud/master/samples/ObjectStore/copy-object.php>`_


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 <https://github.com/rackspace/php-opencloud/master/samples/ObjectStore/symlink-object.php>`_


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 <https://github.com/rackspace/php-opencloud/master/samples/ObjectStore/symlink-object.php>`_


Get object metadata
-------------------

Expand Down
1 change: 1 addition & 0 deletions lib/OpenCloud/Common/Constants/Header.php
Original file line number Diff line number Diff line change
Expand Up @@ -70,4 +70,5 @@ class Header
const USER_AGENT = 'User-Agent';
const VARY = 'Vary';
const VIA = 'Via';
const X_OBJECT_MANIFEST = 'X-Object-Manifest';
}
35 changes: 35 additions & 0 deletions lib/OpenCloud/ObjectStore/Exception/ObjectNotEmptyException.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
<?php
/**
* Copyright 2012-2014 Rackspace US, Inc.
*
* 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
*
* 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 OpenCloud\ObjectStore\Exception;

class ObjectNotEmptyException extends \RuntimeException
{
public static function factory($name)
{
$message = sprintf(
"%s could not be overwritten as it contains content.",
$name
);

$e = new self($message);

$e->name = $name;

return $e;
}
}
118 changes: 113 additions & 5 deletions lib/OpenCloud/ObjectStore/Resource/DataObject.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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)
{
Expand Down Expand Up @@ -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);
Expand All @@ -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);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See my comment above on throwing exceptions instead of returning null. If you use exceptions, you could refactor the code below this comment to use guard clauses (which tend to be more readable in my opinion) like so:

if ($container->objectExists($resourceName)) {
    throw new ... // appropriate exception
}

$object = $container->getPartialObject($source);
if ($object->getContentLength() > ) {
    throw new ... // appropriate exception
}

return $container->uploadObject($resourceName, 'data', array(
    HeaderConst::X_OBJECT_MANIFEST => (string) $this->getUrl()
));

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.
Expand Down Expand Up @@ -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;
}
}
45 changes: 45 additions & 0 deletions samples/ObjectStore/symlink-object.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
<?php
/**
* Copyright 2012-2014 Rackspace US, Inc.
*
* 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
*
* 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.
*/

require dirname(__DIR__) . '/../vendor/autoload.php';

use OpenCloud\Rackspace;

// 1. Instantiate a Rackspace client. You can replace {authUrl} with
// Rackspace::US_IDENTITY_ENDPOINT or similar
$client = new Rackspace('{authUrl}', array(
'username' => '{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}');
84 changes: 84 additions & 0 deletions tests/OpenCloud/Tests/ObjectStore/Resource/DataObjectTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -101,4 +105,84 @@ public function test_Public_Urls()
$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));
// - CDNContainer\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');
}
}