      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\Message\Response;
     22: use Guzzle\Http\Url;
     23: use OpenCloud\Common\Constants\Header as HeaderConst;
     24: use OpenCloud\Common\Exceptions;
     25: use OpenCloud\Common\Lang;
     26: use OpenCloud\ObjectStore\Constants\UrlType;
     27:
     28: /**
     29:  * Objects are the basic storage entities in Cloud Files. They represent the
     30:  * files and their optional metadata you upload to the system. When you upload
     31:  * objects to Cloud Files, the data is stored as-is (without compression or
     32:  * encryption) and consists of a location (container), the object's name, and
     33:  * any metadata you assign consisting of key/value pairs.
     34:  */
     35: class DataObject extends AbstractResource
     36: {
     37:     const METADATA_LABEL = 'Object';
     38:
     39:     /**
     40:      * @var Container
     41:      */
     42:     private $container;
     43:
     44:     /**
     45:      * @var The file name of the object
     46:      */
     47:     protected $name;
     48:
     49:     /**
     50:      * @var EntityBody
     51:      */
     52:     protected $content;
     53:
     54:     /**
     55:      * @var bool Whether or not this object is a "pseudo-directory"
     56:      * @link http://docs.openstack.org/trunk/openstack-object-storage/developer/content/pseudo-hierarchical-folders-directories.html
     57:      */
     58:     protected $directory = false;
     59:
     60:     /**
     61:      * @var string The object's content type
     62:      */
     63:     protected $contentType;
     64:
     65:     /**
     66:      * @var The size of this object.
     67:      */
     68:     protected $contentLength;
     69:
     70:     /**
     71:      * @var string Date of last modification.
     72:      */
     73:     protected $lastModified;
     74:
     75:     /**
     76:      * @var string Etag.
     77:      */
     78:     protected $etag;
     79:
     80:     /**
     81:      * Also need to set Container parent and handle pseudo-directories.
     82:      * {@inheritDoc}
     83:      *
     84:      * @param Container $container
     85:      * @param null      $data
     86:      */
     87:     public function __construct(Container $container, $data = null)
     88:     {
     89:         $this->setContainer($container);
     90:
     91:         parent::__construct($container->getService());
     92:
     93:         // For pseudo-directories, we need to ensure the name is set
     94:         if (!empty($data->subdir)) {
     95:             $this->setName($data->subdir)->setDirectory(true);
     96:
     97:             return;
     98:         }
     99:
    100:         $this->populate($data);
    101:     }
    102:
    103:     /**
    104:      * A collection list of DataObjects contains a different data structure than the one returned for the
    105:      * "Retrieve Object" operation. So we need to stock the values differently.
    106:      * {@inheritDoc}
    107:      */
    108:     public function populate($info, $setObjects = true)
    109:     {
    110:         parent::populate($info, $setObjects);
    111:
    112:         if (isset($info->bytes)) {
    113:             $this->setContentLength($info->bytes);
    114:         }
    115:         if (isset($info->last_modified)) {
    116:             $this->setLastModified($info->last_modified);
    117:         }
    118:         if (isset($info->content_type)) {
    119:             $this->setContentType($info->content_type);
    120:         }
    121:         if (isset($info->hash)) {
    122:             $this->setEtag($info->hash);
    123:         }
    124:     }
    125:
    126:     /**
    127:      * Takes a response and stocks common values from both the body and the headers.
    128:      *
    129:      * @param Response $response
    130:      * @return $this
    131:      */
    132:     public function populateFromResponse(Response $response)
    133:     {
    134:         $this->content = $response->getBody();
    135:
    136:         $headers = $response->getHeaders();
    137:
    138:         return $this->setMetadata($headers, true)
    139:             ->setContentType((string) $headers[HeaderConst::CONTENT_TYPE])
    140:             ->setLastModified((string) $headers[HeaderConst::LAST_MODIFIED])
    141:             ->setContentLength((string) $headers[HeaderConst::CONTENT_LENGTH])
    142:             ->setEtag((string) $headers[HeaderConst::ETAG]);
    143:     }
    144:
    145:     public function refresh()
    146:     {
    147:         $response = $this->getService()->getClient()
    148:             ->get($this->getUrl())
    149:             ->send();
    150:
    151:         return $this->populateFromResponse($response);
    152:     }
    153:
    154:     /**
    155:      * @param Container $container
    156:      * @return $this
    157:      */
    158:     public function setContainer(Container $container)
    159:     {
    160:         $this->container = $container;
    161:
    162:         return $this;
    163:     }
    164:
    165:     /**
    166:      * @return Container
    167:      */
    168:     public function getContainer()
    169:     {
    170:         return $this->container;
    171:     }
    172:
    173:     /**
    174:      * @param $name string
    175:      * @return $this
    176:      */
    177:     public function setName($name)
    178:     {
    179:         $this->name = $name;
    180:
    181:         return $this;
    182:     }
    183:
    184:     /**
    185:      * @return string
    186:      */
    187:     public function getName()
    188:     {
    189:         return $this->name;
    190:     }
    191:
    192:     /**
    193:      * @param $directory bool
    194:      * @return $this
    195:      */
    196:     public function setDirectory($directory)
    197:     {
    198:         $this->directory = $directory;
    199:
    200:         return $this;
    201:     }
    202:
    203:     /**
    204:      * @return bool
    205:      */
    206:     public function getDirectory()
    207:     {
    208:         return $this->directory;
    209:     }
    210:
    211:     /**
    212:      * @return bool Is this data object a pseudo-directory?
    213:      */
    214:     public function isDirectory()
    215:     {
    216:         return (bool) $this->directory;
    217:     }
    218:
    219:     /**
    220:      * @param  mixed $content
    221:      * @return $this
    222:      */
    223:     public function setContent($content)
    224:     {
    225:         $this->etag = null;
    226:         $this->contentType = null;
    227:         $this->content = EntityBody::factory($content);
    228:
    229:         return $this;
    230:     }
    231:
    232:     /**
    233:      * @return EntityBody
    234:      */
    235:     public function getContent()
    236:     {
    237:         return $this->content;
    238:     }
    239:
    240:     /**
    241:      * @param  string $contentType
    242:      * @return $this
    243:      */
    244:     public function setContentType($contentType)
    245:     {
    246:         $this->contentType = $contentType;
    247:
    248:         return $this;
    249:     }
    250:
    251:     /**
    252:      * @return null|string
    253:      */
    254:     public function getContentType()
    255:     {
    256:         return $this->contentType ? : $this->content->getContentType();
    257:     }
    258:
    259:     /**
    260:      * @param $contentType int
    261:      * @return $this
    262:      */
    263:     public function setContentLength($contentLength)
    264:     {
    265:         $this->contentLength = $contentLength;
    266:
    267:         return $this;
    268:     }
    269:
    270:     /**
    271:      * @return int
    272:      */
    273:     public function getContentLength()
    274:     {
    275:         return $this->contentLength !== null ? $this->contentLength : $this->content->getContentLength();
    276:     }
    277:
    278:     /**
    279:      * @param $etag
    280:      * @return $this
    281:      */
    282:     public function setEtag($etag)
    283:     {
    284:         $this->etag = $etag;
    285:
    286:         return $this;
    287:     }
    288:
    289:     /**
    290:      * @return null|string
    291:      */
    292:     public function getEtag()
    293:     {
    294:         return $this->etag ? : $this->content->getContentMd5();
    295:     }
    296:
    297:     public function setLastModified($lastModified)
    298:     {
    299:         $this->lastModified = $lastModified;
    300:
    301:         return $this;
    302:     }
    303:
    304:     public function getLastModified()
    305:     {
    306:         return $this->lastModified;
    307:     }
    308:
    309:     public function primaryKeyField()
    310:     {
    311:         return 'name';
    312:     }
    313:
    314:     public function getUrl($path = null, array $params = array())
    315:     {
    316:         if (!$this->name) {
    317:             throw new Exceptions\NoNameError(Lang::translate('Object has no name'));
    318:         }
    319:
    320:         return $this->container->getUrl($this->name);
    321:     }
    322:
    323:     public function update($params = array())
    324:     {
    325:         $metadata = is_array($this->metadata) ? $this->metadata : $this->metadata->toArray();
    326:         $metadata = self::stockHeaders($metadata);
    327:
    328:         // merge specific properties with metadata
    329:         $metadata += array(
    330:             HeaderConst::CONTENT_TYPE   => $this->contentType,
    331:             HeaderConst::LAST_MODIFIED  => $this->lastModified,
    332:             HeaderConst::CONTENT_LENGTH => $this->contentLength,
    333:             HeaderConst::ETAG           => $this->etag
    334:         );
    335:
    336:         return $this->container->uploadObject($this->name, $this->content, $metadata);
    337:     }
    338:
    339:     /**
    340:      * @param string $destination Path (`container/object') of new object
    341:      * @return \Guzzle\Http\Message\Response
    342:      */
    343:     public function copy($destination)
    344:     {
    345:         return $this->getService()
    346:             ->getClient()
    347:             ->createRequest('COPY', $this->getUrl(), array(
    348:                 'Destination' => (string) $destination
    349:             ))
    350:             ->send();
    351:     }
    352:
    353:     public function delete($params = array())
    354:     {
    355:         return $this->getService()->getClient()->delete($this->getUrl())->send();
    356:     }
    357:
    358:     /**
    359:      * Get a temporary URL for this object.
    360:      *
    361:      * @link http://docs.rackspace.com/files/api/v1/cf-devguide/content/TempURL-d1a4450.html
    362:      *
    363:      * @param $expires Expiration time in seconds
    364:      * @param $method  What method can use this URL? (`GET' or `PUT')
    365:      * @return string
    366:      * @throws \OpenCloud\Common\Exceptions\InvalidArgumentError
    367:      * @throws \OpenCloud\Common\Exceptions\ObjectError
    368:      *
    369:      */
    370:     public function getTemporaryUrl($expires, $method)
    371:     {
    372:         $method = strtoupper($method);
    373:         $expiry = time() + (int) $expires;
    374:
    375:         // check for proper method
    376:         if ($method != 'GET' && $method != 'PUT') {
    377:             throw new Exceptions\InvalidArgumentError(sprintf(
    378:                 'Bad method [%s] for TempUrl; only GET or PUT supported',
    379:                 $method
    380:             ));
    381:         }
    382:
    383:         // @codeCoverageIgnoreStart
    384:         if (!($secret = $this->getService()->getAccount()->getTempUrlSecret())) {
    385:             throw new Exceptions\ObjectError('Cannot produce temporary URL without an account secret.');
    386:         }
    387:         // @codeCoverageIgnoreEnd
    388:
    389:         $url = $this->getUrl();
    390:         $urlPath = urldecode($url->getPath());
    391:         $body = sprintf("%s\n%d\n%s", $method, $expiry, $urlPath);
    392:         $hash = hash_hmac('sha1', $body, $secret);
    393:
    394:         return sprintf('%s?temp_url_sig=%s&temp_url_expires=%d', $url, $hash, $expiry);
    395:     }
    396:
    397:     /**
    398:      * Remove this object from the CDN.
    399:      *
    400:      * @param null $email
    401:      * @return mixed
    402:      */
    403:     public function purge($email = null)
    404:     {
    405:         if (!$cdn = $this->getContainer()->getCdn()) {
    406:             return false;
    407:         }
    408:
    409:         $url = clone $cdn->getUrl();
    410:         $url->addPath($this->name);
    411:
    412:         $headers = ($email !== null) ? array('X-Purge-Email' => $email) : array();
    413:
    414:         return $this->getService()
    415:             ->getClient()
    416:             ->delete($url, $headers)
    417:             ->send();
    418:     }
    419:
    420:     /**
    421:      * @param string $type
    422:      * @return bool|Url
    423:      */
    424:     public function getPublicUrl($type = UrlType::CDN)
    425:     {
    426:         $cdn = $this->container->getCdn();
    427:
    428:         switch ($type) {
    429:             case UrlType::CDN:
    430:                 $uri = $cdn->getCdnUri();
    431:                 break;
    432:             case UrlType::SSL:
    433:                 $uri = $cdn->getCdnSslUri();
    434:                 break;
    435:             case UrlType::STREAMING:
    436:                 $uri = $cdn->getCdnStreamingUri();
    437:                 break;
    438:             case UrlType::IOS_STREAMING:
    439:                 $uri = $cdn->getIosStreamingUri();
    440:                 break;
    441:         }
    442:
    443:         return (isset($uri)) ? Url::factory($uri)->addPath($this->name) : false;
    444:     }
    445:
    446:     protected static function headerIsValidMetadata($header)
    447:     {
    448:         $pattern = sprintf('#^%s-%s-Meta-#i', self::GLOBAL_METADATA_PREFIX, self::METADATA_LABEL);
    449:
    450:         return preg_match($pattern, $header);
    451:     }
    452: }
    453: