Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
90.46% covered (success)
90.46%
256 / 283
60.61% covered (warning)
60.61%
20 / 33
CRAP
0.00% covered (danger)
0.00%
0 / 1
Azure
90.46% covered (success)
90.46%
256 / 283
60.61% covered (warning)
60.61%
20 / 33
132.51
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 create
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 initClient
100.00% covered (success)
100.00%
17 / 17
100.00% covered (success)
100.00%
1 / 1
3
 setClient
100.00% covered (success)
100.00%
2 / 2
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
 setAuth
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 getAuth
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 hasAuth
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 mkdir
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 rmdir
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 listDirs
69.70% covered (warning)
69.70%
23 / 33
0.00% covered (danger)
0.00%
0 / 1
25.04
 listFiles
87.50% covered (success)
87.50%
21 / 24
0.00% covered (danger)
0.00%
0 / 1
13.33
 putFile
94.12% covered (success)
94.12%
16 / 17
0.00% covered (danger)
0.00%
0 / 1
4.00
 putFileContents
93.33% covered (success)
93.33%
14 / 15
0.00% covered (danger)
0.00%
0 / 1
3.00
 uploadFile
94.74% covered (success)
94.74%
18 / 19
0.00% covered (danger)
0.00%
0 / 1
6.01
 copyFile
94.44% covered (success)
94.44%
17 / 18
0.00% covered (danger)
0.00%
0 / 1
9.01
 copyFileToExternal
93.75% covered (success)
93.75%
15 / 16
0.00% covered (danger)
0.00%
0 / 1
8.02
 copyFileFromExternal
94.44% covered (success)
94.44%
17 / 18
0.00% covered (danger)
0.00%
0 / 1
6.01
 moveFileToExternal
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 moveFileFromExternal
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
3
 renameFile
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 replaceFileContents
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 deleteFile
92.31% covered (success)
92.31%
12 / 13
0.00% covered (danger)
0.00%
0 / 1
5.01
 fetchFile
84.62% covered (success)
84.62%
11 / 13
0.00% covered (danger)
0.00%
0 / 1
6.13
 fetchFileInfo
93.75% covered (success)
93.75%
15 / 16
0.00% covered (danger)
0.00%
0 / 1
4.00
 fileExists
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
2
 isDir
88.24% covered (success)
88.24%
15 / 17
0.00% covered (danger)
0.00%
0 / 1
5.04
 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
1
 getFileType
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
3
 getFileMTime
66.67% covered (warning)
66.67%
4 / 6
0.00% covered (danger)
0.00%
0 / 1
5.93
 md5File
