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: