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) |
|
|
|
|
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) |
|
|
|
|
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) |
|
|
|
|
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) { |
|
|
|
|
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
|
|
|
} |
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.