From f0fbce0ad750aa25147d6b38b9dab5185e644c64 Mon Sep 17 00:00:00 2001 From: Mark Challoner Date: Wed, 18 Mar 2015 14:50:43 +0000 Subject: [PATCH 1/6] Added symlink functionality via X-Object-Manifest header management --- lib/OpenCloud/Common/Constants/Header.php | 1 + .../ObjectStore/Resource/DataObject.php | 75 +++++++++++++++++-- .../ObjectStore/Resource/DataObjectTest.php | 17 +++++ 3 files changed, 88 insertions(+), 5 deletions(-) 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/ObjectStore/Resource/DataObject.php b/lib/OpenCloud/ObjectStore/Resource/DataObject.php index 5877431b8..ca74e4d66 100644 --- a/lib/OpenCloud/ObjectStore/Resource/DataObject.php +++ b/lib/OpenCloud/ObjectStore/Resource/DataObject.php @@ -76,6 +76,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 +144,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 +300,26 @@ public function getEtag() { return $this->etag ? : $this->content->getContentMd5(); } + + /** + * @param $manifest + * @return $this + */ + public function setManifest($manifest) + { + $this->manifest = $manifest; + + return $this; + } + + /** + * @return null|string + */ + 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 +354,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 +382,26 @@ public function delete($params = array()) { return $this->getService()->getClient()->delete($this->getUrl())->send(); } + + /** + * @param string $source Path (`container/object') of other object to symlink this object to + * @return \Guzzle\Http\Message\Response + */ + public function symlink($source) + { + $response = $this->getService() + ->getClient() + ->createRequest('PUT', $this->getUrl(), array( + HeaderConst::X_OBJECT_MANIFEST => (string) $source + )) + ->send(); + + if ($response->getStatusCode() == 201) { + $this->setManifest($source); + } + + return $response; + } /** * Get a temporary URL for this object. @@ -449,4 +497,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/tests/OpenCloud/Tests/ObjectStore/Resource/DataObjectTest.php b/tests/OpenCloud/Tests/ObjectStore/Resource/DataObjectTest.php index 8193c267d..fc67ad35d 100644 --- a/tests/OpenCloud/Tests/ObjectStore/Resource/DataObjectTest.php +++ b/tests/OpenCloud/Tests/ObjectStore/Resource/DataObjectTest.php @@ -101,4 +101,21 @@ public function test_Public_Urls() $this->assertNotNull($object->getPublicUrl(UrlType::STREAMING)); $this->assertNotNull($object->getPublicUrl(UrlType::IOS_STREAMING)); } + + public function test_Symlink() + { + $object = $this->container->dataObject('foobar'); + $this->assertInstanceOf( + 'Guzzle\Http\Message\Response', + $object->symlink('/new_container/new_object') + ); + } + + /** + * @expectedException OpenCloud\Common\Exceptions\NoNameError + */ + public function test_Symlink_Fails() + { + $this->container->dataObject()->symlink(null); + } } From 6ce77ce5adbdad7bfff439322c3400795ab13cfa Mon Sep 17 00:00:00 2001 From: Mark Challoner Date: Thu, 19 Mar 2015 18:28:25 +0000 Subject: [PATCH 2/6] Added createSymlinkFrom. Renamed symlink to createSymlinkTo. Made setManifest protected. Updated PHPDoc and tests. --- .../ObjectStore/Resource/DataObject.php | 69 ++++++++++++++----- .../ObjectStore/Resource/DataObjectTest.php | 34 ++++++--- 2 files changed, 78 insertions(+), 25 deletions(-) diff --git a/lib/OpenCloud/ObjectStore/Resource/DataObject.php b/lib/OpenCloud/ObjectStore/Resource/DataObject.php index ca74e4d66..ace797bd3 100644 --- a/lib/OpenCloud/ObjectStore/Resource/DataObject.php +++ b/lib/OpenCloud/ObjectStore/Resource/DataObject.php @@ -302,10 +302,10 @@ public function getEtag() } /** - * @param $manifest + * @param string $manifest Path (`container/object') to set as the value to X-Object-Manifest * @return $this */ - public function setManifest($manifest) + protected function setManifest($manifest) { $this->manifest = $manifest; @@ -313,7 +313,7 @@ public function setManifest($manifest) } /** - * @return null|string + * @return null|string Path (`container/object') from X-Object-Manifest header or null if the header does not exist */ public function getManifest() { @@ -384,23 +384,60 @@ public function delete($params = array()) } /** - * @param string $source Path (`container/object') of other object to symlink this object to - * @return \Guzzle\Http\Message\Response + * @param string $destination Path (`container/object') of other object to symlink this object to + * @return null|\Guzzle\Http\Message\Response The response or null if $this is not empty */ - public function symlink($source) + public function createSymlinkTo($destination) { - $response = $this->getService() - ->getClient() - ->createRequest('PUT', $this->getUrl(), array( - HeaderConst::X_OBJECT_MANIFEST => (string) $source - )) - ->send(); - - if ($response->getStatusCode() == 201) { - $this->setManifest($source); + if (!$this->name) { + throw new Exceptions\NoNameError(Lang::translate('Object has no name')); + } + + if ($this->getContentLength() == 0) { + $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; + } + + return null; + } + + /** + * @param string $source Path (`container/object') of other object to symlink this object from + * @return null|DataObject The symlinked object or null 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 ($unsafe = $container->objectExists($resourceName)) { + $object = $container->getPartialObject($source); + $unsafe = $object->getContentLength() > 0; + } + + if (!$unsafe) { + return $container->uploadObject($resourceName, 'data', array( + HeaderConst::X_OBJECT_MANIFEST => (string) $this->getUrl() + )); } - return $response; + return null; } /** diff --git a/tests/OpenCloud/Tests/ObjectStore/Resource/DataObjectTest.php b/tests/OpenCloud/Tests/ObjectStore/Resource/DataObjectTest.php index fc67ad35d..bac0ebebf 100644 --- a/tests/OpenCloud/Tests/ObjectStore/Resource/DataObjectTest.php +++ b/tests/OpenCloud/Tests/ObjectStore/Resource/DataObjectTest.php @@ -101,21 +101,37 @@ public function test_Public_Urls() $this->assertNotNull($object->getPublicUrl(UrlType::STREAMING)); $this->assertNotNull($object->getPublicUrl(UrlType::IOS_STREAMING)); } - - public function test_Symlink() + + public function test_Symlink_To() { $object = $this->container->dataObject('foobar'); - $this->assertInstanceOf( - 'Guzzle\Http\Message\Response', - $object->symlink('/new_container/new_object') - ); + $this->assertInstanceOf('Guzzle\Http\Message\Response', $object->createSymlinkTo('new_container/new_object')); + // @todo getManifest should return the manifest not null + //$this->assertEquals('new_container/new_object', $object->getManifest()); } - + + /** + * @expectedException OpenCloud\Common\Exceptions\NoNameError + */ + public function test_Symlink_To_Fails() + { + $object = $this->container->dataObject()->createSymlinkTo(null); + } + + public function test_Symlink_From() + { + $object = $this->container->dataObject('foobar'); + $symlink = $object->createSymlinkFrom('new_container/new_object'); + $this->assertInstanceOf('OpenCloud\ObjectStore\Resource\DataObject', $symlink); + // @todo getManifest should return the manifest not null + //$this->assertEquals('new_container/new_object', $symlink->getManifest()); + } + /** * @expectedException OpenCloud\Common\Exceptions\NoNameError */ - public function test_Symlink_Fails() + public function test_Symlink_From_Fails() { - $this->container->dataObject()->symlink(null); + $object = $this->container->dataObject()->createSymlinkFrom(null); } } From ceebd95ec570c671dc4dbe9e80eed64a38283a23 Mon Sep 17 00:00:00 2001 From: Mark Challoner Date: Fri, 20 Mar 2015 15:51:08 +0000 Subject: [PATCH 3/6] Added getManifest assertions to symlink tests --- .../ObjectStore/Resource/DataObjectTest.php | 36 ++++++++++++++----- 1 file changed, 28 insertions(+), 8 deletions(-) diff --git a/tests/OpenCloud/Tests/ObjectStore/Resource/DataObjectTest.php b/tests/OpenCloud/Tests/ObjectStore/Resource/DataObjectTest.php index bac0ebebf..c7859649e 100644 --- a/tests/OpenCloud/Tests/ObjectStore/Resource/DataObjectTest.php +++ b/tests/OpenCloud/Tests/ObjectStore/Resource/DataObjectTest.php @@ -17,7 +17,10 @@ namespace OpenCloud\Tests\ObjectStore\Resource; +use Guzzle\Http\Message\Response; +use OpenCloud\Common\Constants\Header; use OpenCloud\ObjectStore\Constants\UrlType; +use OpenCloud\Tests\MockSubscriber; use OpenCloud\Tests\ObjectStore\ObjectStoreTestCase; class DataObjectTest extends ObjectStoreTestCase @@ -104,10 +107,11 @@ public function test_Public_Urls() 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('new_container/new_object')); - // @todo getManifest should return the manifest not null - //$this->assertEquals('new_container/new_object', $object->getManifest()); + $this->assertInstanceOf('Guzzle\Http\Message\Response', $object->createSymlinkTo($targetName)); + $this->assertEquals($targetName, $object->getManifest()); } /** @@ -120,11 +124,27 @@ public function test_Symlink_To_Fails() public function test_Symlink_From() { - $object = $this->container->dataObject('foobar'); - $symlink = $object->createSymlinkFrom('new_container/new_object'); - $this->assertInstanceOf('OpenCloud\ObjectStore\Resource\DataObject', $symlink); - // @todo getManifest should return the manifest not null - //$this->assertEquals('new_container/new_object', $symlink->getManifest()); + $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); + $this->assertEquals($symlinkName, $object->getManifest()); } /** From a5cba84be3c7362d74a3d93b97076f594ae92f36 Mon Sep 17 00:00:00 2001 From: Mark Challoner Date: Fri, 20 Mar 2015 17:25:06 +0000 Subject: [PATCH 4/6] Changed symlink failures from null to exceptions. Added tests. --- .../Exception/ObjectNotEmptyException.php | 35 ++++++++++++++++ .../ObjectStore/Resource/DataObject.php | 42 +++++++++---------- .../ObjectStore/Resource/DataObjectTest.php | 37 ++++++++++++++-- 3 files changed, 89 insertions(+), 25 deletions(-) create mode 100644 lib/OpenCloud/ObjectStore/Exception/ObjectNotEmptyException.php 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/DataObject.php b/lib/OpenCloud/ObjectStore/Resource/DataObject.php index ace797bd3..12ab65d8b 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 @@ -393,22 +394,22 @@ public function createSymlinkTo($destination) throw new Exceptions\NoNameError(Lang::translate('Object has no name')); } - if ($this->getContentLength() == 0) { - $response = $this->getService() - ->getClient() - ->createRequest('PUT', $this->getUrl(), array( - HeaderConst::X_OBJECT_MANIFEST => (string) $destination - )) - ->send(); + if ($this->getContentLength()) { + throw new ObjectNotEmptyException($this->getContainer()->getName() . '/' . $this->getName()); + } - if ($response->getStatusCode() == 201) { - $this->setManifest($source); - } + $response = $this->getService() + ->getClient() + ->createRequest('PUT', $this->getUrl(), array( + HeaderConst::X_OBJECT_MANIFEST => (string) $destination + )) + ->send(); - return $response; + if ($response->getStatusCode() == 201) { + $this->setManifest($source); } - return null; + return $response; } /** @@ -423,21 +424,18 @@ public function createSymlinkFrom($source) // Use ltrim to remove leading slash from source list($containerName, $resourceName) = explode("/", ltrim($source, '/'), 2); - $container = $this->getService()->getContainer($containerName); - if ($unsafe = $container->objectExists($resourceName)) { + if ($container->objectExists($resourceName)) { $object = $container->getPartialObject($source); - $unsafe = $object->getContentLength() > 0; - } - - if (!$unsafe) { - return $container->uploadObject($resourceName, 'data', array( - HeaderConst::X_OBJECT_MANIFEST => (string) $this->getUrl() - )); + if ($object->getContentLength() > 0) { + throw new ObjectNotEmptyException($source); + } } - return null; + return $container->uploadObject($resourceName, 'data', array( + HeaderConst::X_OBJECT_MANIFEST => (string) $this->getUrl() + )); } /** diff --git a/tests/OpenCloud/Tests/ObjectStore/Resource/DataObjectTest.php b/tests/OpenCloud/Tests/ObjectStore/Resource/DataObjectTest.php index c7859649e..97af383a9 100644 --- a/tests/OpenCloud/Tests/ObjectStore/Resource/DataObjectTest.php +++ b/tests/OpenCloud/Tests/ObjectStore/Resource/DataObjectTest.php @@ -20,6 +20,7 @@ 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; @@ -117,11 +118,20 @@ public function test_Symlink_To() /** * @expectedException OpenCloud\Common\Exceptions\NoNameError */ - public function test_Symlink_To_Fails() + 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'; @@ -144,14 +154,35 @@ public function test_Symlink_From() $object = $this->container->dataObject('foobar')->createSymlinkFrom($symlinkName); $this->assertInstanceOf('OpenCloud\ObjectStore\Resource\DataObject', $object); - $this->assertEquals($symlinkName, $object->getManifest()); } /** * @expectedException OpenCloud\Common\Exceptions\NoNameError */ - public function test_Symlink_From_Fails() + 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'); + } } From f3a5658551004e0e88385ca48e9fdeddb0497c40 Mon Sep 17 00:00:00 2001 From: Mark Challoner Date: Fri, 20 Mar 2015 17:38:32 +0000 Subject: [PATCH 5/6] Added documentation --- doc/services/object-store/objects.rst | 35 ++++++++++++++++++++ samples/ObjectStore/symlink-object.php | 45 ++++++++++++++++++++++++++ 2 files changed, 80 insertions(+) create mode 100644 samples/ObjectStore/symlink-object.php diff --git a/doc/services/object-store/objects.rst b/doc/services/object-store/objects.rst index 0eb0ea836..107d7c584 100644 --- a/doc/services/object-store/objects.rst +++ b/doc/services/object-store/objects.rst @@ -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 `_ +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/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}'); From 9958eb9b61dcff38b3a12e638451512539751ca7 Mon Sep 17 00:00:00 2001 From: Mark Challoner Date: Mon, 23 Mar 2015 15:09:51 +0000 Subject: [PATCH 6/6] Updated phpdoc for createSymlink functions --- lib/OpenCloud/ObjectStore/Resource/DataObject.php | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/lib/OpenCloud/ObjectStore/Resource/DataObject.php b/lib/OpenCloud/ObjectStore/Resource/DataObject.php index 12ab65d8b..de302b4cd 100644 --- a/lib/OpenCloud/ObjectStore/Resource/DataObject.php +++ b/lib/OpenCloud/ObjectStore/Resource/DataObject.php @@ -385,8 +385,12 @@ public function delete($params = array()) } /** + * 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 null|\Guzzle\Http\Message\Response The response or null if $this is not empty + * @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) { @@ -413,8 +417,12 @@ public function createSymlinkTo($destination) } /** + * 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 null|DataObject The symlinked object or null if object already exists and is not empty + * @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) {