Completed
Push — master ( e33acc...db7ab9 )
by Anton
05:34
created

RackspaceServer::setClient()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 6
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 1 Features 0
Metric Value
c 1
b 1
f 0
dl 0
loc 6
rs 9.4285
cc 1
eloc 3
nc 1
nop 1
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)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

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.

Loading history...
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)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

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.

Loading history...
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)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

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.

Loading history...
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
}