HPCloud-PHP  1.2.0
PHP bindings for HPCloud and OpenStack services.
 All Classes Namespaces Files Functions Variables Pages
Container.php
Go to the documentation of this file.
1 <?php
2 /* ============================================================================
3 (c) Copyright 2012 Hewlett-Packard Development Company, L.P.
4 Permission is hereby granted, free of charge, to any person obtaining a copy
5 of this software and associated documentation files (the "Software"), to deal
6 in the Software without restriction, including without limitation the rights to
7 use, copy, modify, merge,publish, distribute, sublicense, and/or sell copies of
8 the Software, and to permit persons to whom the Software is furnished to do so,
9 subject to the following conditions:
10 
11 The above copyright notice and this permission notice shall be included in all
12 copies or substantial portions of the Software.
13 
14 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
16 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
17 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
18 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
19 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
20 SOFTWARE.
21 ============================================================================ */
22 /**
23  * @file
24  *
25  * Contains the class for ObjectStorage Container objects.
26  */
27 
29 
30 /**
31  * A container in an ObjectStorage.
32  *
33  * An Object Storage instance is divided into containers, where each
34  * container can hold an arbitrary number of objects. This class
35  * describes a container, providing access to its properties and to the
36  * objects stored inside of it.
37  *
38  * Containers are iterable, which means you can iterate over a container
39  * and access each file inside of it.
40  *
41  * Typically, containers are created using ObjectStorage::createContainer().
42  * They are retrieved using ObjectStorage::container() or
43  * ObjectStorage::containers().
44  *
45  * @code
46  * <?php
47  * use \HPCloud\Storage\ObjectStorage;
48  * use \HPCloud\Storage\ObjectStorage\Container;
49  * use \HPCloud\Storage\ObjectStorage\Object;
50  *
51  * // Create a new ObjectStorage instance, logging in with older Swift
52  * // credentials.
53  * $store = ObjectStorage::newFromSwiftAuth('user', 'key', 'http://example.com');
54  *
55  * // Get the container called 'foo'.
56  * $container = $store->container('foo');
57  *
58  * // Create an object.
59  * $obj = new Object('bar.txt');
60  * $obj->setContent('Example content.', 'text/plain');
61  *
62  * // Save the new object in the container.
63  * $container->save($obj);
64  *
65  * ?>
66  * @endcode
67  *
68  * Once you have a Container, you manipulate objects inside of the
69  * container.
70  *
71  * @todo Add support for container metadata.
72  * @todo Add CDN support fo container listings.
73  */
74 class Container implements \Countable, \IteratorAggregate {
75  /**
76  * The prefix for any piece of metadata passed in HTTP headers.
77  */
78  const METADATA_HEADER_PREFIX = 'X-Object-Meta-';
79  const CONTAINER_METADATA_HEADER_PREFIX = 'X-Container-Meta-';
80 
81 
82  //protected $properties = array();
83  protected $name = NULL;
84 
85  // These were both changed from 0 to NULL to allow
86  // lazy loading.
87  protected $count = NULL;
88  protected $bytes = NULL;
89 
90  protected $token;
91  protected $url;
92  protected $baseUrl;
93  protected $acl;
94  protected $metadata;
95 
96  // This is only set if CDN service is activated.
97  protected $cdnUrl;
98  protected $cdnSslUrl;
99 
100  /**
101  * Transform a metadata array into headers.
102  *
103  * This is used when storing an object in a container.
104  *
105  * @param array $metadata
106  * An associative array of metadata. Metadata is not escaped in any
107  * way (there is no codified spec by which to escape), so make sure
108  * that keys are alphanumeric (dashes allowed) and values are
109  * ASCII-armored with no newlines.
110  * @param string $prefix
111  * A prefix for the metadata headers.
112  * @retval array
113  * @return array
114  * An array of headers.
115  * @see http://docs.openstack.org/bexar/openstack-object-storage/developer/content/ch03s03.html#d5e635
116  * @see http://docs.openstack.org/bexar/openstack-object-storage/developer/content/ch03s03.html#d5e700
117  */
118  public static function generateMetadataHeaders(array $metadata, $prefix = NULL) {
119  if (empty($prefix)) {
121  }
122  $headers = array();
123  foreach ($metadata as $key => $val) {
124  $headers[$prefix . $key] = $val;
125  }
126  return $headers;
127  }
128  /**
129  * Create an object URL.
130  *
131  * Given a base URL and an object name, create an object URL.
132  *
133  * This is useful because object names can contain certain characters
134  * (namely slashes (`/`)) that are normally URLencoded when they appear
135  * inside of path sequences.
136  *
137  * @note
138  * Swift does not distinguish between @c %2F and a slash character, so
139  * this is not strictly necessary.
140  *
141  * @param string $base
142  * The base URL. This is not altered; it is just prepended to
143  * the returned string.
144  * @param string $oname
145  * The name of the object.
146  * @retval string
147  * @return string
148  * The URL to the object. Characters that need escaping will be escaped,
149  * while slash characters are not. Thus, the URL will look pathy.
150  */
151  public static function objectUrl($base, $oname) {
152  if (strpos($oname, '/') === FALSE) {
153  return $base . '/' . rawurlencode($oname);
154  }
155 
156  $oParts = explode('/', $oname);
157  $buffer = array();
158  foreach ($oParts as $part) {
159  $buffer[] = rawurlencode($part);
160  }
161  $newname = implode('/', $buffer);
162  return $base . '/' . $newname;
163  }
164 
165 
166  /**
167  * Extract object attributes from HTTP headers.
168  *
169  * When OpenStack sends object attributes, it sometimes embeds them in
170  * HTTP headers with a prefix. This function parses the headers and
171  * returns the attributes as name/value pairs.
172  *
173  * Note that no decoding (other than the minimum amount necessary) is
174  * done to the attribute names or values. The Open Stack Swift
175  * documentation does not prescribe encoding standards for name or
176  * value data, so it is left up to implementors to choose their own
177  * strategy.
178  *
179  * @param array $headers
180  * An associative array of HTTP headers.
181  * @param string $prefix
182  * The prefix on metadata headers.
183  * @retval array
184  * @return array
185  * An associative array of name/value attribute pairs.
186  */
187  public static function extractHeaderAttributes($headers, $prefix = NULL) {
188  if (empty($prefix)) {
190  }
191  $attributes = array();
192  $offset = strlen($prefix);
193  foreach ($headers as $header => $value) {
194 
195  $index = strpos($header, $prefix);
196  if ($index === 0) {
197  $key = substr($header, $offset);
198  $attributes[$key] = $value;
199  }
200  }
201  return $attributes;
202  }
203 
204  /**
205  * Create a new Container from JSON data.
206  *
207  * This is used in lieue of a standard constructor when
208  * fetching containers from ObjectStorage.
209  *
210  * @param array $jsonArray
211  * An associative array as returned by json_decode($foo, TRUE);
212  * @param string $token
213  * The auth token.
214  * @param string $url
215  * The base URL. The container name is automatically appended to
216  * this at construction time.
217  *
218  * @retval HPCloud::Storage::ObjectStorage::Comtainer
219  * @return \HPCloud\Storage\ObjectStorage\Container
220  * A new container object.
221  */
222  public static function newFromJSON($jsonArray, $token, $url) {
223  $container = new Container($jsonArray['name']);
224 
225  $container->baseUrl = $url;
226 
227  $container->url = $url . '/' . rawurlencode($jsonArray['name']);
228  $container->token = $token;
229 
230  // Access to count and bytes is basically controlled. This is is to
231  // prevent a local copy of the object from getting out of sync with
232  // the remote copy.
233  if (!empty($jsonArray['count'])) {
234  $container->count = $jsonArray['count'];
235  }
236 
237  if (!empty($jsonArray['bytes'])) {
238  $container->bytes = $jsonArray['bytes'];
239  }
240 
241  //syslog(LOG_WARNING, print_r($jsonArray, TRUE));
242 
243  return $container;
244  }
245 
246  /**
247  * Given an OpenStack HTTP response, build a Container.
248  *
249  * This factory is intended for use by low-level libraries. In most
250  * cases, the standard constructor is preferred for client-size
251  * Container initialization.
252  *
253  * @param string $name
254  * The name of the container.
255  * @param object $response HPCloud::Transport::Response
256  * The HTTP response object from the Transporter layer
257  * @param string $token
258  * The auth token.
259  * @param string $url
260  * The base URL. The container name is automatically appended to
261  * this at construction time.
262  * @retval HPCloud::Storage::ObjectStorage::Container
263  * @return \HPCloud\Storage\ObjectStorage\Container
264  * The Container object, initialized and ready for use.
265  */
266  public static function newFromResponse($name, $response, $token, $url) {
267  $container = new Container($name);
268  $container->bytes = $response->header('X-Container-Bytes-Used', 0);
269  $container->count = $response->header('X-Container-Object-Count', 0);
270  $container->baseUrl = $url;
271  $container->url = $url . '/' . rawurlencode($name);
272  $container->token = $token;
273 
274  $container->acl = ACL::newFromHeaders($response->headers());
275 
277  $metadata = Container::extractHeaderAttributes($response->headers(), $prefix);
278  $container->setMetadata($metadata);
279 
280  return $container;
281  }
282 
283  /**
284  * Construct a new Container.
285  *
286  * @attention
287  * Typically a container should be created by ObjectStorage::createContainer().
288  * Get existing containers with ObjectStorage::container() or
289  * ObjectStorage::containers(). Using the constructor directly has some
290  * side effects of which you should be aware.
291  *
292  * Simply creating a container does not save the container remotely.
293  *
294  * Also, this does no checking of the underlying container. That is, simply
295  * constructing a Container in no way guarantees that such a container exists
296  * on the origin object store.
297  *
298  * The constructor involves a selective lazy loading. If a new container is created,
299  * and one of its accessors is called before the accessed values are initialized, then
300  * this will make a network round-trip to get the container from the remote server.
301  *
302  * Containers loaded from ObjectStorage::container() or Container::newFromRemote()
303  * will have all of the necessary values set, and thus will not require an extra network
304  * transaction to fetch properties.
305  *
306  * The practical result of this:
307  *
308  * - If you are creating a new container, it is best to do so with
309  * ObjectStorage::createContainer().
310  * - If you are manipulating an existing container, it is best to load the
311  * container with ObjectStorage::container().
312  * - If you are simply using the container to fetch resources from the
313  * container, you may wish to use `new Container($name, $url, $token)`
314  * and then load objects from that container. Note, however, that
315  * manipulating the container directly will likely involve an extra HTTP
316  * transaction to load the container data.
317  * - When in doubt, use the ObjectStorage methods. That is always the safer
318  * option.
319  *
320  * @param string $name
321  * The name.
322  * @param string $url
323  * The full URL to the container.
324  * @param string $token
325  * The auth token.
326  *
327  */
328  public function __construct($name , $url = NULL, $token = NULL) {
329  $this->name = $name;
330  $this->url = $url;
331  $this->token = $token;
332  }
333 
334  /**
335  * Set the URL of the CDN to use.
336  *
337  * If this is set, the Container will attempt to fetch objects
338  * from the CDN instead of the Swift storage whenever possible.
339  *
340  * If ObjectStorage::useCDN() is already called, this is not necessary.
341  *
342  * Setting this to NULL will have the effect of turning off CDN for this
343  * container.
344  *
345  * @param string $url
346  * The URL to the CDN for this container.
347  * @param string $sslUrl
348  * The SSL URL to the CDN for this container.
349  */
350  public function useCDN($url, $sslUrl) {
351  $this->cdnUrl = $url;
352  $this->cdnSslUrl = $sslUrl;
353  }
354 
355  /**
356  * Get the name of this container.
357  *
358  * @retval string
359  * @return string
360  * The name of the container.
361  */
362  public function name() {
363  return $this->name;
364  }
365 
366  /**
367  * Get the number of bytes in this container.
368  *
369  * @retval int
370  * @return int
371  * The number of bytes in this container.
372  */
373  public function bytes() {
374  if (is_null($this->bytes)) {
375  $this->loadExtraData();
376  }
377  return $this->bytes;
378  }
379 
380  /**
381  * Get the container metadata.
382  *
383  * Metadata (also called tags) are name/value pairs that can be
384  * attached to a container.
385  *
386  * Names can be no longer than 128 characters, and values can be no
387  * more than 256. UTF-8 or ASCII characters are allowed, though ASCII
388  * seems to be preferred.
389  *
390  * If the container was loaded from a container listing, the metadata
391  * will be fetched in a new HTTP request. This is because container
392  * listings do not supply the metadata, while loading a container
393  * directly does.
394  *
395  * @retval array
396  * @return array
397  * An array of metadata name/value pairs.
398  */
399  public function metadata() {
400 
401  // If created from JSON, metadata does not get fetched.
402  if (!isset($this->metadata)) {
403  $this->loadExtraData();
404  }
405  return $this->metadata;
406  }
407 
408 
409  /**
410  * Set the tags on the container.
411  *
412  * Container metadata (sometimes called "tags") provides a way of
413  * storing arbitrary name/value pairs on a container.
414  *
415  * Since saving a container is a function of the ObjectStorage
416  * itself, if you change the metadta, you will need to call
417  * ObjectStorage::updateContainer() to save the new container metadata
418  * on the remote object storage.
419  *
420  * (Similarly, when it comes to objects, an object's metdata is saved
421  * by the container.)
422  *
423  * Names can be no longer than 128 characters, and values can be no
424  * more than 256. UTF-8 or ASCII characters are allowed, though ASCII
425  * seems to be preferred.
426  *
427  * @retval HPCloud::Storage::ObjectStorage::Container
428  * @return \HPCloud\Storage\ObjectStorage\Container
429  * $this so the method can be used in chaining.
430  */
431  public function setMetadata($metadata) {
432  $this->metadata = $metadata;
433 
434  return $this;
435  }
436 
437  /**
438  * Get the number of items in this container.
439  *
440  * Since Container implements Countable, the PHP builtin
441  * count() can be used on a Container instance:
442  *
443  * @code
444  * <?php
445  * count($container) === $container->count();
446  * ?>
447  * @endcode
448  *
449  * @retval int
450  * @return int
451  * The number of items in this container.
452  */
453  public function count() {
454  if (is_null($this->count)) {
455  $this->loadExtraData();
456  }
457  return $this->count;
458  }
459 
460  /**
461  * Save an Object into Object Storage.
462  *
463  * This takes an HPCloud::Storage::ObjectStorage::Object
464  * and stores it in the given container in the present
465  * container on the remote object store.
466  *
467  * @param object $obj HPCloud::Storage::ObjectStorage::Object
468  * The object to store.
469  * @param resource $file
470  * An optional file argument that, if set, will be treated as the
471  * contents of the object.
472  * @retval boolean
473  * @return boolean
474  * TRUE if the object was saved.
475  * @throws HPCloud::Transport::LengthRequiredException
476  * if the Content-Length could not be determined and chunked
477  * encoding was not enabled. This should not occur for this class,
478  * which always automatically generates Content-Length headers.
479  * However, subclasses could generate this error.
480  * @throws HPCloud::Transport::UnprocessableEntityException
481  * if the checksome passed here does not match the checksum
482  * calculated remotely.
483  * @throws HPCloud::Exception when an unexpected (usually
484  * network-related) error condition arises.
485  */
486  public function save(Object $obj, $file = NULL) {
487 
488  if (empty($this->token)) {
489  throw new \HPCloud\Exception('Container does not have an auth token.');
490  }
491  if (empty($this->url)) {
492  throw new \HPCloud\Exception('Container does not have a URL to send data.');
493  }
494 
495  //$url = $this->url . '/' . rawurlencode($obj->name());
496  $url = self::objectUrl($this->url, $obj->name());
497 
498  // See if we have any metadata.
499  $headers = array();
500  $md = $obj->metadata();
501  if (!empty($md)) {
502  $headers = self::generateMetadataHeaders($md, Container::METADATA_HEADER_PREFIX);
503  }
504 
505 
506  // Set the content type.
507  $headers['Content-Type'] = $obj->contentType();
508 
509 
510  // Add content encoding, if necessary.
511  $encoding = $obj->encoding();
512  if (!empty($encoding)) {
513  $headers['Content-Encoding'] = rawurlencode($encoding);
514  }
515 
516  // Add content disposition, if necessary.
517  $disposition = $obj->disposition();
518  if (!empty($disposition)) {
519  $headers['Content-Disposition'] = $disposition;
520  }
521 
522  // Auth token.
523  $headers['X-Auth-Token'] = $this->token;
524 
525  // Add any custom headers:
526  $moreHeaders = $obj->additionalHeaders();
527  if (!empty($moreHeaders)) {
528  $headers += $moreHeaders;
529  }
530 
531  $client = \HPCloud\Transport::instance();
532 
533  if (empty($file)) {
534  // Now build up the rest of the headers:
535  $headers['Etag'] = $obj->eTag();
536 
537  // If chunked, we set transfer encoding; else
538  // we set the content length.
539  if ($obj->isChunked()) {
540  // How do we handle this? Does the underlying
541  // stream wrapper pay any attention to this?
542  $headers['Transfer-Encoding'] = 'chunked';
543  }
544  else {
545  $headers['Content-Length'] = $obj->contentLength();
546  }
547  $response = $client->doRequest($url, 'PUT', $headers, $obj->content());
548  }
549  else {
550  // Rewind the file.
551  rewind($file);
552 
553 
554  // XXX: What do we do about Content-Length header?
555  //$headers['Transfer-Encoding'] = 'chunked';
556  $stat = fstat($file);
557  $headers['Content-Length'] = $stat['size'];
558 
559  // Generate an eTag:
560  $hash = hash_init('md5');
561  hash_update_stream($hash, $file);
562  $etag = hash_final($hash);
563  $headers['Etag'] = $etag;
564 
565  // Not sure if this is necessary:
566  rewind($file);
567 
568  $response = $client->doRequestWithResource($url, 'PUT', $headers, $file);
569 
570  }
571 
572  if ($response->status() != 201) {
573  throw new \HPCloud\Exception('An unknown error occurred while saving: ' . $response->status());
574  }
575  return TRUE;
576  }
577 
578  /**
579  * Update an object's metadata.
580  *
581  * This updates the metadata on an object without modifying anything
582  * else. This is a convenient way to set additional metadata without
583  * having to re-upload a potentially large object.
584  *
585  * Swift's behavior during this operation is sometimes unpredictable,
586  * particularly in cases where custom headers have been set.
587  * Use with caution.
588  *
589  * @param object $obj HPCloud::Storage::ObjectStorage::Object
590  * The object to update.
591  *
592  * @retval boolean
593  * @return boolean
594  * TRUE if the metadata was updated.
595  *
596  * @throws HPCloud::Transport::FileNotFoundException
597  * if the object does not already exist on the object storage.
598  */
599  public function updateMetadata(Object $obj) {
600  //$url = $this->url . '/' . rawurlencode($obj->name());
601  $url = self::objectUrl($this->url, $obj->name());
602  $headers = array();
603 
604  // See if we have any metadata. We post this even if there
605  // is no metadata.
606  $md = $obj->metadata();
607  if (!empty($md)) {
608  $headers = self::generateMetadataHeaders($md, Container::METADATA_HEADER_PREFIX);
609  }
610  $headers['X-Auth-Token'] = $this->token;
611 
612  // In spite of the documentation's claim to the contrary,
613  // content type IS reset during this operation.
614  $headers['Content-Type'] = $obj->contentType();
615 
616  $client = \HPCloud\Transport::instance();
617 
618  // The POST verb is for updating headers.
619  $response = $client->doRequest($url, 'POST', $headers, $obj->content());
620 
621  if ($response->status() != 202) {
622  throw new \HPCloud\Exception('An unknown error occurred while saving: ' . $response->status());
623  }
624  return TRUE;
625  }
626 
627  /**
628  * Copy an object to another place in object storage.
629  *
630  * An object can be copied within a container. Essentially, this will
631  * give you duplicates of the file, each with a new name.
632  *
633  * An object can be copied to another container if the name of the
634  * other container is specified, and if that container already exists.
635  *
636  * Note that there is no MOVE operation. You must copy and then DELETE
637  * in order to achieve that.
638  *
639  * @param object $obj HPCloud::Storage::ObjectStorage::Object
640  * The object to copy. This object MUST already be saved on the
641  * remote server. The body of the object is not sent. Instead, the
642  * copy operation is performed on the remote server. You can, and
643  * probably should, use a RemoteObject here.
644  * @param string $newName
645  * The new name of this object. If you are copying across
646  * containers, the name can be the same. If you are copying within
647  * the same container, though, you will need to supply a new name.
648  * @param string $container
649  * The name of the alternate container. If this is set, the object
650  * will be saved into this container. If this is not sent, the copy
651  * will be performed inside of the original container.
652  *
653  */
654  public function copy(Object $obj, $newName, $container = NULL) {
655  //$sourceUrl = $obj->url(); // This doesn't work with Object; only with RemoteObject.
656  $sourceUrl = self::objectUrl($this->url, $obj->name());
657 
658  if (empty($newName)) {
659  throw new \HPCloud\Exception("An object name is required to copy the object.");
660  }
661 
662  // Figure out what container we store in.
663  if (empty($container)) {
665  }
666  $container = rawurlencode($container);
667  $destUrl = self::objectUrl('/' . $container, $newName);
668 
669  $headers = array(
670  'X-Auth-Token' => $this->token,
671  'Destination' => $destUrl,
672  );
673 
674  $client = \HPCloud\Transport::instance();
675  $response = $client->doRequest($sourceUrl, 'COPY', $headers);
676 
677  if ($response->status() != 201) {
678  throw new \HPCloud\Exception("An unknown condition occurred during copy. " . $response->status());
679  }
680  return TRUE;
681  }
682 
683  /**
684  * Get the object with the given name.
685  *
686  * This fetches a single object with the given name. It downloads the
687  * entire object at once. This is useful if the object is small (under
688  * a few megabytes) and the content of the object will be used. For
689  * example, this is the right operation for accessing a text file
690  * whose contents will be processed.
691  *
692  * For larger files or files whose content may never be accessed, use
693  * remoteObject(), which delays loading the content until one of its
694  * content methods (e.g. RemoteObject::content()) is called.
695  *
696  * This does not yet support the following features of Swift:
697  *
698  * - Byte range queries.
699  * - If-Modified-Since/If-Unmodified-Since
700  * - If-Match/If-None-Match
701  *
702  * If a CDN has been specified either using useCDN() or
703  * ObjectStorage::useCDN(), this will attempt to fetch the object
704  * from the CDN.
705  *
706  * @param string $name
707  * The name of the object to load.
708  * @param boolean $requireSSL
709  * If this is TRUE (the default), then SSL will always be
710  * used. If this is FALSE, then CDN-based fetching will
711  * use non-SSL, which is faster.
712  * @retval HPCloud::Storage::ObjectStorage::RemoteObject
713  * @return \HPCloud\Storage\ObjectStorage\RemoteObject
714  * A remote object with the content already stored locally.
715  */
716  public function object($name, $requireSSL = TRUE) {
717 
718  $url = self::objectUrl($this->url, $name);
719  $cdn = self::objectUrl($this->cdnUrl, $name);
720  $cdnSsl = self::objectUrl($this->cdnSslUrl, $name);
721  $headers = array();
722 
723  // Auth token.
724  $headers['X-Auth-Token'] = $this->token;
725 
726  $client = \HPCloud\Transport::instance();
727 
728  if (empty($this->cdnUrl)) {
729  $response = $client->doRequest($url, 'GET', $headers);
730  }
731  else {
732  $from = $requireSSL ? $cdnSsl : $cdn;
733  // print "Fetching object from $from\n";
734  $response = $client->doRequest($from, 'GET', $headers);
735  }
736 
737  if ($response->status() != 200) {
738  throw new \HPCloud\Exception('An unknown error occurred while saving: ' . $response->status());
739  }
740 
741  $remoteObject = RemoteObject::newFromHeaders($name, $response->headers(), $this->token, $url);
742  $remoteObject->setContent($response->content());
743 
744  if (!empty($this->cdnUrl)) {
745  $remoteObject->useCDN($cdn, $cdnSsl);
746  }
747 
748  return $remoteObject;
749  }
750 
751  /**
752  * Fetch an object, but delay fetching its contents.
753  *
754  * This retrieves all of the information about an object except for
755  * its contents. Size, hash, metadata, and modification date
756  * information are all retrieved and wrapped.
757  *
758  * The data comes back as a RemoteObject, which can be used to
759  * transparently fetch the object's content, too.
760  *
761  * Why Use This?
762  *
763  * The regular object() call will fetch an entire object, including
764  * its content. This may not be desireable for cases where the object
765  * is large.
766  *
767  * This method can featch the relevant metadata, but delay fetching
768  * the content until it is actually needed.
769  *
770  * Since RemoteObject extends Object, all of the calls that can be
771  * made to an Object can also be made to a RemoteObject. Be aware,
772  * though, that calling RemoteObject::content() will initiate another
773  * network operation.
774  *
775  * @param string $name
776  * The name of the object to fetch.
777  * @retval HPCloud::Storage::ObjectStorage::RemoteObject
778  * @return \HPCloud\Storage\ObjectStorage\RemoteObject
779  * A remote object ready for use.
780  */
781  public function proxyObject($name) {
782  $url = self::objectUrl($this->url, $name);
783  $cdn = self::objectUrl($this->cdnUrl, $name);
784  $cdnSsl = self::objectUrl($this->cdnSslUrl, $name);
785  $headers = array(
786  'X-Auth-Token' => $this->token,
787  );
788 
789 
790  $client = \HPCloud\Transport::instance();
791 
792  if (empty($this->cdnUrl)) {
793  $response = $client->doRequest($url, 'HEAD', $headers);
794  }
795  else {
796  $response = $client->doRequest($cdnSsl, 'HEAD', $headers);
797  }
798 
799  if ($response->status() != 200) {
800  throw new \HPCloud\Exception('An unknown error occurred while saving: ' . $response->status());
801  }
802 
803  $headers = $response->headers();
804 
805  $obj = RemoteObject::newFromHeaders($name, $headers, $this->token, $url);
806 
807  if (!empty($this->cdnUrl)) {
808  $obj->useCDN($cdn, $cdnSsl);
809  }
810 
811  return $obj;
812  }
813  /**
814  * This has been replaced with proxyObject().
815  * @deprecated
816  */
817  public function remoteObject($name) {
818  return $this->proxyObject($name);
819  }
820 
821  /**
822  * Get a list of objects in this container.
823  *
824  * This will return a list of objects in the container. With no
825  * parameters, it will attempt to return a listing of <i>all</i>
826  * objects in the container. However, by setting contraints, you can
827  * retrieve only a specific subset of objects.
828  *
829  * Note that OpenStacks Swift will return no more than 10,000 objects
830  * per request. When dealing with large datasets, you are encouraged
831  * to use paging.
832  *
833  * Paging
834  *
835  * Paging is done with a combination of a limit and a marker. The
836  * limit is an integer indicating the maximum number of items to
837  * return. The marker is the string name of an object. Typically, this
838  * is the last object in the previously returned set. The next batch
839  * will begin with the next item after the marker (assuming the marker
840  * is found.)
841  *
842  * @param int $limit
843  * An integer indicating the maximum number of items to return. This
844  * cannot be greater than the Swift maximum (10k).
845  * @param string $marker
846  * The name of the object to start with. The query will begin with
847  * the next object AFTER this one.
848  * @retval array
849  * @return array
850  * List of RemoteObject or Subdir instances.
851  */
852  public function objects($limit = NULL, $marker = NULL) {
853  $params = array();
854  return $this->objectQuery($params, $limit, $marker);
855  }
856 
857  /**
858  * Retrieve a list of Objects with the given prefix.
859  *
860  * Object Storage containers support directory-like organization. To
861  * get a list of items inside of a particular "subdirectory", provide
862  * the directory name as a "prefix". This will return only objects
863  * that begin with that prefix.
864  *
865  * (Directory-like behavior is also supported by using "directory
866  * markers". See objectsByPath().)
867  *
868  * Prefixes
869  *
870  * Prefixes are basically substring patterns that are matched against
871  * files on the remote object storage.
872  *
873  * When a prefix is used, object storage will begin to return not just
874  * Object instsances, but also Subdir instances. A Subdir is simply a
875  * container for a "path name".
876  *
877  * Delimiters
878  *
879  * Object Storage (OpenStack Swift) does not have a native concept of
880  * files and directories when it comes to paths. Instead, it merely
881  * represents them and simulates their behavior under specific
882  * circumstances.
883  *
884  * The default behavior (when prefixes are used) is to treat the '/'
885  * character as a delimiter. Thus, when it encounters a name like
886  * this: `foo/bar/baz.txt` and the prefix is `foo/`, it will
887  * parse return a Subdir called `foo/bar`.
888  *
889  * Similarly, if you store a file called `foo:bar:baz.txt` and then
890  * set the delimiter to `:` and the prefix to `foo:`, it will return
891  * the Subdir `foo:bar`. However, merely setting the delimiter back to
892  * `/` will not allow you to query `foo/bar` and get the contents of
893  * `foo:bar`.
894  *
895  * Setting $delimiter will tell the Object Storage server which
896  * character to parse the filenames on. This means that if you use
897  * delimiters other than '/', you need to be very consistent with your
898  * usage or else you may get surprising results.
899  *
900  * @param string $prefix
901  * The leading prefix.
902  * @param string $delimiter
903  * The character used to delimit names. By default, this is '/'.
904  * @param int $limit
905  * An integer indicating the maximum number of items to return. This
906  * cannot be greater than the Swift maximum (10k).
907  * @param string $marker
908  * The name of the object to start with. The query will begin with
909  * the next object AFTER this one.
910  * @retval array
911  * @return array
912  * List of RemoteObject or Subdir instances.
913  */
914  public function objectsWithPrefix($prefix, $delimiter = '/', $limit = NULL, $marker = NULL) {
915  $params = array(
916  'prefix' => $prefix,
917  'delimiter' => $delimiter,
918  );
919  return $this->objectQuery($params, $limit, $marker);
920  }
921 
922  /**
923  * Specify a path (subdirectory) to traverse.
924  *
925  * OpenStack Swift provides two basic ways to handle directory-like
926  * structures. The first is using a prefix (see objectsWithPrefix()).
927  * The second is to create directory markers and use a path.
928  *
929  * A directory marker is just a file with a name that is
930  * directory-like. You create it exactly as you create any other file.
931  * Typically, it is 0 bytes long.
932  *
933  * @code
934  * <?php
935  * $dir = new Object('a/b/c', '');
936  * $container->save($dir);
937  * ?>
938  * @endcode
939  *
940  * Using objectsByPath() with directory markers will return a list of
941  * Object instances, some of which are regular files, and some of
942  * which are just empty directory marker files. When creating
943  * directory markers, you may wish to set metadata or content-type
944  * information indicating that they are directory markers.
945  *
946  * At one point, the OpenStack documentation suggested that the path
947  * method was legacy. More recent versions of the documentation no
948  * longer indicate this.
949  *
950  * @param string $path
951  * The path prefix.
952  * @param string $delimiter
953  * The character used to delimit names. By default, this is '/'.
954  * @param int $limit
955  * An integer indicating the maximum number of items to return. This
956  * cannot be greater than the Swift maximum (10k).
957  * @param string $marker
958  * The name of the object to start with. The query will begin with
959  * the next object AFTER this one.
960  */
961  public function objectsByPath($path, $delimiter = '/', $limit = NULL, $marker = NULL) {
962  $params = array(
963  'path' => $path,
964  'delimiter' => $delimiter,
965  );
966  return $this->objectQuery($params, $limit, $marker);
967  }
968 
969  /**
970  * Get the URL to this container.
971  *
972  * Any container that has been created will have a valid URL. If the
973  * Container was set to be public (See
974  * ObjectStorage::createContainer()) will be accessible by this URL.
975  *
976  * @retval string
977  * @return string
978  * The URL.
979  */
980  public function url() {
981  return $this->url;
982  }
983 
984  public function cdnUrl($ssl = TRUE) {
985  return $ssl ? $this->cdnSslUrl : $this->cdnUrl;
986  }
987 
988  /**
989  * Get the ACL.
990  *
991  * Currently, if the ACL wasn't added during object construction,
992  * calling acl() will trigger a request to the remote server to fetch
993  * the ACL. Since only some Swift calls return ACL data, this is an
994  * unavoidable artifact.
995  *
996  * Calling this on a Container that has not been stored on the remote
997  * ObjectStorage will produce an error. However, this should not be an
998  * issue, since containers should always come from one of the
999  * ObjectStorage methods.
1000  *
1001  * @todo Determine how to get the ACL from JSON data.
1002  * @retval \HPCloud\Storage\ObjectStorage\ACL
1003  * @return HPCloud::Storage::ObjectStorage::ACL
1004  * An ACL, or NULL if the ACL could not be retrieved.
1005  */
1006  public function acl() {
1007  if (!isset($this->acl)) {
1008  $this->loadExtraData();
1009  }
1010  return $this->acl;
1011  }
1012 
1013  /**
1014  * Get missing fields.
1015  *
1016  * Not all containers come fully instantiated. This method is sometimes
1017  * called to "fill in" missing fields.
1018  *
1019  * @retval HPCloud::Storage::ObjectStorage::Comtainer
1020  * @return \HPCloud\Storage\ObjectStorage\Container
1021  */
1022  protected function loadExtraData() {
1023 
1024  // If URL and token are empty, we are dealing with
1025  // a local item that has not been saved, and was not
1026  // created with Container::createContainer(). We treat
1027  // this as an error condition.
1028  if (empty($this->url) || empty($this->token)) {
1029  throw new \HPCloud\Exception('Remote data cannot be fetched. Tokena and endpoint URL are required.');
1030  }
1031  // Do a GET on $url to fetch headers.
1032  $client = \HPCloud\Transport::instance();
1033  $headers = array(
1034  'X-Auth-Token' => $this->token,
1035  );
1036  $response = $client->doRequest($this->url, 'GET', $headers);
1037 
1038  // Get ACL.
1039  $this->acl = ACL::newFromHeaders($response->headers());
1040 
1041  // Update size and count.
1042  $this->bytes = $response->header('X-Container-Bytes-Used', 0);
1043  $this->count = $response->header('X-Container-Object-Count', 0);
1044 
1045  // Get metadata.
1047  $this->setMetadata(Container::extractHeaderAttributes($response->headers(), $prefix));
1048 
1049  return $this;
1050  }
1051 
1052  /**
1053  * Perform the HTTP query for a list of objects and de-serialize the
1054  * results.
1055  */
1056  protected function objectQuery($params = array(), $limit = NULL, $marker = NULL) {
1057  if (isset($limit)) {
1058  $params['limit'] = (int) $limit;
1059  if (!empty($marker)) {
1060  $params['marker'] = (string) $marker;
1061  }
1062  }
1063 
1064  // We always want JSON.
1065  $params['format'] = 'json';
1066 
1067  $query = http_build_query($params);
1068  $query = str_replace('%2F', '/', $query);
1069  $url = $this->url . '?' . $query;
1070 
1071  $client = \HPCloud\Transport::instance();
1072  $headers = array(
1073  'X-Auth-Token' => $this->token,
1074  );
1075 
1076  $response = $client->doRequest($url, 'GET', $headers);
1077 
1078  // The only codes that should be returned are 200 and the ones
1079  // already thrown by doRequest.
1080  if ($response->status() != 200) {
1081  throw new \HPCloud\Exception('An unknown exception occurred while processing the request.');
1082  }
1083 
1084  $responseContent = $response->content();
1085  $json = json_decode($responseContent, TRUE);
1086 
1087  // Turn the array into a list of RemoteObject instances.
1088  $list = array();
1089  foreach ($json as $item) {
1090  if (!empty($item['subdir'])) {
1091  $list[] = new Subdir($item['subdir'], $params['delimiter']);
1092  }
1093  elseif (empty($item['name'])) {
1094  throw new \HPCloud\Exception('Unexpected entity returned.');
1095  }
1096  else {
1097  //$url = $this->url . '/' . rawurlencode($item['name']);
1098  $url = self::objectUrl($this->url, $item['name']);
1099  $list[] = RemoteObject::newFromJSON($item, $this->token, $url);
1100  }
1101  }
1102 
1103  return $list;
1104  }
1105 
1106  /**
1107  * Return the iterator of contents.
1108  *
1109  * A Container is Iterable. This means that you can use a container in
1110  * a `foreach` loop directly:
1111  *
1112  * @code
1113  * <?php
1114  * foreach ($container as $object) {
1115  * print $object->name();
1116  * }
1117  * ?>
1118  * @endcode
1119  *
1120  * The above is equivalent to doing the following:
1121  * @code
1122  * <?php
1123  * $objects = $container->objects();
1124  * foreach ($objects as $object) {
1125  * print $object->name();
1126  * }
1127  * ?>
1128  * @endcode
1129  *
1130  * Note that there is no way to pass any constraints into an iterator.
1131  * You cannot limit the number of items, set an marker, or add a
1132  * prefix.
1133  */
1134  public function getIterator() {
1135  return new \ArrayIterator($this->objects());
1136  }
1137 
1138  /**
1139  * Remove the named object from storage.
1140  *
1141  * @param string $name
1142  * The name of the object to remove.
1143  * @retval boolean
1144  * @return boolean
1145  * TRUE if the file was deleted, FALSE if no such file is found.
1146  */
1147  public function delete($name) {
1148  $url = self::objectUrl($this->url, $name);
1149  $headers = array(
1150  'X-Auth-Token' => $this->token,
1151  );
1152 
1153  $client = \HPCloud\Transport::instance();
1154 
1155  try {
1156  $response = $client->doRequest($url, 'DELETE', $headers);
1157  }
1158  catch (\HPCloud\Transport\FileNotFoundException $fnfe) {
1159  return FALSE;
1160  }
1161 
1162  if ($response->status() != 204) {
1163  throw new \HPCloud\Exception("An unknown exception occured while deleting $name.");
1164  }
1165 
1166  return TRUE;
1167  }
1168 
1169 }