Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
81.20% covered (success)
81.20%
216 / 266
60.61% covered (warning)
60.61%
20 / 33
CRAP
0.00% covered (danger)
0.00%
0 / 1
Azure
81.20% covered (success)
81.20%
216 / 266
60.61% covered (warning)
60.61%
20 / 33
226.12
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
83.33% covered (success)
83.33%
20 / 24
0.00% covered (danger)
0.00%
0 / 1
17.19
 listFiles
83.33% covered (success)
83.33%
20 / 24
0.00% covered (danger)
0.00%
0 / 1
17.19
 putFile
76.47% covered (success)
76.47%
13 / 17
0.00% covered (danger)
0.00%
0 / 1
4.21
 putFileContents
73.33% covered (success)
73.33%
11 / 15
0.00% covered (danger)
0.00%
0 / 1
3.17
 uploadFile
78.95% covered (success)
78.95%
15 / 19
0.00% covered (danger)
0.00%
0 / 1
6.34
 copyFile
72.22% covered (success)
72.22%
13 / 18
0.00% covered (danger)
0.00%
0 / 1
10.74
 copyFileToExternal
75.00% covered (success)
75.00%
12 / 16
0.00% covered (danger)
0.00%
0 / 1
9.00
 copyFileFromExternal
77.78% covered (success)
77.78%
14 / 18
0.00% covered (danger)
0.00%
0 / 1
6.40
 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
69.23% covered (warning)
69.23%
9 / 13
0.00% covered (danger)
0.00%
0 / 1
5.73
 fetchFile
61.54% covered (warning)
61.54%
8 / 13
0.00% covered (danger)
0.00%
0 / 1
8.05
 fetchFileInfo
75.00% covered (success)
75.00%
12 / 16
0.00% covered (danger)
0.00%
0 / 1
4.25
 fileExists
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
2
 isDir
