Large overhaul to allow two forms of object fetching.
This commit is contained in:
		| @@ -243,6 +243,82 @@ class Container implements \Countable, \IteratorAggregate { | ||||
|     return TRUE; | ||||
|   } | ||||
|  | ||||
|   /** | ||||
|    * Get the object with the given name. | ||||
|    * | ||||
|    * This fetches a single object with the given name. | ||||
|    * | ||||
|    * This does not yet support the following features of Swift: | ||||
|    * | ||||
|    * - Byte range queries. | ||||
|    * - If-Modified-Since/If-Unmodified-Since | ||||
|    * - If-Match/If-None-Match | ||||
|    * | ||||
|    */ | ||||
|   public function object($name) { | ||||
|  | ||||
|     $headers = array(); | ||||
|  | ||||
|     // Auth token. | ||||
|     $headers['X-Auth-Token'] = $this->token; | ||||
|  | ||||
|     $client = \HPCloud\Transport::instance(); | ||||
|     $response = $client->doRequest($url, 'GET', $headers); | ||||
|  | ||||
|     if ($response->status() != 200) { | ||||
|       throw new \HPCloud\Exception('An unknown error occurred while saving: ' . $response->status()); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   /** | ||||
|    * 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 \HPCloud\Storage\ObjectStorage\RemoteObject | ||||
|    *   A remote object ready for use. | ||||
|    */ | ||||
|   public function remoteObject($name) { | ||||
|     $url = $this->url . '/' . urlencode($name); | ||||
|     $headers = array( | ||||
|       'X-Auth-Token' => $this->token, | ||||
|     ); | ||||
|  | ||||
|  | ||||
|     $client = \HPCloud\Transport::instance(); | ||||
|     $response = $client->doRequest($url, 'HEAD', $headers); | ||||
|  | ||||
|     if ($response->status() != 200) { | ||||
|       throw new \HPCloud\Exception('An unknown error occurred while saving: ' . $response->status()); | ||||
|     } | ||||
|  | ||||
|     $headers = $response->headers(); | ||||
|  | ||||
|     $objectUrl = $this->url . '/' . urlencode($name); | ||||
|     return RemoteObject::newFromHeaders($name, $headers, $this->token, $objectUrl); | ||||
|   } | ||||
|  | ||||
|   /** | ||||
|    * Get a list of objects in this container. | ||||
|    * | ||||
| @@ -284,7 +360,7 @@ class Container implements \Countable, \IteratorAggregate { | ||||
|    * 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  | ||||
|    * (Directory-like behavior is also supported by using "directory | ||||
|    * markers". See objectsWithPath().) | ||||
|    * | ||||
|    * Prefixes | ||||
| @@ -341,12 +417,12 @@ class Container implements \Countable, \IteratorAggregate { | ||||
|   /** | ||||
|    * Specify a path (subdirectory) to traverse. | ||||
|    * | ||||
|    * OpenStack Swift provides two basic ways to handle directory-like  | ||||
|    * structures. The first is using a prefix (see objectsByPrefix()).  | ||||
|    * OpenStack Swift provides two basic ways to handle directory-like | ||||
|    * structures. The first is using a prefix (see objectsByPrefix()). | ||||
|    * 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.  | ||||
|    * 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. | ||||
|    * | ||||
|    * @code | ||||
|   | ||||
| @@ -93,7 +93,10 @@ class Object { | ||||
|    * OpenStack allows you to specify metadata for a file. Metadata items | ||||
|    * must follow these conventions: | ||||
|    * | ||||
|    * - names must contain only letters, numbers, and short dashes. | ||||
|    * - 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. | ||||
|    * - 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. | ||||
| @@ -101,6 +104,11 @@ class Object { | ||||
|    * This library does only minimal processing of metadata, and does no | ||||
|    * error checking, escaping, etc. This is up to the implementor. | ||||
|    * | ||||
|    * IMPORTANT: Current versions of OpenStack Swift see the names FOO, | ||||
|    * Foo, foo, and fOo as the same. This is not to say that it is case | ||||
|    * insensitive; only that it normalizes strings according to its own | ||||
|    * rules. | ||||
|    * | ||||
|    * @param array $array | ||||
|    *   An associative array of metadata names to values. | ||||
|    */ | ||||
|   | ||||
| @@ -32,9 +32,80 @@ class RemoteObject extends Object { | ||||
|     $object->etag = (string) $data['hash']; | ||||
|     $object->lastModified = strtotime($data['last_modified']); | ||||
|  | ||||
|     $object->token = $token; | ||||
|     $object->url = $url; | ||||
|  | ||||
|     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. | ||||
|    */ | ||||
|   public static function newFromHeaders($name, $headers, $token, $url) { | ||||
|     $object = new RemoteObject($name); | ||||
|  | ||||
|     //throw new \Exception(print_r($headers, TRUE)); | ||||
|  | ||||
|     $object->setContentType($headers['Content-Type']); | ||||
|     $object->contentLength = (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(self::extractHeaderAttributes($headers)); | ||||
|  | ||||
|     $object->token = $token; | ||||
|     $object->url = $url; | ||||
|  | ||||
|     return $object; | ||||
|   } | ||||
|  | ||||
|   /** | ||||
|    * 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. | ||||
|    * @return array | ||||
|    *   An associative array of name/value attribute pairs. | ||||
|    */ | ||||
|   public static function extractHeaderAttributes($headers) { | ||||
|     $attributes = array(); | ||||
|     $offset = strlen(Container::METADATA_HEADER_PREFIX); | ||||
|     foreach ($headers as $header => $value) { | ||||
|  | ||||
|       $index = strpos($header, Container::METADATA_HEADER_PREFIX); | ||||
|       if ($index === 0) { | ||||
|         $key = substr($header, $offset); | ||||
|         $attributes[$key] = $value; | ||||
|       } | ||||
|     } | ||||
|     return $attributes; | ||||
|   } | ||||
|  | ||||
|   public function contentLength() { | ||||
|     if (!empty($this->content)) { | ||||
|       return parent::contentLength(); | ||||
|   | ||||
| @@ -110,6 +110,8 @@ class ContainerTest extends \HPCloud\Tests\TestCase { | ||||
|     $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); | ||||
|     } | ||||
| @@ -121,6 +123,36 @@ class ContainerTest extends \HPCloud\Tests\TestCase { | ||||
|     $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); | ||||
|  | ||||
|     // Overwrite the copy: | ||||
|     $object->setContent('HI'); | ||||
|     $this->assertEquals('HI', $object->content()); | ||||
|   } | ||||
|  | ||||
|   /** | ||||
|    * @depends testSave | ||||
|    */ | ||||
|   | ||||
		Reference in New Issue
	
	Block a user
	 Matt Butcher
					Matt Butcher