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

AmazonServer::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, SpiralScout LLC.
4
 *
5
 * @package   spiralFramework
6
 * @author    Anton Titov (Wolfy-J)
7
 * @copyright ©2009-2011
8
 */
9
namespace Spiral\Storage\Servers;
10
11
use GuzzleHttp\Client;
12
use GuzzleHttp\ClientInterface;
13
use GuzzleHttp\Exception\ClientException;
14
use GuzzleHttp\Psr7\Request;
15
use GuzzleHttp\Psr7\Uri;
16
use Psr\Http\Message\RequestInterface;
17
use Psr\Http\Message\ResponseInterface;
18
use Psr\Http\Message\UriInterface;
19
use Spiral\Files\FilesInterface;
20
use Spiral\Storage\BucketInterface;
21
use Spiral\Storage\Exceptions\ServerException;
22
use Spiral\Storage\StorageServer;
23
24
/**
25
 * Provides abstraction level to work with data located in Amazon S3 cloud.
26
 */
27
class AmazonServer extends StorageServer
28
{
29
    /**
30
     * @var array
31
     */
32
    protected $options = [
33
        'server'    => 'https://s3.amazonaws.com',
34
        'timeout'   => 0,
35
        'accessKey' => '',
36
        'secretKey' => ''
37
    ];
38
39
    /**
40
     * @todo DI in constructor
41
     * @var ClientInterface
42
     */
43
    protected $client = null;
44
45
    /**
46
     * {@inheritdoc}
47
     */
48
    public function __construct(FilesInterface $files, array $options)
49
    {
50
        parent::__construct($files, $options);
51
52
        //This code is going to use additional abstraction layer to connect storage and guzzle
53
        $this->client = new Client($this->options);
54
    }
55
56
    /**
57
     * @param ClientInterface $client
58
     * @return $this
59
     */
60
    public function setClient(ClientInterface $client)
61
    {
62
        $this->client = $client;
63
64
        return $this;
65
    }
66
67
    /**
68
     * {@inheritdoc}
69
     *
70
     * @return bool|ResponseInterface
71
     */
72
    public function exists(BucketInterface $bucket, $name)
73
    {
74
        try {
75
            $response = $this->client->send($this->buildRequest('HEAD', $bucket, $name));
76
        } catch (ClientException $exception) {
77
            if ($exception->getCode() == 404) {
78
                return false;
79
            }
80
81
            //Something wrong with connection
82
            throw $exception;
83
        }
84
85
        if ($response->getStatusCode() !== 200) {
86
            return false;
87
        }
88
89
        return $response;
90
    }
91
92
    /**
93
     * {@inheritdoc}
94
     */
95 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...
96
    {
97
        if (empty($response = $this->exists($bucket, $name))) {
98
            return false;
99
        }
100
101
        return (int)$response->getHeaderLine('Content-Length');
102
    }
103
104
    /**
105
     * {@inheritdoc}
106
     */
107
    public function put(BucketInterface $bucket, $name, $source)
108
    {
109
        if (empty($mimetype = \GuzzleHttp\Psr7\mimetype_from_filename($name))) {
110
            $mimetype = self::DEFAULT_MIMETYPE;
111
        }
112
113
        $request = $this->buildRequest(
114
            'PUT',
115
            $bucket,
116
            $name,
117
            $this->createHeaders($bucket, $name, $source),
118
            [
119
                'Acl'          => $bucket->getOption('public') ? 'public-read' : 'private',
120
                'Content-Type' => $mimetype
121
            ]
122
        );
123
124
        $response = $this->client->send($request->withBody($this->castStream($source)));
125
        if ($response->getStatusCode() != 200) {
126
            throw new ServerException("Unable to put '{$name}' to Amazon server.");
127
        }
128
    }
129
130
    /**
131
     * {@inheritdoc}
132
     */
133
    public function allocateStream(BucketInterface $bucket, $name)
134
    {
135
        try {
136
            $response = $this->client->send($this->buildRequest('GET', $bucket, $name));
137
        } catch (ClientException $exception) {
138
            if ($exception->getCode() != 404) {
139
                //Some authorization or other error
140
                throw $exception;
141
            }
142
143
            throw new ServerException($exception->getMessage(), $exception->getCode(), $exception);
144
        }
145
146
        return $response->getBody();
147
    }
148
149
    /**
150
     * {@inheritdoc}
151
     */
152
    public function delete(BucketInterface $bucket, $name)
153
    {
154
        $this->client->send($this->buildRequest('DELETE', $bucket, $name));
155
    }
156
157
    /**
158
     * {@inheritdoc}
159
     */
160 View Code Duplication
    public function rename(BucketInterface $bucket, $oldname, $newname)
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...
161
    {
162
        try {
163
            $request = $this->buildRequest('PUT', $bucket, $newname, [], [
164
                'Acl'         => $bucket->getOption('public') ? 'public-read' : 'private',
165
                'Copy-Source' => $this->buildUri($bucket, $oldname)->getPath()
166
            ]);
167
168
            $this->client->send($request);
169
        } catch (ClientException $exception) {
170
            if ($exception->getCode() != 404) {
171
                //Some authorization or other error
172
                throw $exception;
173
            }
174
175
            throw new ServerException($exception->getMessage(), $exception->getCode(), $exception);
176
        }
177
178
        $this->delete($bucket, $oldname);
179
180
        return true;
181
    }
182
183
    /**
184
     * {@inheritdoc}
185
     */
186 View Code Duplication
    public function copy(BucketInterface $bucket, BucketInterface $destination, $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
            $request = $this->buildRequest('PUT', $destination, $name, [], [
190
                'Acl'         => $destination->getOption('public') ? 'public-read' : 'private',
191
                'Copy-Source' => $this->buildUri($bucket, $name)->getPath()
192
            ]);
193
194
            $this->client->send($request);
195
        } catch (ClientException $exception) {
196
            if ($exception->getCode() != 404) {
197
                //Some authorization or other error
198
                throw $exception;
199
            }
200
201
            throw new ServerException($exception->getMessage(), $exception->getCode(), $exception);
202
        }
203
204
        return true;
205
    }
