~tcuthbert/wordpress/openstack-objectstorage

« back to all changes in this revision

Viewing changes to src/OpenStack/ObjectStore/v1/Resource/Container.php

  • Committer: Jacek Nykis
  • Date: 2015-02-11 15:35:31 UTC
  • Revision ID: jacek.nykis@canonical.com-20150211153531-hmy6zi0ov2qfkl0b
Initial commit

Show diffs side-by-side

added added

removed removed

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