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