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