      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\Upload;
     19:
     20: use Exception;
     21: use Guzzle\Http\EntityBody;
     22: use OpenCloud\Common\Exceptions\RuntimeException;
     23: use OpenCloud\Common\Http\Client;
     24: use OpenCloud\ObjectStore\Exception\UploadException;
     25:
     26: /**
     27:  * Contains abstract functionality for transfer objects.
     28:  */
     29: class AbstractTransfer
     30: {
     31:     /**
     32:      * Minimum chunk size is 1MB.
     33:      */
     34:     const MIN_PART_SIZE = 1048576;
     35:
     36:     /**
     37:      * Maximum chunk size is 5GB.
     38:      */
     39:     const MAX_PART_SIZE = 5368709120;
     40:
     41:     /**
     42:      * Default chunk size is 1GB.
     43:      */
     44:     const DEFAULT_PART_SIZE = 1073741824;
     45:
     46:     /**
     47:      * @var \OpenCloud\Common\Http\Client The client object which handles all HTTP interactions
     48:      */
     49:     protected $client;
     50:
     51:     /**
     52:      * @var \Guzzle\Http\EntityBody The payload being transferred
     53:      */
     54:     protected $entityBody;
     55:
     56:     /**
     57:      * The current state of the transfer responsible for, among other things, holding an itinerary of uploaded parts
     58:      *
     59:      * @var \OpenCloud\ObjectStore\Upload\TransferState
     60:      */
     61:     protected $transferState;
     62:
     63:     /**
     64:      * @var array User-defined key/pair options
     65:      */
     66:     protected $options;
     67:
     68:     /**
     69:      * @var int
     70:      */
     71:     protected $partSize;
     72:
     73:     /**
     74:      * @var array Defaults that will always override user-defined options
     75:      */
     76:     protected $defaultOptions = array(
     77:         'concurrency'    => true,
     78:         'partSize'       => self::DEFAULT_PART_SIZE,
     79:         'prefix'         => 'segment',
     80:         'doPartChecksum' => true
     81:     );
     82:
     83:     /**
     84:      * @return static
     85:      */
     86:     public static function newInstance()
     87:     {
     88:         return new static();
     89:     }
     90:
     91:     /**
     92:      * @param Client $client
     93:      * @return $this
     94:      */
     95:     public function setClient(Client $client)
     96:     {
     97:         $this->client = $client;
     98:
     99:         return $this;
    100:     }
    101:
    102:     /**
    103:      * @param EntityBody $entityBody
    104:      * @return $this
    105:      */
    106:     public function setEntityBody(EntityBody $entityBody)
    107:     {
    108:         $this->entityBody = $entityBody;
    109:
    110:         return $this;
    111:     }
    112:
    113:     /**
    114:      * @param TransferState $transferState
    115:      * @return $this
    116:      */
    117:     public function setTransferState(TransferState $transferState)
    118:     {
    119:         $this->transferState = $transferState;
    120:
    121:         return $this;
    122:     }
    123:
    124:     /**
    125:      * @return array
    126:      */
    127:     public function getOptions()
    128:     {
    129:         return $this->options;
    130:     }
    131:
    132:     /**
    133:      * @param $options
    134:      * @return $this
    135:      */
    136:     public function setOptions($options)
    137:     {
    138:         $this->options = $options;
    139:
    140:         return $this;
    141:     }
    142:
    143:     /**
    144:      * @param $option The key being updated
    145:      * @param $value  The option's value
    146:      * @return $this
    147:      */
    148:     public function setOption($option, $value)
    149:     {
    150:         $this->options[$option] = $value;
    151:
    152:         return $this;
    153:     }
    154:
    155:     public function getPartSize()
    156:     {
    157:         return $this->partSize;
    158:     }
    159:
    160:     /**
    161:      * @return $this
    162:      */
    163:     public function setup()
    164:     {
    165:         $this->options = array_merge($this->defaultOptions, $this->options);
    166:         $this->partSize = $this->validatePartSize();
    167:
    168:         return $this;
    169:     }
    170:
    171:     /**
    172:      * Make sure the part size falls within a valid range
    173:      *
    174:      * @return mixed
    175:      */
    176:     protected function validatePartSize()
    177:     {
    178:         $min = min($this->options['partSize'], self::MAX_PART_SIZE);
    179:
    180:         return max($min, self::MIN_PART_SIZE);
    181:     }
    182:
    183:     /**
    184:      * Initiates the upload procedure.
    185:      *
    186:      * @return \Guzzle\Http\Message\Response
    187:      * @throws RuntimeException If the transfer is not in a "running" state
    188:      * @throws UploadException  If any errors occur during the upload
    189:      * @codeCoverageIgnore
    190:      */
    191:     public function upload()
    192:     {
    193:         if (!$this->transferState->isRunning()) {
    194:             throw new RuntimeException('The transfer has been aborted.');
    195:         }
    196:
    197:         try {
    198:             $this->transfer();
    199:             $response = $this->createManifest();
    200:         } catch (Exception $e) {
    201:             throw new UploadException($this->transferState, $e);
    202:         }
    203:
    204:         return $response;
    205:     }
    206:
    207:     /**
    208:      * With large uploads, you must create a manifest file. Although each segment or TransferPart remains
    209:      * individually addressable, the manifest file serves as the unified file (i.e. the 5GB download) which, when
    210:      * retrieved, streams all the segments concatenated.
    211:      *
    212:      * @link http://docs.rackspace.com/files/api/v1/cf-devguide/content/Large_Object_Creation-d1e2019.html
    213:      * @return \Guzzle\Http\Message\Response
    214:      * @codeCoverageIgnore
    215:      */
    216:     private function createManifest()
    217:     {
    218:         $parts = array();
    219:
    220:         foreach ($this->transferState as $part) {
    221:             $parts[] = (object) array(
    222:                 'path'       => $part->getPath(),
    223:                 'etag'       => $part->getETag(),
    224:                 'size_bytes' => $part->getContentLength()
    225:             );
    226:         }
    227:
    228:         $headers = array(
    229:             'Content-Length'    => 0,
    230:             'X-Object-Manifest' => sprintf('%s/%s/%s/',
    231:                 $this->options['containerName'],
    232:                 $this->options['objectName'],
    233:                 $this->options['prefix']
    234:             )
    235:         );
    236:
    237:         $url = clone $this->options['containerUrl'];
    238:         $url->addPath($this->options['objectName']);
    239:
    240:         return $this->client->put($url, $headers)->send();
    241:     }
    242: }
    243: