Overview

Namespaces

  • OpenCloud
    • Autoscale
      • Resource
    • CDN
      • Resource
    • CloudMonitoring
      • Collection
      • Exception
      • Resource
    • Common
      • Collection
      • Constants
      • Exceptions
      • Http
        • Message
      • Log
      • Resource
      • Service
    • Compute
      • Constants
      • Exception
      • Resource
    • Database
      • Resource
    • DNS
      • Collection
      • Resource
    • Identity
      • Constants
      • Resource
    • Image
      • Enum
      • Resource
        • JsonPatch
        • Schema
    • LoadBalancer
      • Collection
      • Enum
      • Resource
    • Networking
      • Resource
    • ObjectStore
      • Constants
      • Enum
      • Exception
      • Resource
      • Upload
    • Orchestration
      • Resource
    • Queues
      • Collection
      • Exception
      • Resource
    • Volume
      • Resource
  • PHP

Classes

  • AbstractContainer
  • AbstractResource
  • Account
  • CDNContainer
  • Container
  • ContainerMetadata
  • DataObject
  • Overview
  • Namespace
  • Class
  • Tree
  1: <?php
  2: /**
  3:  * Copyright 2012-2014 Rackspace US, Inc.
  4:  *
  5:  * Licensed under the Apache License, Version 2.0 (the "License");
  6:  * you may not use this file except in compliance with the License.
  7:  * You may obtain a copy of the License at
  8:  *
  9:  * http://www.apache.org/licenses/LICENSE-2.0
 10:  *
 11:  * Unless required by applicable law or agreed to in writing, software
 12:  * distributed under the License is distributed on an "AS IS" BASIS,
 13:  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 14:  * See the License for the specific language governing permissions and
 15:  * limitations under the License.
 16:  */
 17: 
 18: namespace OpenCloud\ObjectStore\Resource;
 19: 
 20: use Guzzle\Http\EntityBody;
 21: use Guzzle\Http\Exception\BadResponseException;
 22: use Guzzle\Http\Exception\ClientErrorResponseException;
 23: use Guzzle\Http\Message\Response;
 24: use Guzzle\Http\Url;
 25: use OpenCloud\Common\Constants\Size;
 26: use OpenCloud\Common\Exceptions;
 27: use OpenCloud\Common\Service\ServiceInterface;
 28: use OpenCloud\ObjectStore\Constants\Header as HeaderConst;
 29: use OpenCloud\ObjectStore\Exception\ContainerException;
 30: use OpenCloud\ObjectStore\Exception\ObjectNotFoundException;
 31: use OpenCloud\ObjectStore\Upload\DirectorySync;
 32: use OpenCloud\ObjectStore\Upload\TransferBuilder;
 33: use OpenCloud\ObjectStore\Enum\ReturnType;
 34: 
 35: /**
 36:  * A container is a storage compartment for your data and provides a way for you
 37:  * to organize your data. You can think of a container as a folder in Windows
 38:  * or a directory in Unix. The primary difference between a container and these
 39:  * other file system concepts is that containers cannot be nested.
 40:  *
 41:  * A container can also be CDN-enabled (for public access), in which case you
 42:  * will need to interact with a CDNContainer object instead of this one.
 43:  */
 44: class Container extends AbstractContainer
 45: {
 46:     const METADATA_LABEL = 'Container';
 47: 
 48:     /**
 49:      * This is the object that holds all the CDN functionality. This Container therefore acts as a simple wrapper and is
 50:      * interested in storage concerns only.
 51:      *
 52:      * @var CDNContainer|null
 53:      */
 54:     private $cdn;
 55: 
 56:     public function __construct(ServiceInterface $service, $data = null)
 57:     {
 58:         parent::__construct($service, $data);
 59: 
 60:         // Set metadata items for collection listings
 61:         if (isset($data->count)) {
 62:             $this->metadata->setProperty('Object-Count', $data->count);
 63:         }
 64:         if (isset($data->bytes)) {
 65:             $this->metadata->setProperty('Bytes-Used', $data->bytes);
 66:         }
 67:     }
 68: 
 69:     /**
 70:      * Factory method that instantiates an object from a Response object.
 71:      *
 72:      * @param Response         $response
 73:      * @param ServiceInterface $service
 74:      * @return static
 75:      */
 76:     public static function fromResponse(Response $response, ServiceInterface $service)
 77:     {
 78:         $self = parent::fromResponse($response, $service);
 79: 
 80:         $segments = Url::factory($response->getEffectiveUrl())->getPathSegments();
 81:         $self->name = end($segments);
 82: 
 83:         return $self;
 84:     }
 85: 
 86:     /**
 87:      * Get the CDN object.
 88:      *
 89:      * @return null|CDNContainer
 90:      * @throws \OpenCloud\Common\Exceptions\CdnNotAvailableError
 91:      */
 92:     public function getCdn()
 93:     {
 94:         if (!$this->isCdnEnabled()) {
 95:             throw new Exceptions\CdnNotAvailableError(
 96:                 'Either this container is not CDN-enabled or the CDN is not available'
 97:             );
 98:         }
 99: 
100:         return $this->cdn;
101:     }
102: 
103:     /**
104:      * It would be awesome to put these convenience methods (which are identical to the ones in the Account object) in
105:      * a trait, but we have to wait for v5.3 EOL first...
106:      *
107:      * @return null|string|int
108:      */
109:     public function getObjectCount()
110:     {
111:         return $this->metadata->getProperty('Object-Count');
112:     }
113: 
114:     /**
115:      * @return null|string|int
116:      */
117:     public function getBytesUsed()
118:     {
119:         return $this->metadata->getProperty('Bytes-Used');
120:     }
121: 
122:     /**
123:      * @param $value
124:      * @return mixed
125:      */
126:     public function setCountQuota($value)
127:     {
128:         $this->metadata->setProperty('Quota-Count', $value);
129: 
130:         return $this->saveMetadata($this->metadata->toArray());
131:     }
132: 
133:     /**
134:      * @return null|string|int
135:      */
136:     public function getCountQuota()
137:     {
138:         return $this->metadata->getProperty('Quota-Count');
139:     }
140: 
141:     /**
142:      * @param $value
143:      * @return mixed
144:      */
145:     public function setBytesQuota($value)
146:     {
147:         $this->metadata->setProperty('Quota-Bytes', $value);
148: 
149:         return $this->saveMetadata($this->metadata->toArray());
150:     }
151: 
152:     /**
153:      * @return null|string|int
154:      */
155:     public function getBytesQuota()
156:     {
157:         return $this->metadata->getProperty('Quota-Bytes');
158:     }
159: 
160:     public function delete($deleteObjects = false)
161:     {
162:         if ($deleteObjects === true) {
163:             // Delegate to auxiliary method
164:             return $this->deleteWithObjects();
165:         }
166: 
167:         try {
168:             return $this->getClient()->delete($this->getUrl())->send();
169:         } catch (ClientErrorResponseException $e) {
170:             if ($e->getResponse()->getStatusCode() == 409) {
171:                 throw new ContainerException(sprintf(
172:                     'The API returned this error: %s. You might have to delete all existing objects before continuing.',
173:                     (string) $e->getResponse()->getBody()
174:                 ));
175:             } else {
176:                 throw $e;
177:             }
178:         }
179:     }
180: 
181:     public function deleteWithObjects($secondsToWait = null)
182:     {
183:         // If container is empty, just delete it
184:         $numObjects = (int) $this->retrieveMetadata()->getProperty('Object-Count');
185:         if (0 === $numObjects) {
186:             return $this->delete();
187:         }
188: 
189:         // If timeout ($secondsToWait) is not specified by caller,
190:         // try to estimate it based on number of objects in container
191:         if (null === $secondsToWait) {
192:             $secondsToWait = round($numObjects / 2);
193:         }
194: 
195:         // Attempt to delete all objects and container
196:         $endTime = time() + $secondsToWait;
197:         $containerDeleted = false;
198:         while ((time() < $endTime) && !$containerDeleted) {
199:             $this->deleteAllObjects();
200:             try {
201:                 $response = $this->delete();
202:                 $containerDeleted = true;
203:             } catch (ContainerException $e) {
204:                 // Ignore exception and try again
205:             } catch (ClientErrorResponseException $e) {
206:                 if ($e->getResponse()->getStatusCode() == 404) {
207:                     // Container has been deleted
208:                     $containerDeleted = true;
209:                 } else {
210:                     throw $e;
211:                 }
212:             }
213:         }
214: 
215:         if (!$containerDeleted) {
216:             throw new ContainerException('Container and all its objects could not be deleted.');
217:         }
218: 
219:         return $response;
220:     }
221: 
222:     /**
223:      * Deletes all objects that this container currently contains. Useful when doing operations (like a delete) that
224:      * require an empty container first.
225:      *
226:      * @return mixed
227:      */
228:     public function deleteAllObjects()
229:     {
230:         $paths = array();
231:         $objects = $this->objectList();
232:         foreach ($objects as $object) {
233:             $paths[] = sprintf('/%s/%s', $this->getName(), $object->getName());
234:         }
235:         return $this->getService()->batchDelete($paths);
236:     }
237: 
238:     /**
239:      * Creates a Collection of objects in the container
240:      *
241:      * @param array $params associative array of parameter values.
242:      *                      * account/tenant - The unique identifier of the account/tenant.
243:      *                      * container- The unique identifier of the container.
244:      *                      * limit (Optional) - The number limit of results.
245:      *                      * marker (Optional) - Value of the marker, that the object names
246:      *                      greater in value than are returned.
247:      *                      * end_marker (Optional) - Value of the marker, that the object names
248:      *                      less in value than are returned.
249:      *                      * prefix (Optional) - Value of the prefix, which the returned object
250:      *                      names begin with.
251:      *                      * format (Optional) - Value of the serialized response format, either
252:      *                      json or xml.
253:      *                      * delimiter (Optional) - Value of the delimiter, that all the object
254:      *                      names nested in the container are returned.
255:      * @link   http://api.openstack.org for a list of possible parameter
256:      *                      names and values
257:      * @return \OpenCloud\Common\Collection
258:      * @throws ObjFetchError
259:      */
260:     public function objectList(array $params = array())
261:     {
262:         $params['format'] = 'json';
263: 
264:         return $this->getService()->resourceList('DataObject', $this->getUrl(null, $params), $this);
265:     }
266: 
267:     /**
268:      * Turn on access logs, which track all the web traffic that your data objects accrue.
269:      *
270:      * @return \Guzzle\Http\Message\Response
271:      */
272:     public function enableLogging()
273:     {
274:         return $this->saveMetadata($this->appendToMetadata(array(
275:             HeaderConst::ACCESS_LOGS => 'True'
276:         )));
277:     }
278: 
279:     /**
280:      * Disable access logs.
281:      *
282:      * @return \Guzzle\Http\Message\Response
283:      */
284:     public function disableLogging()
285:     {
286:         return $this->saveMetadata($this->appendToMetadata(array(
287:             HeaderConst::ACCESS_LOGS => 'False'
288:         )));
289:     }
290: 
291:     /**
292:      * Enable this container for public CDN access.
293:      *
294:      * @param null $ttl
295:      */
296:     public function enableCdn($ttl = null)
297:     {
298:         $headers = array('X-CDN-Enabled' => 'True');
299:         if ($ttl) {
300:             $headers['X-TTL'] = (int) $ttl;
301:         }
302: 
303:         $this->getClient()->put($this->getCdnService()->getUrl($this->name), $headers)->send();
304:         $this->refresh();
305:     }
306: 
307:     /**
308:      * Disables the containers CDN function. Note that the container will still
309:      * be available on the CDN until its TTL expires.
310:      *
311:      * @return \Guzzle\Http\Message\Response
312:      */
313:     public function disableCdn()
314:     {
315:         $headers = array('X-CDN-Enabled' => 'False');
316: 
317:         return $this->getClient()
318:             ->put($this->getCdnService()->getUrl($this->name), $headers)
319:             ->send();
320:     }
321: 
322:     public function refresh($id = null, $url = null)
323:     {
324:         $headers = $this->createRefreshRequest()->send()->getHeaders();
325:         $this->setMetadata($headers, true);
326:     }
327: 
328:     /**
329:      * Get either a fresh data object (no $info), or get an existing one by passing in data for population.
330:      *
331:      * @param  mixed $info
332:      * @return DataObject
333:      */
334:     public function dataObject($info = null)
335:     {
336:         return new DataObject($this, $info);
337:     }
338: 
339:     /**
340:      * Retrieve an object from the API. Apart from using the name as an
341:      * identifier, you can also specify additional headers that will be used
342:      * fpr a conditional GET request. These are
343:      *
344:      * * `If-Match'
345:      * * `If-None-Match'
346:      * * `If-Modified-Since'
347:      * * `If-Unmodified-Since'
348:      * * `Range'  For example:
349:      *      bytes=-5    would mean the last 5 bytes of the object
350:      *      bytes=10-15 would mean 5 bytes after a 10 byte offset
351:      *      bytes=32-   would mean all dat after first 32 bytes
352:      *
353:      * These are also documented in RFC 2616.
354:      *
355:      * @param string $name
356:      * @param array  $headers
357:      * @return DataObject
358:      */
359:     public function getObject($name, array $headers = array())
360:     {
361:         try {
362:             $response = $this->getClient()
363:                 ->get($this->getUrl($name), $headers)
364:                 ->send();
365:         } catch (BadResponseException $e) {
366:             if ($e->getResponse()->getStatusCode() == 404) {
367:                 throw ObjectNotFoundException::factory($name, $e);
368:             }
369:             throw $e;
370:         }
371: 
372:         return $this->dataObject()
373:             ->populateFromResponse($response)
374:             ->setName($name);
375:     }
376: 
377:     /**
378:      * Essentially the same as {@see getObject()}, except only the metadata is fetched from the API.
379:      * This is useful for cases when the user does not want to fetch the full entity body of the
380:      * object, only its metadata.
381:      *
382:      * @param       $name
383:      * @param array $headers
384:      * @return $this
385:      */
386:     public function getPartialObject($name, array $headers = array())
387:     {
388:         $response = $this->getClient()
389:             ->head($this->getUrl($name), $headers)
390:             ->send();
391: 
392:         return $this->dataObject()
393:             ->populateFromResponse($response)
394:             ->setName($name);
395:     }
396: 
397:     /**
398:      * Check if an object exists inside a container. Uses {@see getPartialObject()}
399:      * to save on bandwidth and time.
400:      *
401:      * @param  $name    Object name
402:      * @return boolean  True, if object exists in this container; false otherwise.
403:      */
404:     public function objectExists($name)
405:     {
406:         try {
407:             // Send HEAD request to check resource existence
408:             $url = clone $this->getUrl();
409:             $url->addPath((string) $name);
410:             $this->getClient()->head($url)->send();
411:         } catch (ClientErrorResponseException $e) {
412:             // If a 404 was returned, then the object doesn't exist
413:             if ($e->getResponse()->getStatusCode() === 404) {
414:                 return false;
415:             } else {
416:                 throw $e;
417:             }
418:         }
419: 
420:         return true;
421:     }
422: 
423:     /**
424:      * Upload a single file to the API.
425:      *
426:      * @param       $name    Name that the file will be saved as in your container.
427:      * @param       $data    Either a string or stream representation of the file contents to be uploaded.
428:      * @param array $headers Optional headers that will be sent with the request (useful for object metadata).
429:      * @return DataObject
430:      */
431:     public function uploadObject($name, $data, array $headers = array())
432:     {
433:         $entityBody = EntityBody::factory($data);
434: 
435:         $url = clone $this->getUrl();
436:         $url->addPath($name);
437: 
438:         // @todo for new major release: Return response rather than populated DataObject
439: 
440:         $response = $this->getClient()->put($url, $headers, $entityBody)->send();
441: 
442:         return $this->dataObject()
443:             ->populateFromResponse($response)
444:             ->setName($name)
445:             ->setContent($entityBody);
446:     }
447: 
448:     /**
449:      * Upload an array of objects for upload. This method optimizes the upload procedure by batching requests for
450:      * faster execution. This is a very useful procedure when you just have a bunch of unremarkable files to be
451:      * uploaded quickly. Each file must be under 5GB.
452:      *
453:      * @param array $files   With the following array structure:
454:      *                       `name' Name that the file will be saved as in your container. Required.
455:      *                       `path' Path to an existing file, OR
456:      *                       `body' Either a string or stream representation of the file contents to be uploaded.
457:      * @param array $headers Optional headers that will be sent with the request (useful for object metadata).
458:      * @param string $returnType One of OpenCloud\ObjectStore\Enum\ReturnType::RESPONSE_ARRAY (to return an array of
459:      *                           Guzzle\Http\Message\Response objects) or OpenCloud\ObjectStore\Enum\ReturnType::DATA_OBJECT_ARRAY
460:      *                           (to return an array of OpenCloud\ObjectStore\Resource\DataObject objects).
461:      *
462:      * @throws \OpenCloud\Common\Exceptions\InvalidArgumentError
463:      * @return Guzzle\Http\Message\Response[] or OpenCloud\ObjectStore\Resource\DataObject[] depending on $returnType
464:      */
465:     public function uploadObjects(array $files, array $commonHeaders = array(), $returnType = ReturnType::RESPONSE_ARRAY)
466:     {
467:         $requests = $entities = array();
468: 
469:         foreach ($files as $entity) {
470:             if (empty($entity['name'])) {
471:                 throw new Exceptions\InvalidArgumentError('You must provide a name.');
472:             }
473: 
474:             if (!empty($entity['path']) && file_exists($entity['path'])) {
475:                 $body = fopen($entity['path'], 'r+');
476:             } elseif (!empty($entity['body'])) {
477:                 $body = $entity['body'];
478:             } else {
479:                 throw new Exceptions\InvalidArgumentError('You must provide either a readable path or a body');
480:             }
481: 
482:             $entityBody = $entities[] = EntityBody::factory($body);
483: 
484:             // @codeCoverageIgnoreStart
485:             if ($entityBody->getContentLength() >= 5 * Size::GB) {
486:                 throw new Exceptions\InvalidArgumentError(
487:                     'For multiple uploads, you cannot upload more than 5GB per '
488:                     . ' file. Use the UploadBuilder for larger files.'
489:                 );
490:             }
491:             // @codeCoverageIgnoreEnd
492: 
493:             // Allow custom headers and common
494:             $headers = (isset($entity['headers'])) ? $entity['headers'] : $commonHeaders;
495: 
496:             $url = clone $this->getUrl();
497:             $url->addPath($entity['name']);
498: 
499:             $requests[] = $this->getClient()->put($url, $headers, $entityBody);
500:         }
501: 
502:         $responses = $this->getClient()->send($requests);
503: 
504:         if (ReturnType::RESPONSE_ARRAY === $returnType) {
505:             foreach ($entities as $entity) {
506:                 $entity->close();
507:             }
508:             return $responses;
509:         } else {
510:             // Convert responses to DataObjects before returning
511:             $dataObjects = array();
512:             foreach ($responses as $index => $response) {
513:                 $dataObjects[] = $this->dataObject()
514:                                ->populateFromResponse($response)
515:                                ->setName($files[$index]['name'])
516:                                ->setContent($entities[$index]);
517:             }
518:             return $dataObjects;
519:         }
520:     }
521: 
522:     /**
523:      * When uploading large files (+5GB), you need to upload the file as chunks using multibyte transfer. This method
524:      * sets up the transfer, and in order to execute the transfer, you need to call upload() on the returned object.
525:      *
526:      * @param array Options
527:      * @see \OpenCloud\ObjectStore\Upload\UploadBuilder::setOptions for a list of accepted options.
528:      * @throws \OpenCloud\Common\Exceptions\InvalidArgumentError
529:      * @return mixed
530:      */
531:     public function setupObjectTransfer(array $options = array())
532:     {
533:         // Name is required
534:         if (empty($options['name'])) {
535:             throw new Exceptions\InvalidArgumentError('You must provide a name.');
536:         }
537: 
538:         // As is some form of entity body
539:         if (!empty($options['path']) && file_exists($options['path'])) {
540:             $body = fopen($options['path'], 'r+');
541:         } elseif (!empty($options['body'])) {
542:             $body = $options['body'];
543:         } else {
544:             throw new Exceptions\InvalidArgumentError('You must provide either a readable path or a body');
545:         }
546: 
547:         // Build upload
548:         $transfer = TransferBuilder::newInstance()
549:             ->setOption('objectName', $options['name'])
550:             ->setEntityBody(EntityBody::factory($body))
551:             ->setContainer($this);
552: 
553:         // Add extra options
554:         if (!empty($options['metadata'])) {
555:             $transfer->setOption('metadata', $options['metadata']);
556:         }
557:         if (!empty($options['partSize'])) {
558:             $transfer->setOption('partSize', $options['partSize']);
559:         }
560:         if (!empty($options['concurrency'])) {
561:             $transfer->setOption('concurrency', $options['concurrency']);
562:         }
563:         if (!empty($options['progress'])) {
564:             $transfer->setOption('progress', $options['progress']);
565:         }
566: 
567:         return $transfer->build();
568:     }
569: 
570:     /**
571:      * Upload the contents of a local directory to a remote container, effectively syncing them.
572:      *
573:      * @param $path The local path to the directory.
574:      */
575:     public function uploadDirectory($path)
576:     {
577:         $sync = DirectorySync::factory($path, $this);
578:         $sync->execute();
579:     }
580: 
581:     public function isCdnEnabled()
582:     {
583:         // If CDN object is not already populated, try to populate it.
584:         if (null === $this->cdn) {
585:             $this->refreshCdnObject();
586:         }
587:         return ($this->cdn instanceof CDNContainer) && $this->cdn->isCdnEnabled();
588:     }
589: 
590:     protected function refreshCdnObject()
591:     {
592:         try {
593:             if (null !== ($cdnService = $this->getService()->getCDNService())) {
594:                 $cdn = new CDNContainer($cdnService);
595:                 $cdn->setName($this->name);
596: 
597:                 $response = $cdn->createRefreshRequest()->send();
598: 
599:                 if ($response->isSuccessful()) {
600:                     $this->cdn = $cdn;
601:                     $this->cdn->setMetadata($response->getHeaders(), true);
602:                 }
603:             } else {
604:                 $this->cdn = null;
605:             }
606:         } catch (ClientErrorResponseException $e) {
607:         }
608:     }
609: }
610: 
API documentation generated by ApiGen 2.8.0