Completed
Push — master ( 3ac5c4...483436 )
by Anton
03:47
created

AmazonServer   B

Complexity

Total Complexity 36

Size/Duplication

Total Lines 340
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 11

Importance

Changes 0
Metric Value
dl 0
loc 340
rs 8.8
c 0
b 0
f 0
wmc 36
lcom 1
cbo 11

14 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 10 1
A setClient() 0 6 1
B exists() 0 22 4
A size() 0 11 2
B put() 0 24 4
A allocateStream() 0 15 3
A delete() 0 8 2
B rename() 0 22 4
A copy() 0 20 4
A buildUri() 0 6 1
A buildRequest() 0 20 1
A packCommands() 0 9 2
B signRequest() 0 32 4
A createHeaders() 0 23 3
1
<?php
2
/**
3
 * Spiral Framework, SpiralScout LLC.
4
 *
5
 * @package   spiralFramework
6
 * @author    Anton Titov (Wolfy-J)
7
 * @copyright ©2009-2011
8
 */
9
10
namespace Spiral\Storage\Servers;
11
12
use GuzzleHttp\Client;
13
use GuzzleHttp\ClientInterface;
14
use GuzzleHttp\Exception\ClientException;
15
use GuzzleHttp\Psr7\Request;
16
use GuzzleHttp\Psr7\Uri;
17
use Psr\Http\Message\RequestInterface;
18
use Psr\Http\Message\ResponseInterface;
19
use Psr\Http\Message\StreamInterface;
20
use Psr\Http\Message\UriInterface;
21
use Spiral\Files\FilesInterface;
22
use Spiral\Storage\BucketInterface;
23
use Spiral\Storage\Exceptions\ServerException;
24
25
/**
26
 * Provides abstraction level to work with data located in Amazon S3 cloud.
27
 */
