Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
94.39% |
101 / 107 |
|
85.71% |
24 / 28 |
CRAP | |
0.00% |
0 / 1 |
S3 | |
94.39% |
101 / 107 |
|
85.71% |
24 / 28 |
63.70 | |
0.00% |
0 / 1 |
__construct | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
setClient | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
1 | |||
getClient | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
hasClient | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
mkdir | |
100.00% |
5 / 5 |
|
100.00% |
1 / 1 |
1 | |||
rmdir | |
70.00% |
7 / 10 |
|
0.00% |
0 / 1 |
3.24 | |||
listDirs | |
92.86% |
13 / 14 |
|
0.00% |
0 / 1 |
8.02 | |||
listFiles | |
91.67% |
11 / 12 |
|
0.00% |
0 / 1 |
8.04 | |||
putFile | |
75.00% |
3 / 4 |
|
0.00% |
0 / 1 |
3.14 | |||
putFileContents | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
uploadFile | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
3 | |||
copyFile | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
2 | |||
copyFileToExternal | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
2 | |||
copyFileFromExternal | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
2 | |||
moveFileToExternal | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
2 | |||
moveFileFromExternal | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
2 | |||
renameFile | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
2 | |||
replaceFileContents | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
2 | |||
deleteFile | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
2 | |||
fetchFile | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
2 | |||
fetchFileInfo | |
100.00% |
7 / 7 |
|
100.00% |
1 / 1 |
2 | |||
fileExists | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
isDir | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
isFile | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getFileSize | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
2 | |||
getFileType | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
2 | |||
getFileMTime | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
2 | |||
md5File | |
100.00% |
7 / 7 |
|
100.00% |
1 / 1 |
3 |
1 | <?php |
2 | /** |
3 | * Pop PHP Framework (https://www.popphp.org/) |
4 | * |
5 | * @link https://github.com/popphp/popphp-framework |
6 | * @author Nick Sagona, III <dev@noladev.com> |
7 | * @copyright Copyright (c) 2009-2025 NOLA Interactive, LLC. |
8 | * @license https://www.popphp.org/license New BSD License |
9 | */ |
10 | |
11 | /** |
12 | * @namespace |
13 | */ |
14 | namespace Pop\Storage\Adapter; |
15 | |
16 | use Aws\S3\S3Client; |
17 | use RecursiveDirectoryIterator; |
18 | use RecursiveIteratorIterator; |
19 | |
20 | /** |
21 | * Storage adapter S3 class |
22 | * |
23 | * @category Pop |
24 | * @package Pop\Storage |
25 | * @author Nick Sagona, III <dev@noladev.com> |
26 | * @copyright Copyright (c) 2009-2025 NOLA Interactive, LLC. |
27 | * @license https://www.popphp.org/license New BSD License |
28 | * @version 2.1.0 |
29 | */ |
30 | class S3 extends AbstractAdapter |
31 | { |
32 | |
33 | /** |
34 | * S3 client |
35 | * @var ?S3Client |
36 | */ |
37 | protected ?S3Client $client = null; |
38 | |
39 | /** |
40 | * Constructor |
41 | * |
42 | * @param string $directory |
43 | * @param S3Client $client |
44 | */ |
45 | public function __construct(string $directory, S3Client $client) |
46 | { |
47 | parent::__construct($directory); |
48 | $this->setClient($client); |
49 | } |
50 | |
51 | /** |
52 | * Set S3 client |
53 | * |
54 | * @param S3Client $client |
55 | * @return S3 |
56 | */ |
57 | public function setClient(S3Client $client): S3 |
58 | { |
59 | $this->client = $client; |
60 | $this->client->registerStreamWrapper(); |
61 | return $this; |
62 | } |
63 | |
64 | /** |
65 | * Get S3 client |
66 | * |
67 | * @return ?S3Client |
68 | */ |
69 | public function getClient(): ?S3Client |
70 | { |
71 | return $this->client; |
72 | } |
73 | |
74 | /** |
75 | * Has S3 client |
76 | * |
77 | * @return bool |
78 | */ |
79 | public function hasClient(): bool |
80 | { |
81 | return ($this->client !== null); |
82 | } |
83 | |
84 | /** |
85 | * Make directory |
86 | * |
87 | * @param string $directory |
88 | * @return void |
89 | */ |
90 | public function mkdir(string $directory): void |
91 | { |
92 | $this->client->putObject([ |
93 | 'Bucket' => str_replace('s3://', '', $this->directory), |
94 | 'Key' => $this->scrub($directory) . '/', |
95 | 'Body' => '' |
96 | ]); |
97 | } |
98 | |
99 | /** |
100 | * Remove a directory |
101 | * |
102 | * @param string $directory |
103 | * @throws \Pop\Dir\Exception |
104 | * @return void |
105 | */ |
106 | public function rmdir(string $directory): void |
107 | { |
108 | $directory = $this->directory . DIRECTORY_SEPARATOR . $this->scrub($directory); |
109 | $iterator = new RecursiveIteratorIterator( |
110 | new RecursiveDirectoryIterator($directory, RecursiveDirectoryIterator::SKIP_DOTS), |
111 | RecursiveIteratorIterator::CHILD_FIRST |
112 | ); |
113 | |
114 | foreach ($iterator as $fileInfo) { |
115 | if ($fileInfo->isDir()) { |
116 | rmdir((string)$fileInfo); |
117 | } else { |
118 | unlink((string)$fileInfo); |
119 | } |
120 | } |
121 | |
122 | rmdir($directory); |
123 | } |
124 | |
125 | /** |
126 | * List directories |
127 | * |
128 | * @param ?string $search |
129 | * @return array |
130 | */ |
131 | public function listDirs(?string $search = null): array |
132 | { |
133 | $dirs = []; |
134 | $params = ['Bucket' => str_replace('s3://', '', $this->baseDirectory)]; |
135 | |
136 | if ($this->baseDirectory != $this->directory) { |
137 | $params['Prefix'] = str_replace($this->baseDirectory . '/', '', $this->directory . '/'); |
138 | } |
139 | |
140 | $objects = $this->client->listObjects($params); |
141 | |
142 | foreach ($objects['Contents'] as $object) { |
143 | if ($object['Size'] == 0) { |
144 | $key = (isset($params['Prefix']) && str_starts_with($object['Key'], $params['Prefix'])) ? |
145 | substr($object['Key'], strlen($params['Prefix'])) : $object['Key']; |
146 | if (substr_count($key, '/') == 1) { |
147 | $dirs[] = $key; |
148 | } |
149 | } |
150 | } |
151 | |
152 | if ($search !== null) { |
153 | $dirs = $this->searchFilter($dirs, $search); |
154 | } |
155 | |
156 | return $dirs; |
157 | |
158 | } |
159 | |
160 | /** |
161 | * List files |
162 | * |
163 | * @param ?string $search |
164 | * @return array |
165 | */ |
166 | public function listFiles(?string $search = null): array |
167 | { |
168 | $files = []; |
169 | $params = ['Bucket' => str_replace('s3://', '', $this->baseDirectory), 'Delimiter' => '/']; |
170 | |
171 | if ($this->baseDirectory != $this->directory) { |
172 | $params['Prefix'] = str_replace($this->baseDirectory . '/', '', $this->directory . '/'); |
173 | } |
174 | |
175 | $objects = $this->client->listObjects($params); |
176 | |
177 | foreach ($objects['Contents'] as $object) { |
178 | if (!isset($params['Prefix']) || ($object['Key'] != $params['Prefix'])) { |
179 | $files[] = (isset($params['Prefix']) && str_starts_with($object['Key'], $params['Prefix'])) ? |
180 | substr($object['Key'], strlen($params['Prefix'])) : $object['Key']; |
181 | } |
182 | } |
183 | |
184 | if ($search !== null) { |
185 | $files = $this->searchFilter($files, $search); |
186 | } |
187 | |
188 | return $files; |
189 | } |
190 | |
191 | /** |
192 | * Put file |
193 | * |
194 | * @param string $fileFrom |
195 | * @param bool $copy |
196 | * @return void |
197 | */ |
198 | public function putFile(string $fileFrom, bool $copy = true): void |
199 | { |
200 | if (file_exists($fileFrom)) { |
201 | if ($copy) { |
202 | copy($fileFrom, $this->directory . DIRECTORY_SEPARATOR . basename($fileFrom)); |
203 | } else { |
204 | rename($fileFrom, $this->directory . DIRECTORY_SEPARATOR . basename($fileFrom)); |
205 | } |
206 | } |
207 | } |
208 | |
209 | /** |
210 | * Put file contents |
211 | * |
212 | * @param string $filename |
213 | * @param string $fileContents |
214 | * @return void |
215 | */ |
216 | public function putFileContents(string $filename, string $fileContents): void |
217 | { |
218 | file_put_contents($this->directory . DIRECTORY_SEPARATOR . $this->scrub($filename), $fileContents); |
219 | } |
220 | |
221 | /** |
222 | * Upload file from server request $_FILES['file'] |
223 | * |
224 | * @param array $file |
225 | * @throws Exception |
226 | * @return void |
227 | */ |
228 | public function uploadFile(array $file): void |
229 | { |
230 | if (!isset($file['tmp_name']) || !isset($file['name'])) { |
231 | throw new Exception('Error: The uploaded file array was not valid'); |
232 | } |
233 | |
234 | file_put_contents($this->directory . DIRECTORY_SEPARATOR . $file['name'], file_get_contents($file['tmp_name'])); |
235 | } |
236 | |
237 | /** |
238 | * Copy file |
239 | * |
240 | * @param string $sourceFile |
241 | * @param string $destFile |
242 | * @return void |
243 | */ |
244 | public function copyFile(string $sourceFile, string $destFile): void |
245 | { |
246 | $sourceFile = $this->directory . DIRECTORY_SEPARATOR . $this->scrub($sourceFile); |
247 | $destFile = $this->directory . DIRECTORY_SEPARATOR . $this->scrub($destFile); |
248 | if (file_exists($sourceFile)) { |
249 | copy($sourceFile, $destFile); |
250 | } |
251 | } |
252 | |
253 | /** |
254 | * Copy file to a location external to the current location |
255 | * |
256 | * @param string $sourceFile |
257 | * @param string $externalFile |
258 | * @return void |
259 | */ |
260 | public function copyFileToExternal(string $sourceFile, string $externalFile): void |
261 | { |
262 | $sourceFile = $this->directory . DIRECTORY_SEPARATOR . $this->scrub($sourceFile); |
263 | if (file_exists($sourceFile)) { |
264 | copy($sourceFile, $externalFile); |
265 | } |
266 | } |
267 | |
268 | /** |
269 | * Copy file from a location external to the current location |
270 | * |
271 | * @param string $externalFile |
272 | * @param string $destFile |
273 | * @return void |
274 | */ |
275 | public function copyFileFromExternal(string $externalFile, string $destFile): void |
276 | { |
277 | $destFile = $this->directory . DIRECTORY_SEPARATOR . $this->scrub($destFile); |
278 | if (file_exists($externalFile)) { |
279 | copy($externalFile, $destFile); |
280 | } |
281 | } |
282 | |
283 | /** |
284 | * Move file to a location external to the current location |
285 | * |
286 | * @param string $sourceFile |
287 | * @param string $externalFile |
288 | * @return void |
289 | */ |
290 | public function moveFileToExternal(string $sourceFile, string $externalFile): void |
291 | { |
292 | $oldFile = $this->directory . DIRECTORY_SEPARATOR . $this->scrub($sourceFile); |
293 | if (file_exists($oldFile)) { |
294 | rename($oldFile, $externalFile); |
295 | } |
296 | } |
297 | |
298 | /** |
299 | * Move file from a location external to the current location |
300 | * |
301 | * @param string $externalFile |
302 | * @param string $destFile |
303 | * @return void |
304 | */ |
305 | public function moveFileFromExternal(string $externalFile, string $destFile): void |
306 | { |
307 | $destFile = $this->directory . DIRECTORY_SEPARATOR . $this->scrub($destFile); |
308 | if (file_exists($externalFile)) { |
309 | rename($externalFile, $destFile); |
310 | } |
311 | } |
312 | |
313 | /** |
314 | * Rename file |
315 | * |
316 | * @param string $oldFile |
317 | * @param string $newFile |
318 | * @return void |
319 | */ |
320 | public function renameFile(string $oldFile, string $newFile): void |
321 | { |
322 | $oldFile = $this->directory . DIRECTORY_SEPARATOR . $this->scrub($oldFile); |
323 | $newFile = $this->directory . DIRECTORY_SEPARATOR . $this->scrub($newFile); |
324 | if (file_exists($oldFile)) { |
325 | rename($oldFile, $newFile); |
326 | } |
327 | } |
328 | |
329 | /** |
330 | * Replace file |
331 | * |
332 | * @param string $filename |
333 | * @param string $fileContents |
334 | * @return void |
335 | */ |
336 | public function replaceFileContents(string $filename, string $fileContents): void |
337 | { |
338 | $filename = $this->directory . DIRECTORY_SEPARATOR . $this->scrub($filename); |
339 | if (file_exists($filename)) { |
340 | file_put_contents($filename, $fileContents); |
341 | } |
342 | } |
343 | |
344 | /** |
345 | * Delete file |
346 | * |
347 | * @param string $filename |
348 | * @return void |
349 | */ |
350 | public function deleteFile(string $filename): void |
351 | { |
352 | $filename = $this->directory . DIRECTORY_SEPARATOR . $this->scrub($filename); |
353 | if (file_exists($filename)) { |
354 | unlink($filename); |
355 | } |
356 | } |
357 | |
358 | /** |
359 | * Fetch file |
360 | * |
361 | * @param string $filename |
362 | * @return mixed |
363 | */ |
364 | public function fetchFile(string $filename): mixed |
365 | { |
366 | $filename = $this->directory . DIRECTORY_SEPARATOR . $this->scrub($filename); |
367 | return (file_exists($filename)) ? file_get_contents($filename) : false; |
368 | } |
369 | |
370 | /** |
371 | * Fetch file info |
372 | * |
373 | * @param string $filename |
374 | * @return array |
375 | */ |
376 | public function fetchFileInfo(string $filename): array |
377 | { |
378 | if (file_exists($this->directory . DIRECTORY_SEPARATOR . $this->scrub($filename))) { |
379 | $fileObject = $this->client->headObject([ |
380 | 'Bucket' => str_replace('s3://', '', $this->directory), |
381 | 'Key' => $this->scrub($filename), |
382 | ]); |
383 | |
384 | return $fileObject->toArray(); |
385 | } else { |
386 | return []; |
387 | } |
388 | } |
389 | |
390 | /** |
391 | * File exists |
392 | * |
393 | * @param string $filename |
394 | * @return bool |
395 | */ |
396 | public function fileExists(string $filename): bool |
397 | { |
398 | return file_exists($this->directory . DIRECTORY_SEPARATOR . $this->scrub($filename)); |
399 | } |
400 | |
401 | /** |
402 | * Check if is a dir |
403 | * |
404 | * @param string $directory |
405 | * @return bool |
406 | */ |
407 | public function isDir(string $directory): bool |
408 | { |
409 | return is_dir($this->directory . DIRECTORY_SEPARATOR . $this->scrub($directory)); |
410 | } |
411 | |
412 | /** |
413 | * Check if is a file |
414 | * |
415 | * @param string $filename |
416 | * @return bool |
417 | */ |
418 | public function isFile(string $filename): bool |
419 | { |
420 | return is_file($this->directory . DIRECTORY_SEPARATOR . $this->scrub($filename)); |
421 | } |
422 | |
423 | /** |
424 | * Get file size |
425 | * |
426 | * @param string $filename |
427 | * @return int|bool |
428 | */ |
429 | public function getFileSize(string $filename): int|bool |
430 | { |
431 | $filename = $this->directory . DIRECTORY_SEPARATOR . $this->scrub($filename); |
432 | return (file_exists($filename)) ? filesize($filename) : false; |
433 | } |
434 | |
435 | /** |
436 | * Get file type |
437 | * |
438 | * @param string $filename |
439 | * @return string|bool |
440 | */ |
441 | public function getFileType(string $filename): string|bool |
442 | { |
443 | $filename = $this->directory . DIRECTORY_SEPARATOR . $this->scrub($filename); |
444 | return (file_exists($filename)) ? filetype($filename) : false; |
445 | } |
446 | |
447 | /** |
448 | * Get file modified time |
449 | * |
450 | * @param string $filename |
451 | * @return int|string|bool |
452 | */ |
453 | public function getFileMTime(string $filename): int|string|bool |
454 | { |
455 | $filename = $this->directory . DIRECTORY_SEPARATOR . $this->scrub($filename); |
456 | return (file_exists($filename)) ? filemtime($filename) : false; |
457 | } |
458 | |
459 | /** |
460 | * Create MD5 checksum of the file |
461 | * |
462 | * @param string $filename |
463 | * @return string|bool |
464 | */ |
465 | public function md5File(string $filename): string|bool |
466 | { |
467 | if (file_exists($this->directory . DIRECTORY_SEPARATOR . $this->scrub($filename))) { |
468 | $fileObject = $this->client->getObject([ |
469 | 'Bucket' => str_replace('s3://', '', $this->baseDirectory), |
470 | 'Key' => str_replace($this->baseDirectory . '/', '', $this->directory . '/') . $this->scrub($filename), |
471 | ]); |
472 | |
473 | return (isset($fileObject['ETag'])) ? str_replace('"', '', $fileObject['ETag']) : false; |
474 | } else { |
475 | return false; |
476 | } |
477 | } |
478 | |
479 | } |