100.00% covered (success)
100.00%
4 / 4
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 Pop\Storage\Adapter\Azure\Auth;
17use Pop\Http\Client;
18use Pop\Http\Client\Request;
19use Pop\Utils\File;
20
21/**
22 * Storage adapter Azure class
23 *
24 * @category   Pop
25 * @package    Pop\Storage
26 * @author     Nick Sagona, III <dev@nolainteractive.com>
27 * @copyright  Copyright (c) 2009-2024 NOLA Interactive, LLC. (http://www.nolainteractive.com)
28 * @license    http://www.popphp.org/license     New BSD License
29 * @version    2.0.0
30 */
31class Azure extends AbstractAdapter
32{
33
34    /**
35     * HTTP client
36     * @var ?Client
37     */
38    protected ?Client $client = null;
39
40    /**
41     * Azure auth object
42     * @var ?Auth
43     */
44    protected ?Auth $auth = null;
45
46    /**
47     * Constructor
48     *
49     * @param string $location
50     * @param Auth   $auth
51     */
52    public function __construct(string $location, Auth $auth)
53    {
54        parent::__construct($location);
55        $this->setAuth($auth);
56        $this->initClient();
57    }
58
59    /**
60     * Create Azure client
61     *
62     * @param  string $accountName
63     * @param  string $accountKey
64     * @return Azure
65     */
66    public static function create(string $accountName, string $accountKey): Azure
67    {
68        return new self($accountName, new Azure\Auth($accountName, $accountKey));
69    }
70
71    /**
72     * Initialize client
73     *
74     * @param  string $method
75     * @param  array  $headers
76     * @param  bool   $auto
77     * @return Azure
78     */
79    public function initClient(string $method = 'GET', array $headers = [], bool $auto = true): Azure
80    {
81        $request = new Request('/', $method);
82        $request->addHeader('Date', gmdate('D, d M Y H:i:s T'))
83            ->addHeader('Host', $this->auth->getAccountName() . '.blob.core.windows.net')
84            ->addHeader('Content-Type', Client\Request::URLFORM)
85            ->addHeader('User-Agent', 'pop-storage/2.0.0 (PHP ' . PHP_VERSION . ')/' . PHP_OS)
86            ->addHeader('x-ms-client-request-id', uniqid())
87            ->addHeader('x-ms-version', '2023-11-03');
88
89        if (!empty($headers)) {
90            foreach ($headers as $header => $value) {
91                $request->addHeader($header, $value);
92            }
93        }
94
95        $this->setClient(new Client(
96            $request, [
97                'base_uri' => $this->auth->getBaseUri(),
98                'auto'     => $auto
99            ]
100        ));
101
102        return $this;
103    }
104
105    /**
106     * Set client
107     *
108     * @param  Client $client
109     * @return Azure
110     */
111    public function setClient(Client $client): Azure
112    {
113        $this->client = $client;
114        return $this;
115    }
116
117    /**
118     * Get client
119     *
120     * @return ?Client
121     */
122    public function getClient(): ?Client
123    {
124        return $this->client;
125    }
126
127    /**
128     * Has client
129     *
130     * @return bool
131     */
132    public function hasClient(): bool
133    {
134        return ($this->client !== null);
135    }
136
137    /**
138     * Set auth
139     *
140     * @param  Auth $auth
141     * @return Azure
142     */
143    public function setAuth(Auth $auth): Azure
144    {
145        $this->auth = $auth;
146        return $this;
147    }
148
149    /**
150     * Get auth
151     *
152     * @return ?Auth
153     */
154    public function getAuth(): ?Auth
155    {
156        return $this->auth;
157    }
158
159    /**
160     * Has auth
161     *
162     * @return bool
163     */
164    public function hasAuth(): bool
165    {
166        return ($this->auth !== null);
167    }
168
169    /**
170     * Make directory
171     *
172     * @param  string $directory
173     * @return void
174     */
175    public function mkdir(string $directory): void
176    {
177        /**
178         * Azure storage doesn't allow the creation of empty "directories" (prefixes.)
179         * A new "directory" (prefix) is automatically created with an uploaded file that utilizes a prefix
180         */
181    }
182
183    /**
184     * Remove a directory
185     *
186     * @param  string $directory
187     * @return void
188     */
189    public function rmdir(string $directory): void
190    {
191        /**
192         * Azure storage doesn't allow the direct removal of "directories" (prefixes.)
193         * A "directory" (prefix) is automatically removed when the last file that utilizes the prefix is deleted.
194         */
195    }
196
197    /**
198     * List directories
199     *
200     * @param  ?string $search
201     * @return array
202     */
203    public function listDirs(?string $search = null): array
204    {
205        $dirs = [];
206
207        if ($this->baseDirectory == $this->directory) {
208            $this->initClient();
209            $this->client->getRequest()->setQuery(['comp' => 'list']);
210            $this->auth->signRequest($this->client->getRequest());
211
212            $response = $this->client->send();
213
214            if (is_array($response) && !empty($response['Containers'])) {
215                foreach ($response['Containers'] as $container) {
216                    $dirs[] = $container['Name'];
217                }
218            }
219        } else {
220            $container = str_replace($this->baseDirectory, '', $this->directory);
221
222            $params = ['restype' => 'container', 'comp' => 'list'];
223            if (substr_count($container, '/') > 1) {
224                $folders          = array_filter(explode('/', $container));
225                $container        = '/' . array_shift($folders);
226                $params['prefix'] = implode('/', $folders) . '/';
227            }
228
229            $this->initClient();
230            $this->client->getRequest()->setQuery($params);
231            $this->client->getRequest()->setUri($container);
232            $this->auth->signRequest($this->client->getRequest());
233
234            $response = $this->client->send();
235
236            if (is_array($response) && !empty($response['Blobs']) && !empty($response['Blobs']['Blob'])) {
237                $blobs = (!isset($response['Blobs']['Blob'][0])) ?
238                    [$response['Blobs']['Blob']] : $response['Blobs']['Blob'];
239                foreach ($blobs as $blob) {
240                    $name = (isset($params['prefix']) && str_starts_with($blob['Name'], $params['prefix'])) ?
241                        substr($blob['Name'], strlen($params['prefix'])) : $blob['Name'];
242                    if (!empty($name) && (substr_count($name, '/') >= 1)) {
243                        $folder = substr($name, 0, (strpos($name, '/') + 1));
244                        if (!in_array($folder, $dirs)) {
245                            $dirs[] = $folder;
246                        }
247                    }
248                }
249            }
250        }
251
252        if ($search !== null) {
253            $dirs = $this->searchFilter($dirs, $search);
254        }
255
256        return $dirs;
257    }
258
259    /**
260     * List files
261     *
262     * @param  ?string $search
263     * @return array
264     */
265    public function listFiles(?string $search = null): array
266    {
267        $files = [];
268
269        if ($this->baseDirectory !== $this->directory) {
270            $container = str_replace($this->baseDirectory, '', $this->directory);
271
272            $params = ['restype' => 'container', 'comp' => 'list'];
273            if (substr_count($container, '/') > 1) {
274                $folders          = array_filter(explode('/', $container));
275                $container        = '/' . array_shift($folders);
276                $params['prefix'] = implode('/', $folders) . '/';
277            }
278
279            $this->initClient();
280            $this->client->getRequest()->setQuery($params);
281            $this->client->getRequest()->setUri($container);
282            $this->auth->signRequest($this->client->getRequest());
283
284            $response = $this->client->send();
285
286            if (is_array($response) && !empty($response['Blobs']) && !empty($response['Blobs']['Blob'])) {
287                $blobs = (!isset($response['Blobs']['Blob'][0])) ?
288                    [$response['Blobs']['Blob']] : $response['Blobs']['Blob'];
289                foreach ($blobs as $blob) {
290                    $name = (isset($params['prefix']) && str_starts_with($blob['Name'], $params['prefix'])) ?
291                        substr($blob['Name'], strlen($params['prefix'])) : $blob['Name'];
292                    if (!empty($name) && !str_contains($name, '/')) {
293                        $files[] = $name;
294                    }
295                }
296            }
297        }
298
299        if ($search !== null) {
300            $files = $this->searchFilter($files, $search);
301        }
302
303        return $files;
304    }
305
306    /**
307     * Put file
308     *
309     * @param  string $fileFrom
310     * @param  bool $copy
311     * @throws Exception|Client\Handler\Exception|\Pop\Http\Exception|\Pop\Utils\Exception
312     * @return void
313     */
314    public function putFile(string $fileFrom, bool $copy = true): void
315    {
316        if (file_exists($fileFrom)) {
317            $uri = '/' . basename($fileFrom);
318            if ($this->baseDirectory !== $this->directory) {
319                $directory = str_replace($this->baseDirectory, '', $this->directory);
320                if (str_ends_with($directory, '/')) {
321                    $directory = substr($directory, 0, -1);
322                }
323                $uri = $directory . $uri;
324            }
325
326            $fileContents = file_get_contents($fileFrom);
327
328            $this->initClient('PUT', [
329                'content-length'         => strlen($fileContents),
330                'x-ms-blob-type'         => 'BlockBlob',
331                'x-ms-blob-content-type' => File::getFileMimeType($fileFrom)
332            ]);
333            $this->client->getRequest()->setUri($uri);
334            $this->client->getRequest()->setBody($fileContents);
335            $this->auth->signRequest($this->client->getRequest());
336            $this->client->send();
337        }
338    }
339
340    /**
341     * Put file contents
342     *
343     * @param  string $filename
344     * @param  string $fileContents
345     * @return void
346     */
347    public function putFileContents(string $filename, string $fileContents): void
348    {
349        $uri = '/' . $filename;
350        if ($this->baseDirectory !== $this->directory) {
351            $directory = str_replace($this->baseDirectory, '', $this->directory);
352            if (str_ends_with($directory, '/')) {
353                $directory = substr($directory, 0, -1);
354            }
355            $uri = $directory . $uri;
356        }
357
358        $this->initClient('PUT', [
359            'content-length'         => strlen($fileContents),
360            'x-ms-blob-type'         => 'BlockBlob',
361            'x-ms-blob-content-type' => File::getFileMimeType($filename)
362        ]);
363        $this->client->getRequest()->setUri($uri);
364        $this->client->getRequest()->setBody($fileContents);
365        $this->auth->signRequest($this->client->getRequest());
366        $this->client->send();
367    }
368
369    /**
370     * Upload file from server request $_FILES['file']
371     *
372     * @param  array $file
373     * @throws Exception
374     * @return void
375     */
376    public function uploadFile(array $file): void
377    {
378        if (!isset($file['tmp_name']) || !isset($file['name'])) {
379            throw new Exception('Error: The uploaded file array was not valid');
380        }
381        if (file_exists($file['tmp_name'])) {
382            $uri = '/' . $file['name'];
383            if ($this->baseDirectory !== $this->directory) {
384                $directory = str_replace($this->baseDirectory, '', $this->directory);
385                if (str_ends_with($directory, '/')) {
386                    $directory = substr($directory, 0, -1);
387                }
388                $uri = $directory . $uri;
389            }
390
391            $fileContents = file_get_contents($file['tmp_name']);
392
393            $this->initClient('PUT', [
394                'content-length'         => strlen($fileContents),
395                'x-ms-blob-type'         => 'BlockBlob',
396                'x-ms-blob-content-type' => File::getFileMimeType($file['name'])
397            ]);
398            $this->client->getRequest()->setUri($uri);
399            $this->client->getRequest()->setBody($fileContents);
400            $this->auth->signRequest($this->client->getRequest());
401            $this->client->send();
402        }
403    }
404
405    /**
406     * Copy file
407     *
408     * @param  string $sourceFile
409     * @param  string $destFile
410     * @return void
411     */
412    public function copyFile(string $sourceFile, string $destFile): void
413    {
414        $sourceFileInfo = $this->fetchFileInfo($sourceFile);
415
416        if (is_array($sourceFileInfo) && isset($sourceFileInfo['headers']) &&
417            isset($sourceFileInfo['headers']['Content-Type']) && (!$sourceFileInfo['isError'])) {
418            $sourceUri = (!str_starts_with($sourceFile, '/')) ? '/' . $sourceFile : $sourceFile;
419            $destUri   = (!str_starts_with($destFile, '/')) ? '/' . $destFile : $destFile;
420
421            if ($this->baseDirectory !== $this->directory) {
422                $directory = str_replace($this->baseDirectory, '', $this->directory);
423                if (str_ends_with($directory, '/')) {
424                    $directory = substr($directory, 0, -1);
425                }
426                $sourceUri = $directory . $sourceUri;
427                $destUri   = $directory . $destUri;
428            }
429
430            $this->initClient('PUT', [
431                'content-length'   => $sourceFileInfo['headers']['Content-Length'],
432                'x-ms-copy-source' => $this->auth->getBaseUri() . $sourceUri,
433            ]);
434            $this->client->getRequest()->setUri($destUri);
435            $this->auth->signRequest($this->client->getRequest());
436            $this->client->send();
437        }
438    }
439
440    /**
441     * Copy file to a location external to the current location
442     *
443     * @param  string $sourceFile
444     * @param  string $externalFile
445     * @return void
446     */
447    public function copyFileToExternal(string $sourceFile, string $externalFile): void
448    {
449        $sourceFileInfo = $this->fetchFileInfo($sourceFile);
450
451        if (is_array($sourceFileInfo) && isset($sourceFileInfo['headers']) &&
452            isset($sourceFileInfo['headers']['Content-Type']) && (!$sourceFileInfo['isError'])) {
453            $sourceUri = (!str_starts_with($sourceFile, '/')) ? '/' . $sourceFile : $sourceFile;
454
455            if ($this->baseDirectory !== $this->directory) {
456                $directory = str_replace($this->baseDirectory, '', $this->directory);
457                if (str_ends_with($directory, '/')) {
458                    $directory = substr($directory, 0, -1);
459                }
460                $sourceUri = $directory . $sourceUri;
461            }
462
463            $this->initClient('PUT', [
464                'content-length'   => $sourceFileInfo['headers']['Content-Length'],
465                'x-ms-copy-source' => $this->auth->getBaseUri() . $sourceUri,
466            ]);
467            $this->client->getRequest()->setUri($externalFile);
468            $this->auth->signRequest($this->client->getRequest());
469            $this->client->send();
470        }
471    }
472
473    /**
474     * Copy file from a location external to the current location
475     *
476     * @param  string $externalFile
477     * @param  string $destFile
478     * @return void
479     */
480    public function copyFileFromExternal(string $externalFile, string $destFile): void
481    {
482        $this->initClient('HEAD', [], false);
483        $this->client->getRequest()->setUri($externalFile);
484        $this->auth->signRequest($this->client->getRequest());
485        $response = $this->client->send();
486
487        if (($response->isSuccess()) && ($response->hasHeader('Content-Length'))) {
488            $destUri = (!str_starts_with($destFile, '/')) ? '/' . $destFile : $destFile;
489
490            if ($this->baseDirectory !== $this->directory) {
491                $directory = str_replace($this->baseDirectory, '', $this->directory);
492                if (str_ends_with($directory, '/')) {
493                    $directory = substr($directory, 0, -1);
494                }
495                $destUri   = $directory . $destUri;
496            }
497
498            $this->initClient('PUT', [
499                'content-length'   => $response->getHeader('Content-Length')->getValueAsString(),
500                'x-ms-copy-source' => $this->auth->getBaseUri() . $externalFile,
501            ]);
502            $this->client->getRequest()->setUri($destUri);
503            $this->auth->signRequest($this->client->getRequest());
504            $this->client->send();
505        }
506    }
507
508    /**
509     * Move file to a location external to the current location
510     *
511     * @param  string $sourceFile
512     * @param  string $externalFile
513     * @return void
514     */
515    public function moveFileToExternal(string $sourceFile, string $externalFile): void
516    {
517        $this->copyFileToExternal($sourceFile, $externalFile);
518        $this->deleteFile($sourceFile);
519    }
520
521    /**
522     * Move file from a location external to the current location
523     *
524     * @param  string  $externalFile
525     * @param  string  $destFile
526     * @param  ?string $snapshots ['include', 'only', null]
527     * @return void
528     */
529    public function moveFileFromExternal(string $externalFile, string $destFile, ?string $snapshots = 'include'): void
530    {
531        $this->copyFileFromExternal($externalFile, $destFile);
532
533        $headers = [];
534        if ($snapshots !== null) {
535            $headers['x-ms-delete-snapshots'] = ($snapshots == 'only') ? 'only' : 'include';
536        }
537
538        $this->initClient('DELETE', $headers);
539        $this->client->getRequest()->setUri($externalFile);
540        $this->auth->signRequest($this->client->getRequest());
541        $this->client->send();
542    }
543
544    /**
545     * Rename file
546     *
547     * @param  string $oldFile
548     * @param  string $newFile
549     * @return void
550     */
551    public function renameFile(string $oldFile, string $newFile): void
552    {
553        $this->copyFile($oldFile, $newFile);
554        $this->deleteFile($oldFile);
555    }
556
557    /**
558     * Replace file
559     *
560     * @param  string $filename
561     * @param  string $fileContents
562     * @return void
563     */
564    public function replaceFileContents(string $filename, string $fileContents): void
565    {
566        $this->putFileContents($filename, $fileContents);
567    }
568
569    /**
570     * Delete file
571     *
572     * @param  string  $filename
573     * @param  ?string $snapshots ['include', 'only', null]
574     * @return void
575     */
576    public function deleteFile(string $filename, ?string $snapshots = 'include'): void
577    {
578        $uri = '/' . $filename;
579        if ($this->baseDirectory !== $this->directory) {
580            $directory = str_replace($this->baseDirectory, '', $this->directory);
581            if (str_ends_with($directory, '/')) {
582                $directory = substr($directory, 0, -1);
583            }
584            $uri = $directory . $uri;
585        }
586
587        $headers = [];
588        if ($snapshots !== null) {
589            $headers['x-ms-delete-snapshots'] = ($snapshots == 'only') ? 'only' : 'include';
590        }
591
592        $this->initClient('DELETE', $headers);
593        $this->client->getRequest()->setUri($uri);
594        $this->auth->signRequest($this->client->getRequest());
595        $this->client->send();
596    }
597
598    /**
599     * Fetch file
600     *
601     * @param  string $filename
602     * @param  bool   $raw
603     * @return mixed
604     */
605    public function fetchFile(string $filename, bool $raw = true): mixed
606    {
607        $filename = (!str_starts_with($filename, '/')) ? '/' . $filename : $filename;
608
609        if ($this->baseDirectory !== $this->directory) {
610            $directory = str_replace($this->baseDirectory, '', $this->directory);
611            if (str_ends_with($directory, '/')) {
612                $directory = substr($directory, 0, -1);
613            }
614            $filename = $directory . $filename;
615        }
616
617        $this->initClient('GET', [], false);
618        $this->client->getRequest()->setUri($filename);
619        $this->auth->signRequest($this->client->getRequest());
620        $response = $this->client->send();
621
622        if ($response->isSuccess()) {
623            return ($raw) ? $response->getBody()->getContent(): $response;
624        } else {
625            return null;
626        }
627    }
628
629    /**
630     * Fetch file info
631     *
632     * @param  string $filename
633     * @return array
634     */
635    public function fetchFileInfo(string $filename): array
636    {
637        $filename = (!str_starts_with($filename, '/')) ? '/' . $filename : $filename;
638
639        if ($this->baseDirectory !== $this->directory) {
640            $directory = str_replace($this->baseDirectory, '', $this->directory);
641            if (str_ends_with($directory, '/')) {
642                $directory = substr($directory, 0, -1);
643            }
644            $filename = $directory . $filename;
645        }
646
647        $this->initClient('HEAD', [], false);
648        $this->client->getRequest()->setUri($filename);
649        $this->auth->signRequest($this->client->getRequest());
650        $response = $this->client->send();
651
652        return [
653            'code'    => $response->getCode(),
654            'message' => $response->getMessage(),
655            'headers' => $response->getHeadersAsArray(),
656            'isError' => $response->isError()
657        ];
658    }
659
660    /**
661     * File exists
662     *
663     * @param  string $filename
664     * @return bool
665     */
666    public function fileExists(string $filename): bool
667    {
668        $info = $this->fetchFileInfo($filename);
669        return (isset($info['code']) && ((int)$info['code'] == 200));
670    }
671
672    /**
673     * Check if is a dir
674     *
675     * @param  string $directory
676     * @return bool
677     */
678    public function isDir(string $directory): bool
679    {
680        if (str_starts_with($directory, '/')) {
681            $directory = substr($directory, 1);
682        }
683        if (str_ends_with($directory, '/')) {
684            $directory = substr($directory, 0, -1);
685        }
686
687        $directories  = explode('/', $directory);
688        $result       = false;
689        $curDirectory = $this->directory;
690        $dirString    = substr(str_replace($this->baseDirectory, '', $this->directory), 1) . '/';
691        foreach ($directories as $directory) {
692            $dirs       = $this->listDirs();
693            $dirString .= $directory . '/';
694            if (in_array($directory . '/', $dirs)) {
695                $result = true;
696            } else {
697                $result = false;
698            }
699            $this->chdir($dirString);
700        }
701
702        $this->directory = $curDirectory;
703
704        return $result;
705    }
706
707    /**
708     * Check if is a file
709     *
710     * @param  string $filename
711     * @return bool
712     */
713    public function isFile(string $filename): bool
714    {
715        return $this->fileExists($filename);
716    }
717
718    /**
719     * Get file size
720     *
721     * @param  string $filename
722     * @return int|bool
723     */
724    public function getFileSize(string $filename): int|bool
725    {
726        $info = $this->fetchFileInfo($filename);
727        return $info['headers']['Content-Length'] ?? false;
728    }
729
730    /**
731     * Get file type
732     *
733     * @param  string $filename
734     * @return string|bool
735     */
736    public function getFileType(string $filename): string|bool
737    {
738        if ($this->isFile($filename)) {
739            return 'file';
740        } else if ($this->isDir($filename)) {
741            return 'dir';
742        } else {
743            return false;
744        }
745    }
746
747    /**
748     * Get file modified time
749     *
750     * @param  string $filename
751     * @return int|string|bool
752     */
753    public function getFileMTime(string $filename): int|string|bool
754    {
755        $info = $this->fetchFileInfo($filename);
756        if (isset($info['headers']) && !empty($info['headers']['Last-Modified'])) {
757            return $info['headers']['Last-Modified'];
758        } else if (isset($info['headers']) && !empty($info['headers']['x-ms-creation-time'])) {
759            return $info['headers']['x-ms-creation-time'];
760        } else {
761            return false;
762        }
763    }
764
765    /**
766     * Create MD5 checksum of the file
767     *
768     * @param  string $filename
769     * @return string|bool
770     */
771    public function md5File(string $filename): string|bool
772    {
773        $info = $this->fetchFileInfo($filename);
774        if (isset($info['headers']) && !empty($info['headers']['Content-MD5'])) {
775            return $info['headers']['Content-MD5'];
776        } else {
777            return false;
778        }
779    }
780
781}