71.43% covered (success)
71.43%
5 / 7
0.00% covered (danger)
0.00%
0 / 1
5.58
 isFile
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
3
 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 (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 */
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@noladev.com>
27 * @copyright  Copyright (c) 2009-2025 NOLA Interactive, LLC.
28 * @license    https://www.popphp.org/license     New BSD License
29 * @version    2.1.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::URLENCODED)
85            ->addHeader('User-Agent', 'pop-storage/2.1.0 (PHP ' . PHP_VERSION . ')/' . PHP_OS)
86            ->addHeader('x-ms-client-request-id', uniqid())
87            ->addHeader('x-ms-version', '2025-01-05');
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        $uri = '/' . $this->baseDirectory;
208
209        $params = ['restype' => 'container', 'comp' => 'list'];
210
211        if ($this->baseDirectory !== $this->directory) {
212            $directory = str_replace($this->baseDirectory, '', $this->directory);
213            if (str_ends_with($directory, '/')) {
214                $directory = substr($directory, 0, -1);
215            }
216            $params['prefix'] = $directory;
217        }
218
219        $this->initClient();
220        $this->client->getRequest()->setQuery($params);
221        $this->client->getRequest()->setUri($uri);
222        $this->auth->signRequest($this->client->getRequest());
223
224        $response = $this->client->send();
225
226        if (is_array($response) && !empty($response['Blobs']) && !empty($response['Blobs']['Blob'])) {
227            $blobs = (!isset($response['Blobs']['Blob'][0])) ? [$response['Blobs']['Blob']] : $response['Blobs']['Blob'];
228            foreach ($blobs as $blob) {
229                if (isset($blob['Properties']) && isset($blob['Properties']['ResourceType']) &&
230                    ($blob['Properties']['ResourceType'] == 'directory')) {
231                    if ((!isset($params['prefix']) && !str_contains($blob['Name'], '/')) ||
232                        (isset($params['prefix']) && str_contains($blob['Name'], '/'))) {
233                        $dirs[] = $blob['Name'];
234                    }
235                }
236            }
237        }
238
239        if ($search !== null) {
240            $dirs = $this->searchFilter($dirs, $search);
241        }
242
243        return $dirs;
244    }
245
246    /**
247     * List files
248     *
249     * @param  ?string $search
250     * @return array
251     */
252    public function listFiles(?string $search = null): array
253    {
254        $files = [];
255
256        $uri = '/' . $this->baseDirectory;
257
258        $params = ['restype' => 'container', 'comp' => 'list'];
259
260        if ($this->baseDirectory !== $this->directory) {
261            $directory = str_replace($this->baseDirectory, '', $this->directory);
262            if (str_ends_with($directory, '/')) {
263                $directory = substr($directory, 0, -1);
264            }
265            $params['prefix'] = $directory;
266        }
267
268        $this->initClient();
269        $this->client->getRequest()->setQuery($params);
270        $this->client->getRequest()->setUri($uri);
271        $this->auth->signRequest($this->client->getRequest());
272
273        $response = $this->client->send();
274
275        if (is_array($response) && !empty($response['Blobs']) && !empty($response['Blobs']['Blob'])) {
276            $blobs = (!isset($response['Blobs']['Blob'][0])) ? [$response['Blobs']['Blob']] : $response['Blobs']['Blob'];
277            foreach ($blobs as $blob) {
278                if (isset($blob['Properties']) && isset($blob['Properties']['ResourceType']) &&
279                    ($blob['Properties']['ResourceType'] == 'file')) {
280                    if ((!isset($params['prefix']) && !str_contains($blob['Name'], '/')) ||
281                        (isset($params['prefix']) && str_contains($blob['Name'], '/'))) {
282                        $files[] = $blob['Name'];
283                    }
284                }
285            }
286        }
287
288        if ($search !== null) {
289            $files = $this->searchFilter($files, $search);
290        }
291
292        return $files;
293    }
294
295    /**
296     * Put file
297     *
298     * @param  string $fileFrom
299     * @param  bool $copy
300     * @throws Exception|Client\Handler\Exception|\Pop\Http\Exception|\Pop\Utils\Exception
301     * @return void
302     */
303    public function putFile(string $fileFrom, bool $copy = true): void
304    {
305        if (file_exists($fileFrom)) {
306            $uri = '/' . $this->baseDirectory . '/' . basename($fileFrom);
307            if ($this->baseDirectory !== $this->directory) {
308                $directory = str_replace($this->baseDirectory, '', $this->directory);
309                if (str_ends_with($directory, '/')) {
310                    $directory = substr($directory, 0, -1);
311                }
312                $uri = $directory . $uri;
313            }
314
315            $fileContents = file_get_contents($fileFrom);
316
317            $this->initClient('PUT', [
318                'content-length'         => strlen($fileContents),
319                'x-ms-blob-type'         => 'BlockBlob',
320                'x-ms-blob-content-type' => File::getFileMimeType($fileFrom)
321            ]);
322            $this->client->getRequest()->setUri($uri);
323            $this->client->getRequest()->setBody($fileContents);
324            $this->auth->signRequest($this->client->getRequest());
325            $this->client->send();
326        }
327    }
328
329    /**
330     * Put file contents
331     *
332     * @param  string $filename
333     * @param  string $fileContents
334     * @return void
335     */
336    public function putFileContents(string $filename, string $fileContents): void
337    {
338        $uri = '/' . $this->baseDirectory . '/' . $filename;
339        if ($this->baseDirectory !== $this->directory) {
340            $directory = str_replace($this->baseDirectory, '', $this->directory);
341            if (str_ends_with($directory, '/')) {
342                $directory = substr($directory, 0, -1);
343            }
344            $uri = $directory . $uri;
345        }
346
347        $this->initClient('PUT', [
348            'content-length'         => strlen($fileContents),
349            'x-ms-blob-type'         => 'BlockBlob',
350            'x-ms-blob-content-type' => File::getFileMimeType($filename)
351        ]);
352        $this->client->getRequest()->setUri($uri);
353        $this->client->getRequest()->setBody($fileContents);
354        $this->auth->signRequest($this->client->getRequest());
355        $this->client->send();
356    }
357
358    /**
359     * Upload file from server request $_FILES['file']
360     *
361     * @param  array $file
362     * @throws Exception
363     * @return void
364     */
365    public function uploadFile(array $file): void
366    {
367        if (!isset($file['tmp_name']) || !isset($file['name'])) {
368            throw new Exception('Error: The uploaded file array was not valid');
369        }
370        if (file_exists($file['tmp_name'])) {
371            $uri = '/' . $this->baseDirectory . '/' . $file['name'];
372            if ($this->baseDirectory !== $this->directory) {
373                $directory = str_replace($this->baseDirectory, '', $this->directory);
374                if (str_ends_with($directory, '/')) {
375                    $directory = substr($directory, 0, -1);
376                }
377                $uri = $directory . $uri;
378            }
379
380            $fileContents = file_get_contents($file['tmp_name']);
381
382            $this->initClient('PUT', [
383                'content-length'         => strlen($fileContents),
384                'x-ms-blob-type'         => 'BlockBlob',
385                'x-ms-blob-content-type' => File::getFileMimeType($file['name'])
386            ]);
387            $this->client->getRequest()->setUri($uri);
388            $this->client->getRequest()->setBody($fileContents);
389            $this->auth->signRequest($this->client->getRequest());
390            $this->client->send();
391        }
392    }
393
394    /**
395     * Copy file
396     *
397     * @param  string $sourceFile
398     * @param  string $destFile
399     * @return void
400     */
401    public function copyFile(string $sourceFile, string $destFile): void
402    {
403        $sourceFileInfo = $this->fetchFileInfo($sourceFile);
404
405        if (is_array($sourceFileInfo) && isset($sourceFileInfo['headers']) &&
406            isset($sourceFileInfo['headers']['Content-Type']) && (!$sourceFileInfo['isError'])) {
407            $sourceUri = (!str_starts_with($sourceFile, '/')) ? '/' . $this->baseDirectory . '/' . $sourceFile : $sourceFile;
408            $destUri   = (!str_starts_with($destFile, '/')) ? '/' . $this->baseDirectory . '/' . $destFile : $destFile;
409
410            if ($this->baseDirectory !== $this->directory) {
411                $directory = str_replace($this->baseDirectory, '', $this->directory);
412                if (str_ends_with($directory, '/')) {
413                    $directory = substr($directory, 0, -1);
414                }
415                $sourceUri = $directory . $sourceUri;
416                $destUri   = $directory . $destUri;
417            }
418
419            $this->initClient('PUT', [
420                'content-length'   => $sourceFileInfo['headers']['Content-Length'],
421                'x-ms-copy-source' => $this->auth->getBaseUri() . $sourceUri,
422            ]);
423            $this->client->getRequest()->setUri($destUri);
424            $this->auth->signRequest($this->client->getRequest());
425            $this->client->send();
426        }
427    }
428
429    /**
430     * Copy file to a location external to the current location
431     *
432     * @param  string $sourceFile
433     * @param  string $externalFile
434     * @return void
435     */
436    public function copyFileToExternal(string $sourceFile, string $externalFile): void
437    {
438        $sourceFileInfo = $this->fetchFileInfo($sourceFile);
439
440        if (is_array($sourceFileInfo) && isset($sourceFileInfo['headers']) &&
441            isset($sourceFileInfo['headers']['Content-Type']) && (!$sourceFileInfo['isError'])) {
442            $sourceUri = (!str_starts_with($sourceFile, '/')) ? '/' . $this->baseDirectory . '/' . $sourceFile : $sourceFile;
443
444            if ($this->baseDirectory !== $this->directory) {
445                $directory = str_replace($this->baseDirectory, '', $this->directory);
446                if (str_ends_with($directory, '/')) {
447                    $directory = substr($directory, 0, -1);
448                }
449                $sourceUri = $directory . $sourceUri;
450            }
451
452            $this->initClient('PUT', [
453                'content-length'   => $sourceFileInfo['headers']['Content-Length'],
454                'x-ms-copy-source' => $this->auth->getBaseUri() . $sourceUri,
455            ]);
456            $this->client->getRequest()->setUri($externalFile);
457            $this->auth->signRequest($this->client->getRequest());
458            $this->client->send();
459        }
460    }
461
462    /**
463     * Copy file from a location external to the current location
464     *
465     * @param  string $externalFile
466     * @param  string $destFile
467     * @return void
468     */
469    public function copyFileFromExternal(string $externalFile, string $destFile): void
470    {
471        $this->initClient('HEAD', [], false);
472        $this->client->getRequest()->setUri($externalFile);
473        $this->auth->signRequest($this->client->getRequest());
474        $response = $this->client->send();
475
476        if (($response->isSuccess()) && ($response->hasHeader('Content-Length'))) {
477            $destUri = (!str_starts_with($destFile, '/')) ? '/' . $this->baseDirectory . '/' . $destFile : $destFile;
478
479            if ($this->baseDirectory !== $this->directory) {
480                $directory = str_replace($this->baseDirectory, '', $this->directory);
481                if (str_ends_with($directory, '/')) {
482                    $directory = substr($directory, 0, -1);
483                }
484                $destUri   = $directory . $destUri;
485            }
486
487            $this->initClient('PUT', [
488                'content-length'   => $response->getHeader('Content-Length')->getValueAsString(),
489                'x-ms-copy-source' => $this->auth->getBaseUri() . $externalFile,
490            ]);
491            $this->client->getRequest()->setUri($destUri);
492            $this->auth->signRequest($this->client->getRequest());
493            $this->client->send();
494        }
495    }
496
497    /**
498     * Move file to a location external to the current location
499     *
500     * @param  string $sourceFile
501     * @param  string $externalFile
502     * @return void
503     */
504    public function moveFileToExternal(string $sourceFile, string $externalFile): void
505    {
506        $this->copyFileToExternal($sourceFile, $externalFile);
507        $this->deleteFile($sourceFile);
508    }
509
510    /**
511     * Move file from a location external to the current location
512     *
513     * @param  string  $externalFile
514     * @param  string  $destFile
515     * @param  ?string $snapshots ['include', 'only', null]
516     * @return void
517     */
518    public function moveFileFromExternal(string $externalFile, string $destFile, ?string $snapshots = 'include'): void
519    {
520        $this->copyFileFromExternal($externalFile, $destFile);
521
522        $headers = [];
523        if ($snapshots !== null) {
524            $headers['x-ms-delete-snapshots'] = ($snapshots == 'only') ? 'only' : 'include';
525        }
526
527        $this->initClient('DELETE', $headers);
528        $this->client->getRequest()->setUri($externalFile);
529        $this->auth->signRequest($this->client->getRequest());
530        $this->client->send();
531    }
532
533    /**
534     * Rename file
535     *
536     * @param  string $oldFile
537     * @param  string $newFile
538     * @return void
539     */
540    public function renameFile(string $oldFile, string $newFile): void
541    {
542        $this->copyFile($oldFile, $newFile);
543        $this->deleteFile($oldFile);
544    }
545
546    /**
547     * Replace file
548     *
549     * @param  string $filename
550     * @param  string $fileContents
551     * @return void
552     */
553    public function replaceFileContents(string $filename, string $fileContents): void
554    {
555        $this->putFileContents($filename, $fileContents);
556    }
557
558    /**
559     * Delete file
560     *
561     * @param  string  $filename
562     * @param  ?string $snapshots ['include', 'only', null]
563     * @return void
564     */
565    public function deleteFile(string $filename, ?string $snapshots = 'include'): void
566    {
567        $uri = '/' . $this->baseDirectory . '/' . $filename;
568        if ($this->baseDirectory !== $this->directory) {
569            $directory = str_replace($this->baseDirectory, '', $this->directory);
570            if (str_ends_with($directory, '/')) {
571                $directory = substr($directory, 0, -1);
572            }
573            $uri = $directory . $uri;
574        }
575
576        $headers = [];
577        if ($snapshots !== null) {
578            $headers['x-ms-delete-snapshots'] = ($snapshots == 'only') ? 'only' : 'include';
579        }
580
581        $this->initClient('DELETE', $headers);
582        $this->client->getRequest()->setUri($uri);
583        $this->auth->signRequest($this->client->getRequest());
584        $this->client->send();
585    }
586
587    /**
588     * Fetch file
589     *
590     * @param  string $filename
591     * @param  bool   $raw
592     * @return mixed
593     */
594    public function fetchFile(string $filename, bool $raw = true): mixed
595    {
596        $filename = (!str_starts_with($filename, '/')) ? '/' . $this->baseDirectory . '/' . $filename : $filename;
597
598        if ($this->baseDirectory !== $this->directory) {
599            $directory = str_replace($this->baseDirectory, '', $this->directory);
600            if (str_ends_with($directory, '/')) {
601                $directory = substr($directory, 0, -1);
602            }
603            $filename = $directory . $filename;
604        }
605
606        $this->initClient('GET', [], false);
607        $this->client->getRequest()->setUri($filename);
608        $this->auth->signRequest($this->client->getRequest());
609        $response = $this->client->send();
610
611        if ($response->isSuccess()) {
612            return ($raw) ? $response->getBody()->getContent(): $response;
613        } else {
614            return null;
615        }
616    }
617
618    /**
619     * Fetch file info
620     *
621     * @param  string $filename
622     * @return array
623     */
624    public function fetchFileInfo(string $filename): array
625    {
626        $filename = (!str_starts_with($filename, '/')) ? '/' . $this->baseDirectory . '/' . $filename : $filename;
627
628        if ($this->baseDirectory !== $this->directory) {
629            $directory = str_replace($this->baseDirectory, '', $this->directory);
630            if (str_ends_with($directory, '/')) {
631                $directory = substr($directory, 0, -1);
632            }
633            $filename = $directory . $filename;
634        }
635
636        $this->initClient('HEAD', [], false);
637        $this->client->getRequest()->setUri($filename);
638        $this->auth->signRequest($this->client->getRequest());
639        $response = $this->client->send();
640
641        return [
642            'code'    => $response->getCode(),
643            'message' => $response->getMessage(),
644            'headers' => $response->getHeadersAsArray(),
645            'isError' => $response->isError()
646        ];
647    }
648
649    /**
650     * File exists
651     *
652     * @param  string $filename
653     * @return bool
654     */
655    public function fileExists(string $filename): bool
656    {
657        $info = $this->fetchFileInfo($filename);
658        return (isset($info['code']) && ((int)$info['code'] == 200));
659    }
660
661    /**
662     * Check if is a dir
663     *
664     * @param  string $directory
665     * @return bool
666     */
667    public function isDir(string $directory): bool
668    {
669        if (str_starts_with($directory, '/')) {
670            $directory = substr($directory, 1);
671        }
672        if (str_ends_with($directory, '/')) {
673            $directory = substr($directory, 0, -1);
674        }
675        $info = $this->fetchFileInfo($directory);
676        return (isset($info['headers']) && isset($info['headers']['x-ms-resource-type']) &&
677            $info['headers']['x-ms-resource-type'] == 'directory');
678    }
679
680    /**
681     * Check if is a file
682     *
683     * @param  string $filename
684     * @return bool
685     */
686    public function isFile(string $filename): bool
687    {
688        $info = $this->fetchFileInfo($filename);
689        return (isset($info['headers']) && isset($info['headers']['x-ms-resource-type']) &&
690            $info['headers']['x-ms-resource-type'] == 'file');
691    }
692
693    /**
694     * Get file size
695     *
696     * @param  string $filename
697     * @return int|bool
698     */
699    public function getFileSize(string $filename): int|bool
700    {
701        $info = $this->fetchFileInfo($filename);
702        return $info['headers']['Content-Length'] ?? false;
703    }
704
705    /**
706     * Get file type
707     *
708     * @param  string $filename
709     * @return string|bool
710     */
711    public function getFileType(string $filename): string|bool
712    {
713        if ($this->isFile($filename)) {
714            return 'file';
715        } else if ($this->isDir($filename)) {
716            return 'dir';
717        } else {
718            return false;
719        }
720    }
721
722    /**
723     * Get file modified time
724     *
725     * @param  string $filename
726     * @return int|string|bool
727     */
728    public function getFileMTime(string $filename): int|string|bool
729    {
730        $info = $this->fetchFileInfo($filename);
731        if (isset($info['headers']) && !empty($info['headers']['Last-Modified'])) {
732            return $info['headers']['Last-Modified'];
733        } else if (isset($info['headers']) && !empty($info['headers']['x-ms-creation-time'])) {
734            return $info['headers']['x-ms-creation-time'];
735        } else {
736            return false;
737        }
738    }
739
740    /**
741     * Create MD5 checksum of the file
742     *
743     * @param  string $filename
744     * @return string|bool
745     */
746    public function md5File(string $filename): string|bool
747    {
748        $info = $this->fetchFileInfo($filename);
749        if (isset($info['headers']) && !empty($info['headers']['Content-MD5'])) {
750            return $info['headers']['Content-MD5'];
751        } else {
752            return false;
753        }
754    }
755
756}