Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
94.39% covered (success)
94.39%
101 / 107
85.71% covered (success)
85.71%
24 / 28
CRAP
0.00% covered (danger)
0.00%
0 / 1
S3
94.39% covered (success)
94.39%
101 / 107
85.71% covered (success)
85.71%
24 / 28
63.70
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 setClient
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 getClient
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 hasClient
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 mkdir
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
1
 rmdir
70.00% covered (success)
70.00%
7 / 10
0.00% covered (danger)
0.00%
0 / 1
3.24
 listDirs
92.86% covered (success)
92.86%
13 / 14
0.00% covered (danger)
0.00%
0 / 1
8.02
 listFiles
91.67% covered (success)
91.67%
11 / 12
0.00% covered (danger)
0.00%
0 / 1
8.04
 putFile
75.00% covered (success)
75.00%
3 / 4
0.00% covered (danger)
0.00%
0 / 1
3.14
 putFileContents
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 uploadFile
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
3
 copyFile
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
2
 copyFileToExternal
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 copyFileFromExternal
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 moveFileToExternal
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 moveFileFromExternal
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 renameFile
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
2
 replaceFileContents
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 deleteFile
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 fetchFile
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
2
 fetchFileInfo
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
2
 fileExists
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 isDir
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 isFile
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getFileSize
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
2
 getFileType
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
2
 getFileMTime
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
2
 md5File
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
3
1<?php
2/**
3 * Pop PHP Framework (http://www.popphp.org/)
4 *
5 * @link       https://github.com/popphp/popphp-framework
6 * @author     Nick Sagona, III <dev@nolainteractive.com>
7 * @copyright  Copyright (c) 2009-2024 NOLA Interactive, LLC. (http://www.nolainteractive.com)
8 * @license    http://www.popphp.org/license     New BSD License
9 */
10
11/**
12 * @namespace
13 */
14namespace Pop\Storage\Adapter;
15
16use Aws\S3\S3Client;
17use RecursiveDirectoryIterator;
18use RecursiveIteratorIterator;
19
20/**
21 * Storage adapter S3 class
22 *
23 * @category   Pop
24 * @package    Pop\Storage
25 * @author     Nick Sagona, III <dev@nolainteractive.com>
26 * @copyright  Copyright (c) 2009-2024 NOLA Interactive, LLC. (http://www.nolainteractive.com)
27 * @license    http://www.popphp.org/license     New BSD License
28 * @version    2.0.0
29 */
30class 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->directory),
470                'Key'    => $this->scrub($filename),
471            ]);
472
473            return (isset($fileObject['ETag'])) ? str_replace('"', '', $fileObject['ETag']) : false;
474        } else {
475            return false;
476        }
477    }
478
479}