Completed
Branch develop (c2aa4c)
by Anton
05:17
created

AmazonServer::copy()   A

Complexity

Conditions 4
Paths 5

Size

Total Lines 20
Code Lines 11

Duplication

Lines 20
Ratio 100 %

Importance

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