206
207
    /**
208
     * Create instance of UriInterface based on provided bucket options and storage object name.
209
     *
210
     * @param BucketInterface $bucket
211
     * @param string          $name
212
     * @return UriInterface
213
     */
214
    protected function buildUri(BucketInterface $bucket, $name)
215
    {
216
        return new Uri(
217
            $this->options['server'] . '/' . $bucket->getOption('bucket') . '/' . rawurlencode($name)
218
        );
219
    }
220
221
    /**
222
     * Helper to create configured PSR7 request with set of amazon commands.
223
     *
224
     * @param string          $method
225
     * @param BucketInterface $bucket
226
     * @param string          $name
227
     * @param array           $headers
228
     * @param array           $commands Amazon commands associated with values.
229
     * @return RequestInterface
230
     */
231
    protected function buildRequest(
232
        $method,
233
        BucketInterface $bucket,
234
        $name,
235
        array $headers = [],
236
        array $commands = []
237
    ) {
238
        $headers += [
239
            'Date'         => gmdate('D, d M Y H:i:s T'),
240
            'Content-MD5'  => '',
241
            'Content-Type' => ''
242
        ];
243
244
        $packedCommands = $this->packCommands($commands);
245
246
        return $this->signRequest(
247
            new Request($method, $this->buildUri($bucket, $name), $headers + $packedCommands),
248
            $packedCommands
249
        );
250
    }
251
252
    /**
253
     * Generate request headers based on provided set of amazon commands.
254
     *
255
     * @param array $commands
256
     * @return array
257
     */
258
    private function packCommands(array $commands)
259
    {
260
        $headers = [];
261
        foreach ($commands as $command => $value) {
262
            $headers['X-Amz-' . $command] = $value;
263
        }
264
265
        return $headers;
266
    }
267
268
    /**
269
     * Sign amazon request.
270
     *
271
     * @param RequestInterface $request
272
     * @param array            $packedCommands Headers generated based on request commands, see
273
     *                                         packCommands() method for more information.
274
     * @return RequestInterface
275
     */
276
    private function signRequest(RequestInterface $request, array $packedCommands = [])
277
    {
278
        $signature = [
279
            $request->getMethod(),
280
            $request->getHeaderLine('Content-MD5'),
281
            $request->getHeaderLine('Content-Type'),
282
            $request->getHeaderLine('Date')
283
        ];
284
285
        $normalizedCommands = [];
286
        foreach ($packedCommands as $command => $value) {
287
            if (!empty($value)) {
288
                $normalizedCommands[] = strtolower($command) . ':' . $value;
289
            }
290
        }
291
292
        if ($normalizedCommands) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $normalizedCommands of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
293
            sort($normalizedCommands);
294
            $signature[] = join("\n", $normalizedCommands);
295
        }
296
297
        $signature[] = $request->getUri()->getPath();
298
299
        return $request->withAddedHeader(
300
            'Authorization',
301
            'AWS ' . $this->options['accessKey'] . ':' . base64_encode(
302
                hash_hmac('sha1', join("\n", $signature), $this->options['secretKey'], true)
303
            )
304
        );
305
    }
306
307
    /**
308
     * Generate object headers.
309
     *
310
     * @param BucketInterface $bucket
311
     * @param string          $name
312
     * @param mixed           $source
313
     * @return array
314
     */
315
    private function createHeaders(BucketInterface $bucket, $name, $source)
316
    {
317
        if (empty($mimetype = \GuzzleHttp\Psr7\mimetype_from_filename($name))) {
318
            $mimetype = self::DEFAULT_MIMETYPE;
319
        };
320
321
        $headers = $bucket->getOption('headers', []);
322
323
        if (!empty($maxAge = $bucket->getOption('maxAge', 0))) {
324
            //Shortcut
325
            $headers['Cache-control'] = 'max-age=' . $bucket->getOption('maxAge', 0) . ', public';
326
            $headers['Expires'] = gmdate(
327
                'D, d M Y H:i:s T',
328
                time() + $bucket->getOption('maxAge', 0)
329
            );
330
        }
331
332
        return $headers + [
333
            'Content-MD5'  => base64_encode(md5_file($this->castFilename($source), true)),
334
            'Content-Type' => $mimetype
335
        ];
336
    }
337
}