28
class AmazonServer extends AbstractServer
29
{
30
    /**
31
     * @invisible
32
     * @var array
33
     */
34
    protected $options = [
35
        'server'    => 'https://s3.amazonaws.com',
36
        'timeout'   => 0,
37
        'accessKey' => '',
38
        'secretKey' => ''
39
    ];
40
41
    /**
42
     * @invisible
43
     * @var ClientInterface
44
     */
45
    protected $client = null;
46
47
    /**
48
     * @param array                $options
49
     * @param FilesInterface|null  $files
50
     * @param ClientInterface|null $client
51
     */
52
    public function __construct(
53
        array $options,
54
        FilesInterface $files = null,
55
        ClientInterface $client = null
56
    ) {
57
        parent::__construct($options, $files);
58
59
        //This code is going to use additional abstraction layer to connect storage and guzzle
60
        $this->setClient($client ?? new Client($this->options));
61
    }
62
63
    /**
64
     * @param ClientInterface $client
65
     *
66
     * @return self
67
     */
68
    public function setClient(ClientInterface $client): AmazonServer
69
    {
70
        $this->client = $client;
71
72
        return $this;
73
    }
74
75
    /**
76
     * {@inheritdoc}
77
     *
78
     * @param ResponseInterface $response Reference.
79
     *
80
     * @return bool|ResponseInterface
81
     */
82
    public function exists(
83
        BucketInterface $bucket,
84
        string $name,
85
        ResponseInterface &$response = null
86
    ): bool {
87
        try {
88
            $response = $this->client->send($this->buildRequest('HEAD', $bucket, $name));
89
        } catch (ClientException $e) {
90
            if ($e->getCode() == 404) {
91
                return false;
92
            }
93
94
            //Something wrong with connection
95
            throw $e;
96
        }
97
98
        if ($response->getStatusCode() !== 200) {
99
            return false;
100
        }
101
102
        return true;
103
    }
104
105
    /**
106
     * {@inheritdoc}
107
     */
108
    public function size(BucketInterface $bucket, string $name)
109
    {
110
        if (!$this->exists($bucket, $name, $response)) {
111
            return null;
112
        }
113
114
        /**
115
         * @var ResponseInterface $response
116
         */
117
        return (int)$response->getHeaderLine('Content-Length');
118
    }
119
120
    /**
121
     * {@inheritdoc}
122
     */
123
    public function put(BucketInterface $bucket, string $name, $source): bool
124
    {
125
        if (empty($mimetype = \GuzzleHttp\Psr7\mimetype_from_filename($name))) {
126
            $mimetype = self::DEFAULT_MIMETYPE;
127
        }
128
129
        $request = $this->buildRequest(
130
            'PUT',
131
            $bucket,
132
            $name,
133
            $this->createHeaders($bucket, $name, $source),
134
            [
135
                'Acl'          => $bucket->getOption('public') ? 'public-read' : 'private',
136
                'Content-Type' => $mimetype
137
            ]
138
        );
139
140
        $response = $this->client->send($request->withBody($this->castStream($source)));
141
        if ($response->getStatusCode() != 200) {
142
            throw new ServerException("Unable to put '{$name}' to Amazon server");
143
        }
144
145
        return true;
146
    }
147
148
    /**
149
     * {@inheritdoc}
150
     */
151
    public function allocateStream(BucketInterface $bucket, string $name): StreamInterface
152
    {
153
        try {
154
            $response = $this->client->send($this->buildRequest('GET', $bucket, $name));
155
        } catch (ClientException $e) {
156
            if ($e->getCode() != 404) {
157
                //Some authorization or other error
158
                throw $e;
159
            }
160
161
            throw new ServerException($e->getMessage(), $e->getCode(), $e);
162
        }
163
164
        return $response->getBody();
165
    }
166
167
    /**
168
     * {@inheritdoc}
169
     */
170
    public function delete(BucketInterface $bucket, string $name)
171
    {
172
        if (!$this->exists($bucket, $name)) {
173
            throw new ServerException("Unable to delete object, file not found");
174
        }
175
176
        $this->client->send($this->buildRequest('DELETE', $bucket, $name));
177
    }
178
179
    /**
180
     * {@inheritdoc}
181
     */
182
    public function rename(BucketInterface $bucket, string $oldName, string $newName): bool
183
    {
184
        try {
185
            $request = $this->buildRequest('PUT', $bucket, $newName, [], [
186
                'Acl'         => $bucket->getOption('public') ? 'public-read' : 'private',
187
                'Copy-Source' => $this->buildUri($bucket, $oldName)->getPath()
188
            ]);
189
190
            $this->client->send($request);
191
        } catch (ClientException $e) {
192
            if ($e->getCode() != 404) {
193
                //Some authorization or other error
194
                throw $e;
195
            }
196
197
            throw new ServerException($e->getMessage(), $e->getCode(), $e);
198
        }
199
200
        $this->delete($bucket, $oldName);
201
202
        return true;
203
    }
204
205
    /**
206
     * {@inheritdoc}
207
     */
208
    public function copy(BucketInterface $bucket, BucketInterface $destination, string $name): bool
209
    {
210
        try {
211
            $request = $this->buildRequest('PUT', $destination, $name, [], [
212
                'Acl'         => $destination->getOption('public') ? 'public-read' : 'private',
213
                'Copy-Source' => $this->buildUri($bucket, $name)->getPath()
214
            ]);
215
216
            $this->client->send($request);
217
        } catch (ClientException $e) {
218
            if ($e->getCode() != 404) {
219
                //Some authorization or other error
220
                throw $e;
221
            }
222
223
            throw new ServerException($e->getMessage(), $e->getCode(), $e);
224
        }
225
226
        return true;
227
    }
228
229
    /**
230
     * Create instance of UriInterface based on provided bucket options and storage object name.
231
     *
232
     * @param BucketInterface $bucket
233
     * @param string          $name
234
     *
235
     * @return UriInterface
236
     */
237
    protected function buildUri(BucketInterface $bucket, string $name): UriInterface
238
    {
239
        return new Uri(
240
            $this->options['server'] . '/' . $bucket->getOption('bucket') . '/' . rawurlencode($name)
241
        );
242
    }
243
244
    /**
245
     * Helper to create configured PSR7 request with set of amazon commands.
246
     *
247
     * @param string          $method
248
     * @param BucketInterface $bucket
249
     * @param string          $name
250
     * @param array           $headers
251
     * @param array           $commands Amazon commands associated with values.
252
     *
253
     * @return RequestInterface
254
     */
255
    protected function buildRequest(
256
        string $method,
257
        BucketInterface $bucket,
258
        string $name,
259
        array $headers = [],
260
        array $commands = []
261
    ): RequestInterface {
262
        $headers += [
263
            'Date'         => gmdate('D, d M Y H:i:s T'),
264
            'Content-MD5'  => '',
265
            'Content-Type' => ''
266
        ];
267
268
        $packedCommands = $this->packCommands($commands);
269
270
        return $this->signRequest(
271
            new Request($method, $this->buildUri($bucket, $name), $headers + $packedCommands),
272
            $packedCommands
273
        );
274
    }
275
276
    /**
277
     * Generate request headers based on provided set of amazon commands.
278
     *
279
     * @param array $commands
280
     *
281
     * @return array
282
     */
283
    private function packCommands(array $commands): array
284
    {
285
        $headers = [];
286
        foreach ($commands as $command => $value) {
287
            $headers['X-Amz-' . $command] = $value;
288
        }
289
290
        return $headers;
291
    }
292
293
    /**
294
     * Sign amazon request.
295
     *
296
     * @param RequestInterface $request
297
     * @param array            $packedCommands Headers generated based on request commands, see
298
     *                                         packCommands() method for more information.
299
     *
300
     * @return RequestInterface
301
     */
302
    private function signRequest(
303
        RequestInterface $request,
304
        array $packedCommands = []
305
    ): RequestInterface {
306
        $signature = [
307
            $request->getMethod(),
308
            $request->getHeaderLine('Content-MD5'),
309
            $request->getHeaderLine('Content-Type'),
310
            $request->getHeaderLine('Date')
311
        ];
312
313
        $normalizedCommands = [];
314
        foreach ($packedCommands as $command => $value) {
315
            if (!empty($value)) {
316
                $normalizedCommands[] = strtolower($command) . ':' . $value;
317
            }
318
        }
319
320
        if (!empty($normalizedCommands)) {
321
            sort($normalizedCommands);
322
            $signature[] = join("\n", $normalizedCommands);
323
        }
324
325
        $signature[] = $request->getUri()->getPath();
326
327
        return $request->withAddedHeader(
328
            'Authorization',
329
            'AWS ' . $this->options['accessKey'] . ':' . base64_encode(
330
                hash_hmac('sha1', join("\n", $signature), $this->options['secretKey'], true)
331
            )
332
        );
333
    }
334
335
    /**
336
     * Generate object headers.
337
     *
338
     * @param BucketInterface $bucket
339
     * @param string          $name
340
     * @param mixed           $source
341
     *
342
     * @return array
343
     */
344
    private function createHeaders(BucketInterface $bucket, string $name, $source): array
345
    {
346
        if (empty($mimetype = \GuzzleHttp\Psr7\mimetype_from_filename($name))) {
347
            $mimetype = self::DEFAULT_MIMETYPE;
348
        };
349
350
        //Possible to add custom headers into the bucket
351
        $headers = $bucket->getOption('headers', []);
352
353
        if (!empty($maxAge = $bucket->getOption('maxAge', 0))) {
354
            //Shortcut
355
            $headers['Cache-control'] = 'max-age=' . $bucket->getOption('maxAge', 0) . ', public';
356
            $headers['Expires'] = gmdate(
357
                'D, d M Y H:i:s T',
358
                time() + $bucket->getOption('maxAge', 0)
359
            );
360
        }
361
362
        return $headers + [
363
                'Content-MD5'  => base64_encode(md5_file($this->castFilename($source), true)),
364
                'Content-Type' => $mimetype
365
            ];
366
    }
367
}