zend-blog-3-backend

Форк
0
407 строк · 13.5 Кб
1
<?php
2
/**
3
 * User: morontt
4
 * Date: 18.10.2024
5
 * Time: 21:52
6
 */
7

8
namespace App\Utils\Flysystem;
9

10
use League\Flysystem\Config;
11
use League\Flysystem\DirectoryAttributes;
12
use League\Flysystem\FileAttributes;
13
use League\Flysystem\FilesystemAdapter;
14
use League\Flysystem\PathPrefixer;
15
use League\Flysystem\UnableToCheckFileExistence;
16
use League\Flysystem\UnableToCopyFile;
17
use League\Flysystem\UnableToCreateDirectory;
18
use League\Flysystem\UnableToDeleteDirectory;
19
use League\Flysystem\UnableToDeleteFile;
20
use League\Flysystem\UnableToMoveFile;
21
use League\Flysystem\UnableToReadFile;
22
use League\Flysystem\UnableToRetrieveMetadata;
23
use League\Flysystem\UnableToSetVisibility;
24
use League\Flysystem\UnableToWriteFile;
25
use RuntimeException;
26
use Sabre\DAV\Client;
27
use Sabre\DAV\Xml\Property\ResourceType;
28
use Sabre\HTTP\ClientHttpException;
29
use Sabre\HTTP\Request;
30
use Throwable;
31

