1
|
|
|
<?php |
2
|
|
|
/** |
3
|
|
|
* Spiral Framework. |
4
|
|
|
* |
5
|
|
|
* @license MIT |
6
|
|
|
* @author Anton Titov (Wolfy-J) |
7
|
|
|
*/ |
8
|
|
|
namespace Spiral\Storage\Servers; |
9
|
|
|
|
10
|
|
|
use GuzzleHttp\Client; |
11
|
|
|
use GuzzleHttp\ClientInterface; |
12
|
|
|
use GuzzleHttp\Exception\ClientException; |
13
|
|
|
use GuzzleHttp\Psr7\Request; |
14
|
|
|
use GuzzleHttp\Psr7\Uri; |
15
|
|
|
use Psr\Http\Message\RequestInterface; |
16
|
|
|
use Psr\Http\Message\ResponseInterface; |
17
|
|
|
use Psr\Http\Message\UriInterface; |
18
|
|
|
use Psr\Log\LoggerAwareInterface; |
19
|
|
|
use Spiral\Cache\StoreInterface; |
20
|
|
|
use Spiral\Debug\Traits\LoggerTrait; |
21
|
|
|
use Spiral\Files\FilesInterface; |
22
|
|
|
use Spiral\Storage\BucketInterface; |
23
|
|
|
use Spiral\Storage\Exceptions\ServerException; |
24
|
|
|
use Spiral\Storage\StorageServer; |
25
|
|
|
|
26
|
|
|
/** |
27
|
|
|
* Provides abstraction level to work with data located in Rackspace cloud. |
28
|
|
|
*/ |
29
|
|
|
class RackspaceServer extends StorageServer implements LoggerAwareInterface |
30
|
|
|
{ |
31
|
|
|
/** |
32
|
|
|
* There is few warning messages. |
33
|
|
|
*/ |
34
|
|
|
use LoggerTrait; |
35
|
|
|
|
36
|
|
|
/** |
37
|
|
|
* @var string |
38
|
|
|
*/ |
39
|
|
|
private $authToken = []; |
40
|
|
|
|
41
|
|
|
/** |
42
|
|
|
* Some operations can be performed only inside one region. |
43
|
|
|
* |
44
|
|
|
* @var array |
45
|
|
|
*/ |
46
|
|
|
private $regions = []; |
47
|
|
|
|
48
|
|
|
/** |
49
|
|
|
* @var array |
50
|
|
|
*/ |
51
|
|
|
protected $options = [ |
52
|
|
|
'server' => 'https://auth.api.rackspacecloud.com/v1.0', |
53
|
|
|
'authServer' => 'https://identity.api.rackspacecloud.com/v2.0/tokens', |
54
|
|
|
'username' => '', |
55
|
|
|
'apiKey' => '', |
56
|
|
|
'cache' => true, |
57
|
|
|
'lifetime' => 86400 |
58
|
|
|
]; |
59
|
|
|
|
60
|
|
|
/** |
61
|
|
|
* Cache store to remember connection. |
62
|
|
|
* |
63
|
|
|
* @invisible |
64
|
|
|
* @var StoreInterface |
65
|
|
|
*/ |
66
|
|
|
protected $store = null; |
67
|
|
|
|
68
|
|
|
/** |
69
|
|
|
* @todo DI in constructor |
70
|
|
|
* @var ClientInterface |
71
|
|
|
*/ |
72
|
|
|
protected $client = null; |
73
|
|
|
|
74
|
|
|
/** |
75
|
|
|
* @param FilesInterface $files |
76
|
|
|
* @param StoreInterface $store |
77
|
|
|
* @param array $options |
78
|
|
|
*/ |
79
|
|
|
public function __construct(FilesInterface $files, StoreInterface $store, array $options) |
80
|
|
|
{ |
81
|
|
|
parent::__construct($files, $options); |
82
|
|
|
$this->store = $store; |
83
|
|
|
|
84
|
|
|
if ($this->options['cache']) { |
85
|
|
|
$this->authToken = $this->store->get( |
86
|
|
|
$this->options['username'] . '@rackspace-token' |
87
|
|
|
); |
88
|
|
|
|
89
|
|
|
$this->regions = (array)$this->store->get( |
90
|
|
|
$this->options['username'] . '@rackspace-regions' |
91
|
|
|
); |
92
|
|
|
} |
93
|
|
|
|
94
|
|
|
//This code is going to use additional abstraction layer to connect storage and guzzle |
95
|
|
|
$this->client = new Client($this->options); |
96
|
|
|
$this->connect(); |
97
|
|
|
} |
98
|
|
|
|
99
|
|
|
/** |
100
|
|
|
* @param ClientInterface $client |
101
|
|
|
* @return $this |
102
|
|
|
*/ |
103
|
|
|
public function setClient(ClientInterface $client) |
104
|
|
|
{ |
105
|
|
|
$this->client = $client; |
106
|
|
|
|
107
|
|
|
return $this; |
108
|
|
|
} |
109
|
|
|
|
110
|
|
|
/** |
111
|
|
|
* {@inheritdoc} |
112
|
|
|
* |
113
|
|
|
* @return bool|ResponseInterface |
114
|
|
|
*/ |
115
|
|
|
public function exists(BucketInterface $bucket, $name) |
116
|
|
|
{ |
117
|
|
|
try { |
118
|
|
|
$response = $this->client->send($this->buildRequest('HEAD', $bucket, $name)); |
119
|
|
|
} catch (ClientException $exception) { |
120
|
|
|
if ($exception->getCode() == 404) { |
121
|
|
|
return false; |
122
|
|
|
} |
123
|
|
|
|
124
|
|
|
if ($exception->getCode() == 401) { |
125
|
|
|
$this->reconnect(); |
126
|
|
|
|
127
|
|
|
return $this->exists($bucket, $name); |
128
|
|
|
} |
129
|
|
|
|
130
|
|
|
//Some unexpected error |
131
|
|
|
throw new ServerException($exception->getMessage(), $exception->getCode(), $exception); |
132
|
|
|
} |
133
|
|
|
|
134
|
|
|
if ($response->getStatusCode() !== 200) { |
135
|
|
|
return false; |
136
|
|
|
} |
137
|
|
|
|
138
|
|
|
return $response; |
139
|
|
|
} |
140
|
|
|
|
141
|
|
|
/** |
142
|
|
|
* {@inheritdoc} |
143
|
|
|
*/ |
144
|
|
View Code Duplication |
public function size(BucketInterface $bucket, $name) |
|
|
|
|
145
|
|
|
{ |
146
|
|
|
if (empty($response = $this->exists($bucket, $name))) { |
147
|
|
|
return false; |
148
|
|
|
} |
149
|
|
|
|
150
|
|
|
return (int)$response->getHeaderLine('Content-Length'); |
151
|
|
|
} |
152
|
|
|
|
153
|
|
|
/** |
154
|
|
|
* {@inheritdoc} |
155
|
|
|
*/ |
156
|
|
|
public function put(BucketInterface $bucket, $name, $source) |
157
|
|
|
{ |
158
|
|
|
if (empty($mimetype = \GuzzleHttp\Psr7\mimetype_from_filename($name))) { |
159
|
|
|
$mimetype = self::DEFAULT_MIMETYPE; |
160
|
|
|
} |
161
|
|
|
|
162
|
|
|
try { |
163
|
|
|
$request = $this->buildRequest('PUT', $bucket, $name, [ |
164
|
|
|
'Content-Type' => $mimetype, |
165
|
|
|
'Etag' => md5_file($this->castFilename($source)) |
166
|
|
|
]); |
167
|
|
|
|
168
|
|
|
$this->client->send($request->withBody($this->castStream($source))); |
169
|
|
|
} catch (ClientException $exception) { |
170
|
|
|
if ($exception->getCode() == 401) { |
171
|
|
|
$this->reconnect(); |
172
|
|
|
|
173
|
|
|
return $this->put($bucket, $name, $source); |
174
|
|
|
} |
175
|
|
|
|
176
|
|
|
//Some unexpected error |
177
|
|
|
throw new ServerException($exception->getMessage(), $exception->getCode(), $exception); |
178
|
|
|
} |
179
|
|
|
|
180
|
|
|
return true; |
181
|
|
|
} |
182
|
|
|
|
183
|
|
|
/** |
184
|
|
|
* {@inheritdoc} |
185
|
|
|
*/ |
186
|
|
View Code Duplication |
public function allocateStream(BucketInterface $bucket, $name) |
|
|
|
|
187
|
|
|
{ |
188
|
|
|
try { |
189
|
|
|
$response = $this->client->send($this->buildRequest('GET', $bucket, $name)); |
190
|
|
|
} catch (ClientException $exception) { |
191
|
|
|
if ($exception->getCode() == 401) { |
192
|
|
|
$this->reconnect(); |
193
|
|
|
|
194
|
|
|
return $this->allocateStream($bucket, $name); |
195
|
|
|
} |
196
|
|
|
|
197
|
|
|
throw new ServerException($exception->getMessage(), $exception->getCode(), $exception); |
198
|
|
|
} |
199
|
|
|
|
200
|
|
|
return $response->getBody(); |
201
|
|
|
} |
202
|
|
|
|
203
|
|
|
/** |
204
|
|
|
* {@inheritdoc} |
205
|
|
|
*/ |
206
|
|
View Code Duplication |
public function delete(BucketInterface $bucket, $name) |
|
|
|
|
207
|
|
|
{ |
208
|
|
|
try { |
209
|
|
|
$this->client->send($this->buildRequest('DELETE', $bucket, $name)); |
210
|
|
|
} catch (ClientException $exception) { |
211
|
|
|
if ($exception->getCode() == 401) { |
212
|
|
|
$this->reconnect(); |
213
|
|
|
$this->delete($bucket, $name); |
214
|
|
|
} elseif ($exception->getCode() != 404) { |
215
|
|
|
throw new ServerException($exception->getMessage(), $exception->getCode(), |
216
|
|
|
$exception); |
217
|
|
|
} |
218
|
|
|
} |
219
|
|
|
} |
220
|
|
|
|
221
|
|
|
/** |
222
|
|
|
* {@inheritdoc} |
223
|
|
|
*/ |
224
|
|
|
public function rename(BucketInterface $bucket, $oldname, $newname) |
225
|
|
|
{ |
226
|
|
|
try { |
227
|
|
|
$request = $this->buildRequest('PUT', $bucket, $newname, [ |
228
|
|
|
'X-Copy-From' => '/' . $bucket->getOption('container') . '/' . rawurlencode($oldname), |
229
|
|
|
'Content-Length' => 0 |
230
|
|
|
]); |
231
|
|
|
|
232
|
|
|
$this->client->send($request); |
233
|
|
|
} catch (ClientException $exception) { |
234
|
|
|
if ($exception->getCode() == 401) { |
235
|
|
|
$this->reconnect(); |
236
|
|
|
|
237
|
|
|
return $this->rename($bucket, $oldname, $newname); |
238
|
|
|
} |
239
|
|
|
|
240
|
|
|
throw new ServerException($exception->getMessage(), $exception->getCode(), $exception); |
241
|
|
|
} |
242
|
|
|
|
243
|
|
|
//Deleting old file |
244
|
|
|
$this->delete($bucket, $oldname); |
245
|
|
|
|
246
|
|
|
return true; |
247
|
|
|
} |
248
|
|
|
|
249
|
|
|
/** |
250
|
|
|
* {@inheritdoc} |
251
|
|
|
*/ |
252
|
|
|
public function copy(BucketInterface $bucket, BucketInterface $destination, $name) |
253
|
|
|
{ |
254
|
|
|
if ($bucket->getOption('region') != $destination->getOption('region')) { |
255
|
|
|
$this->logger()->warning( |
256
|
|
|
"Copying between regions are not allowed by Rackspace and performed using local buffer." |
257
|
|
|
); |
258
|
|
|
|
259
|
|
|
//Using local memory/disk as buffer |
260
|
|
|
return parent::copy($bucket, $destination, $name); |
261
|
|
|
} |
262
|
|
|
|
263
|
|
|
try { |
264
|
|
|
$request = $this->buildRequest('PUT', $destination, $name, [ |
265
|
|
|
'X-Copy-From' => '/' . $bucket->getOPtion('container') . '/' . rawurlencode($name), |
266
|
|
|
'Content-Length' => 0 |
267
|
|
|
]); |
268
|
|
|
|
269
|
|
|
$this->client->send($request); |
270
|
|
|
} catch (ClientException $exception) { |
271
|
|
|
if ($exception->getCode() == 401) { |
272
|
|
|
$this->reconnect(); |
273
|
|
|
|
274
|
|
|
return $this->copy($bucket, $destination, $name); |
275
|
|
|
} |
276
|
|
|
|
277
|
|
|
throw new ServerException($exception->getMessage(), $exception->getCode(), $exception); |
278
|
|
|
} |
279
|
|
|
|
280
|
|
|
return true; |
281
|
|
|
} |
282
|
|
|
|
283
|
|
|
/** |
284
|
|
|
* Connect to rackspace servers using new or cached token. |
285
|
|
|
* |
286
|
|
|
* @throws ServerException |
287
|
|
|
*/ |
288
|
|
|
protected function connect() |
289
|
|
|
{ |
290
|
|
|
if (!empty($this->authToken)) { |
291
|
|
|
//Already got credentials from cache |
292
|
|
|
return; |
293
|
|
|
} |
294
|
|
|
|
295
|
|
|
//Credentials request |
296
|
|
|
$request = new Request( |
297
|
|
|
'POST', |
298
|
|
|
$this->options['authServer'], |
299
|
|
|
['Content-Type' => 'application/json'], |
300
|
|
|
json_encode([ |
301
|
|
|
'auth' => [ |
302
|
|
|
'RAX-KSKEY:apiKeyCredentials' => [ |
303
|
|
|
'username' => $this->options['username'], |
304
|
|
|
'apiKey' => $this->options['apiKey'] |
305
|
|
|
] |
306
|
|
|
] |
307
|
|
|
]) |
308
|
|
|
); |
309
|
|
|
|
310
|
|
|
try { |
311
|
|
|
/** |
312
|
|
|
* @var ResponseInterface $response |
313
|
|
|
*/ |
314
|
|
|
$response = $this->client->send($request); |
315
|
|
|
} catch (ClientException $exception) { |
316
|
|
|
if ($exception->getCode() == 401) { |
317
|
|
|
throw new ServerException( |
318
|
|
|
"Unable to perform Rackspace authorization using given credentials." |
319
|
|
|
); |
320
|
|
|
} |
321
|
|
|
|
322
|
|
|
throw new ServerException($exception->getMessage(), $exception->getCode(), $exception); |
323
|
|
|
} |
324
|
|
|
|
325
|
|
|
$response = json_decode((string)$response->getBody(), 1); |
326
|
|
|
foreach ($response['access']['serviceCatalog'] as $location) { |
327
|
|
|
if ($location['name'] == 'cloudFiles') { |
328
|
|
|
foreach ($location['endpoints'] as $server) { |
329
|
|
|
$this->regions[$server['region']] = $server['publicURL']; |
330
|
|
|
} |
331
|
|
|
} |
332
|
|
|
} |
333
|
|
|
|
334
|
|
|
if (!isset($response['access']['token']['id'])) { |
335
|
|
|
throw new ServerException("Unable to fetch rackspace auth token."); |
336
|
|
|
} |
337
|
|
|
|
338
|
|
|
$this->authToken = $response['access']['token']['id']; |
339
|
|
|
|
340
|
|
|
if ($this->options['cache']) { |
341
|
|
|
$this->store->set( |
342
|
|
|
$this->options['username'] . '@rackspace-token', |
343
|
|
|
$this->authToken, |
344
|
|
|
$this->options['lifetime'] |
345
|
|
|
); |
346
|
|
|
|
347
|
|
|
$this->store->set( |
348
|
|
|
$this->options['username'] . '@rackspace-regions', |
349
|
|
|
$this->regions, |
350
|
|
|
$this->options['lifetime'] |
351
|
|
|
); |
352
|
|
|
} |
353
|
|
|
} |
354
|
|
|
|
355
|
|
|
/** |
356
|
|
|
* Reconnect. |
357
|
|
|
* |
358
|
|
|
* @throws ServerException |
359
|
|
|
*/ |
360
|
|
|
protected function reconnect() |
361
|
|
|
{ |
362
|
|
|
$this->authToken = null; |
363
|
|
|
$this->connect(); |
364
|
|
|
} |
365
|
|
|
|
366
|
|
|
/** |
367
|
|
|
* Create instance of UriInterface based on provided bucket options and storage object name. |
368
|
|
|
* |
369
|
|
|
* @param BucketInterface $bucket |
370
|
|
|
* @param string $name |
371
|
|
|
* @return UriInterface |
372
|
|
|
* @throws ServerException |
373
|
|
|
*/ |
374
|
|
|
protected function buildUri(BucketInterface $bucket, $name) |
375
|
|
|
{ |
376
|
|
|
if (empty($bucket->getOption('region'))) { |
377
|
|
|
throw new ServerException("Every rackspace container should have specified region."); |
378
|
|
|
} |
379
|
|
|
|
380
|
|
|
$region = $bucket->getOption('region'); |
381
|
|
|
if (!isset($this->regions[$region])) { |
382
|
|
|
throw new ServerException("'{$region}' region is not supported by Rackspace."); |
383
|
|
|
} |
384
|
|
|
|
385
|
|
|
return new Uri( |
386
|
|
|
$this->regions[$region] . '/' . $bucket->getOption('container') . '/' . rawurlencode($name) |
387
|
|
|
); |
388
|
|
|
} |
389
|
|
|
|
390
|
|
|
/** |
391
|
|
|
* Create pre-configured object request. |
392
|
|
|
* |
393
|
|
|
* @param string $method |
394
|
|
|
* @param BucketInterface $bucket |
395
|
|
|
* @param string $name |
396
|
|
|
* @param array $headers |
397
|
|
|
* @return RequestInterface |
398
|
|
|
*/ |
399
|
|
|
protected function buildRequest($method, BucketInterface $bucket, $name, array $headers = []) |
400
|
|
|
{ |
401
|
|
|
//Adding auth headers |
402
|
|
|
$headers += [ |
403
|
|
|
'X-Auth-Token' => $this->authToken, |
404
|
|
|
'Date' => gmdate('D, d M Y H:i:s T') |
405
|
|
|
]; |
406
|
|
|
|
407
|
|
|
return new Request($method, $this->buildUri($bucket, $name), $headers); |
408
|
|
|
} |
409
|
|
|
} |
Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.
You can also find more detailed suggestions in the “Code” section of your repository.