32
class WebDAVAdapter implements FilesystemAdapter
33
{
34
    public const FIND_PROPERTIES = [
35
        '{DAV:}displayname',
36
        '{DAV:}getcontentlength',
37
        '{DAV:}getcontenttype',
38
        '{DAV:}getlastmodified',
39
        '{DAV:}iscollection',
40
        '{DAV:}resourcetype',
41
    ];
42

43
    private PathPrefixer $prefixer;
44

45
    private Client $client;
46

47
    public function __construct(
48
        Client $client,
49
        string $prefix = ''
50
    ) {
51
        $this->client = $client;
52
        $this->prefixer = new PathPrefixer($prefix);
53
    }
54

55
    public function fileExists(string $path): bool
56
    {
57
        $location = $this->encodePath($this->prefixer->prefixPath($path));
58

59
        try {
60
            $properties = $this->client->propFind($location, ['{DAV:}resourcetype', '{DAV:}iscollection']);
61

62
            return !$this->propsIsDirectory($properties);
63
        } catch (Throwable $exception) {
64
            if ($exception instanceof ClientHttpException && $exception->getHttpStatus() === 404) {
65
                return false;
66
            }
67

68
            throw UnableToCheckFileExistence::forLocation($path, $exception);
69
        }
70
    }
71

72
    protected function encodePath(string $path): string
73
    {
74
        $parts = explode('/', $path);
75

76
        foreach ($parts as $i => $part) {
77
            $parts[$i] = rawurlencode($part);
78
        }
79

80
        return implode('/', $parts);
81
    }
82

83
    public function directoryExists(string $path): bool
84
    {
85
        $location = $this->encodePath($this->prefixer->prefixPath($path));
86

87
        try {
88
            $properties = $this->client->propFind($location, ['{DAV:}resourcetype', '{DAV:}iscollection']);
89

90
            return $this->propsIsDirectory($properties);
91
        } catch (Throwable $exception) {
92
            if ($exception instanceof ClientHttpException && $exception->getHttpStatus() === 404) {
93
                return false;
94
            }
95

96
            throw UnableToCheckFileExistence::forLocation($path, $exception);
97
        }
98
    }
99

100
    public function write(string $path, string $contents, Config $config): void
101
    {
102
        $this->upload($path, $contents);
103
    }
104

105
    public function writeStream(string $path, $contents, Config $config): void
106
    {
107
        $this->upload($path, $contents);
108
    }
109

110
    /**
111
     * @param resource|string $contents
112
     */
113
    private function upload(string $path, $contents): void
114
    {
115
        $this->createParentDirFor($path);
116
        $location = $this->encodePath($this->prefixer->prefixPath($path));
117

118
        try {
119
            $response = $this->client->request('PUT', $location, $contents);
120
            $statusCode = $response['statusCode'];
121

122
            if ($statusCode < 200 || $statusCode >= 300) {
123
                throw new RuntimeException('Unexpected status code received: ' . $statusCode);
124
            }
125
        } catch (Throwable $exception) {
126
            throw UnableToWriteFile::atLocation($path, $exception->getMessage(), $exception);
127
        }
128
    }
129

130
    public function read(string $path): string
131
    {
132
        $location = $this->encodePath($this->prefixer->prefixPath($path));
133

134
        try {
135
            $response = $this->client->request('GET', $location);
136

137
            if ($response['statusCode'] !== 200) {
138
                throw new RuntimeException('Unexpected response code for GET: ' . $response['statusCode']);
139
            }
140

141
            return $response['body'];
142
        } catch (Throwable $exception) {
143
            throw UnableToReadFile::fromLocation($path, $exception->getMessage(), $exception);
144
        }
145
    }
146

147
    public function readStream(string $path)
148
    {
149
        $location = $this->encodePath($this->prefixer->prefixPath($path));
150

151
        try {
152
            $url = $this->client->getAbsoluteUrl($location);
153
            $request = new Request('GET', $url);
154
            $response = $this->client->send($request);
155
            $status = $response->getStatus();
156

157
            if ($status !== 200) {
158
                throw new RuntimeException('Unexpected response code for GET: ' . $status);
159
            }
160

161
            return $response->getBodyAsStream();
162
        } catch (Throwable $exception) {
163
            throw UnableToReadFile::fromLocation($path, $exception->getMessage(), $exception);
164
        }
165
    }
166

167
    public function delete(string $path): void
168
    {
169
        $location = $this->encodePath($this->prefixer->prefixPath($path));
170

171
        try {
172
            $response = $this->client->request('DELETE', $location);
173
            $statusCode = $response['statusCode'];
174

175
            if ($statusCode !== 404 && ($statusCode < 200 || $statusCode >= 300)) {
176
                throw new RuntimeException('Unexpected status code received while deleting file: ' . $statusCode);
177
            }
178
        } catch (Throwable $exception) {
179
            if (!($exception instanceof ClientHttpException && $exception->getCode() === 404)) {
180
                throw UnableToDeleteFile::atLocation($path, $exception->getMessage(), $exception);
181
            }
182
        }
183
    }
184

185
    public function deleteDirectory(string $path): void
186
    {
187
        $location = $this->encodePath($this->prefixer->prefixDirectoryPath($path));
188

189
        try {
190
            $statusCode = $this->client->request('DELETE', $location)['statusCode'];
191

192
            if ($statusCode !== 404 && ($statusCode < 200 || $statusCode >= 300)) {
193
                throw new RuntimeException('Unexpected status code received while deleting file: ' . $statusCode);
194
            }
195
        } catch (Throwable $exception) {
196
            if (!($exception instanceof ClientHttpException && $exception->getCode() === 404)) {
197
                throw UnableToDeleteDirectory::atLocation($path, $exception->getMessage(), $exception);
198
            }
199
        }
200
    }
201

202
    public function createDirectory(string $path, Config $config): void
203
    {
204
        $parts = explode('/', $this->prefixer->prefixDirectoryPath($path));
205
        $directoryParts = [];
206

207
        foreach ($parts as $directory) {
208
            if ($directory === '.' || $directory === '') {
209
                return;
210
            }
211

212
            $directoryParts[] = $directory;
213
            $directoryPath = implode('/', $directoryParts) . '/';
214
            $location = $this->encodePath($directoryPath);
215

216
            if ($this->directoryExists($this->prefixer->stripDirectoryPrefix($directoryPath))) {
217
                continue;
218
            }
219

220
            try {
221
                $response = $this->client->request('MKCOL', $location);
222
            } catch (Throwable $exception) {
223
                throw UnableToCreateDirectory::dueToFailure($path, $exception);
224
            }
225

226
            if ($response['statusCode'] !== 201) {
227
                throw UnableToCreateDirectory::atLocation($path, 'Failed to create directory at: ' . $location);
228
            }
229
        }
230
    }
231

232
    public function setVisibility(string $path, string $visibility): void
233
    {
234
        throw UnableToSetVisibility::atLocation($path, 'WebDAV does not support this operation.');
235
    }
236

237
    public function visibility(string $path): FileAttributes
238
    {
239
        throw UnableToRetrieveMetadata::visibility($path, 'WebDAV does not support this operation.');
240
    }
241

242
    public function mimeType(string $path): FileAttributes
243
    {
244
        $mimeType = (string)$this->propFind($path, 'mime_type', '{DAV:}getcontenttype');
245

246
        return new FileAttributes($path, null, null, null, $mimeType);
247
    }
248

249
    public function lastModified(string $path): FileAttributes
250
    {
251
        $lastModified = $this->propFind($path, 'last_modified', '{DAV:}getlastmodified');
252

253
        return new FileAttributes($path, null, null, strtotime($lastModified));
254
    }
255

256
    public function fileSize(string $path): FileAttributes
257
    {
258
        $fileSize = (int)$this->propFind($path, 'file_size', '{DAV:}getcontentlength');
259

260
        return new FileAttributes($path, $fileSize);
261
    }
262

263
    public function listContents(string $path, bool $deep): iterable
264
    {
265
        $location = $this->encodePath($this->prefixer->prefixDirectoryPath($path));
266
        $response = $this->client->propFind($location, self::FIND_PROPERTIES, 1);
267

268
        // This is the directory itself, the files are subsequent entries.
269
        array_shift($response);
270

271
        foreach ($response as $pathKey => $object) {
272
            $pathKey = (string)parse_url(rawurldecode($pathKey), PHP_URL_PATH);
273
            $pathKey = $this->prefixer->stripPrefix($pathKey);
274
            $object = $this->normalizeObject($object);
275

276
            if ($this->propsIsDirectory($object)) {
277
                yield new DirectoryAttributes($pathKey, null, $object['last_modified'] ?? null);
278

279
                if (!$deep) {
280
                    continue;
281
                }
282

283
                foreach ($this->listContents($pathKey, true) as $child) {
284
                    yield $child;
285
                }
286
            } else {
287
                yield new FileAttributes(
288
                    $pathKey,
289
                    $object['file_size'] ?? null,
290
                    null,
291
                    $object['last_modified'] ?? null,
292
                    $object['mime_type'] ?? null,
293
                );
294
            }
295
        }
296
    }
297

298
    private function normalizeObject(array $object): array
299
    {
300
        $mapping = [
301
            '{DAV:}getcontentlength' => 'file_size',
302
            '{DAV:}getcontenttype' => 'mime_type',
303
            'content-length' => 'file_size',
304
            'content-type' => 'mime_type',
305
        ];
306

307
        foreach ($mapping as $from => $to) {
308
            if (array_key_exists($from, $object)) {
309
                $object[$to] = $object[$from];
310
            }
311
        }
312

313
        array_key_exists('file_size', $object) && $object['file_size'] = (int)$object['file_size'];
314

315
        if (array_key_exists('{DAV:}getlastmodified', $object)) {
316
            $object['last_modified'] = strtotime($object['{DAV:}getlastmodified']);
317
        }
318

319
        return $object;
320
    }
321

322
    public function move(string $source, string $destination, Config $config): void
323
    {
324
        if ($source === $destination) {
325
            return;
326
        }
327

328
        $this->createParentDirFor($destination);
329
        $location = $this->encodePath($this->prefixer->prefixPath($source));
330
        $newLocation = $this->encodePath($this->prefixer->prefixPath($destination));
331

332
        try {
333
            $response = $this->client->request('MOVE', $location, null, [
334
                'Destination' => $this->client->getAbsoluteUrl($newLocation),
335
            ]);
336

337
            if ($response['statusCode'] < 200 || $response['statusCode'] >= 300) {
338
                throw new RuntimeException('MOVE command returned unexpected status code: ' . $response['statusCode'] . "\n{$response['body']}");
339
            }
340
        } catch (Throwable $e) {
341
            throw UnableToMoveFile::fromLocationTo($source, $destination, $e);
342
        }
343
    }
344

345
    public function copy(string $source, string $destination, Config $config): void
346
    {
347
        if ($source === $destination) {
348
            return;
349
        }
350

351
        $this->createParentDirFor($destination);
352
        $location = $this->encodePath($this->prefixer->prefixPath($source));
353
        $newLocation = $this->encodePath($this->prefixer->prefixPath($destination));
354

355
        try {
356
            $response = $this->client->request('COPY', $location, null, [
357
                'Destination' => $this->client->getAbsoluteUrl($newLocation),
358
            ]);
359

360
            if ($response['statusCode'] < 200 || $response['statusCode'] >= 300) {
361
                throw new RuntimeException('COPY command returned unexpected status code: ' . $response['statusCode']);
362
            }
363
        } catch (Throwable $e) {
364
            throw UnableToCopyFile::fromLocationTo($source, $destination, $e);
365
        }
366
    }
367

368
    private function propsIsDirectory(array $properties): bool
369
    {
370
        if (isset($properties['{DAV:}resourcetype'])) {
371
            /** @var ResourceType $resourceType */
372
            $resourceType = $properties['{DAV:}resourcetype'];
373

374
            return $resourceType->is('{DAV:}collection');
375
        }
376

377
        return isset($properties['{DAV:}iscollection']) && $properties['{DAV:}iscollection'] === '1';
378
    }
379

380
    private function createParentDirFor(string $path): void
381
    {
382
        $dirname = dirname($path);
383

384
        if ($this->directoryExists($dirname)) {
385
            return;
386
        }
387

388
        $this->createDirectory($dirname, new Config());
389
    }
390

391
    private function propFind(string $path, string $section, string $property)
392
    {
393
        $location = $this->encodePath($this->prefixer->prefixPath($path));
394

395
        try {
396
            $result = $this->client->propFind($location, [$property]);
397

398
            if (!array_key_exists($property, $result)) {
399
                throw new RuntimeException('Invalid response, missing key: ' . $property);
400
            }
401

402
            return $result[$property];
403
        } catch (Throwable $exception) {
404
            throw UnableToRetrieveMetadata::create($path, $section, $exception->getMessage(), $exception);
405
        }
406
    }
407
}
408

Использование cookies

Мы используем файлы cookie в соответствии с Политикой конфиденциальности и Политикой использования cookies.

Нажимая кнопку «Принимаю», Вы даете АО «СберТех» согласие на обработку Ваших персональных данных в целях совершенствования нашего веб-сайта и Сервиса GitVerse, а также повышения удобства их использования.

Запретить использование cookies Вы можете самостоятельно в настройках Вашего браузера.