zend-blog-3-backend
407 строк · 13.5 Кб
1<?php
2/**
3* User: morontt
4* Date: 18.10.2024
5* Time: 21:52
6*/
7
8namespace App\Utils\Flysystem;
9
10use League\Flysystem\Config;
11use League\Flysystem\DirectoryAttributes;
12use League\Flysystem\FileAttributes;
13use League\Flysystem\FilesystemAdapter;
14use League\Flysystem\PathPrefixer;
15use League\Flysystem\UnableToCheckFileExistence;
16use League\Flysystem\UnableToCopyFile;
17use League\Flysystem\UnableToCreateDirectory;
18use League\Flysystem\UnableToDeleteDirectory;
19use League\Flysystem\UnableToDeleteFile;
20use League\Flysystem\UnableToMoveFile;
21use League\Flysystem\UnableToReadFile;
22use League\Flysystem\UnableToRetrieveMetadata;
23use League\Flysystem\UnableToSetVisibility;
24use League\Flysystem\UnableToWriteFile;
25use RuntimeException;
26use Sabre\DAV\Client;
27use Sabre\DAV\Xml\Property\ResourceType;
28use Sabre\HTTP\ClientHttpException;
29use Sabre\HTTP\Request;
30use Throwable;
31
32class WebDAVAdapter implements FilesystemAdapter
33{
34public const FIND_PROPERTIES = [
35'{DAV:}displayname',
36'{DAV:}getcontentlength',
37'{DAV:}getcontenttype',
38'{DAV:}getlastmodified',
39'{DAV:}iscollection',
40'{DAV:}resourcetype',
41];
42
43private PathPrefixer $prefixer;
44
45private Client $client;
46
47public function __construct(
48Client $client,
49string $prefix = ''
50) {
51$this->client = $client;
52$this->prefixer = new PathPrefixer($prefix);
53}
54
55public function fileExists(string $path): bool
56{
57$location = $this->encodePath($this->prefixer->prefixPath($path));
58
59try {
60$properties = $this->client->propFind($location, ['{DAV:}resourcetype', '{DAV:}iscollection']);
61
62return !$this->propsIsDirectory($properties);
63} catch (Throwable $exception) {
64if ($exception instanceof ClientHttpException && $exception->getHttpStatus() === 404) {
65return false;
66}
67
68throw UnableToCheckFileExistence::forLocation($path, $exception);
69}
70}
71
72protected function encodePath(string $path): string
73{
74$parts = explode('/', $path);
75
76foreach ($parts as $i => $part) {
77$parts[$i] = rawurlencode($part);
78}
79
80return implode('/', $parts);
81}
82
83public function directoryExists(string $path): bool
84{
85$location = $this->encodePath($this->prefixer->prefixPath($path));
86
87try {
88$properties = $this->client->propFind($location, ['{DAV:}resourcetype', '{DAV:}iscollection']);
89
90return $this->propsIsDirectory($properties);
91} catch (Throwable $exception) {
92if ($exception instanceof ClientHttpException && $exception->getHttpStatus() === 404) {
93return false;
94}
95
96throw UnableToCheckFileExistence::forLocation($path, $exception);
97}
98}
99
100public function write(string $path, string $contents, Config $config): void
101{
102$this->upload($path, $contents);
103}
104
105public function writeStream(string $path, $contents, Config $config): void
106{
107$this->upload($path, $contents);
108}
109
110/**
111* @param resource|string $contents
112*/
113private function upload(string $path, $contents): void
114{
115$this->createParentDirFor($path);
116$location = $this->encodePath($this->prefixer->prefixPath($path));
117
118try {
119$response = $this->client->request('PUT', $location, $contents);
120$statusCode = $response['statusCode'];
121
122if ($statusCode < 200 || $statusCode >= 300) {
123throw new RuntimeException('Unexpected status code received: ' . $statusCode);
124}
125} catch (Throwable $exception) {
126throw UnableToWriteFile::atLocation($path, $exception->getMessage(), $exception);
127}
128}
129
130public function read(string $path): string
131{
132$location = $this->encodePath($this->prefixer->prefixPath($path));
133
134try {
135$response = $this->client->request('GET', $location);
136
137if ($response['statusCode'] !== 200) {
138throw new RuntimeException('Unexpected response code for GET: ' . $response['statusCode']);
139}
140
141return $response['body'];
142} catch (Throwable $exception) {
143throw UnableToReadFile::fromLocation($path, $exception->getMessage(), $exception);
144}
145}
146
147public function readStream(string $path)
148{
149$location = $this->encodePath($this->prefixer->prefixPath($path));
150
151try {
152$url = $this->client->getAbsoluteUrl($location);
153$request = new Request('GET', $url);
154$response = $this->client->send($request);
155$status = $response->getStatus();
156
157if ($status !== 200) {
158throw new RuntimeException('Unexpected response code for GET: ' . $status);
159}
160
161return $response->getBodyAsStream();
162} catch (Throwable $exception) {
163throw UnableToReadFile::fromLocation($path, $exception->getMessage(), $exception);
164}
165}
166
167public function delete(string $path): void
168{
169$location = $this->encodePath($this->prefixer->prefixPath($path));
170
171try {
172$response = $this->client->request('DELETE', $location);
173$statusCode = $response['statusCode'];
174
175if ($statusCode !== 404 && ($statusCode < 200 || $statusCode >= 300)) {
176throw new RuntimeException('Unexpected status code received while deleting file: ' . $statusCode);
177}
178} catch (Throwable $exception) {
179if (!($exception instanceof ClientHttpException && $exception->getCode() === 404)) {
180throw UnableToDeleteFile::atLocation($path, $exception->getMessage(), $exception);
181}
182}
183}
184
185public function deleteDirectory(string $path): void
186{
187$location = $this->encodePath($this->prefixer->prefixDirectoryPath($path));
188
189try {
190$statusCode = $this->client->request('DELETE', $location)['statusCode'];
191
192if ($statusCode !== 404 && ($statusCode < 200 || $statusCode >= 300)) {
193throw new RuntimeException('Unexpected status code received while deleting file: ' . $statusCode);
194}
195} catch (Throwable $exception) {
196if (!($exception instanceof ClientHttpException && $exception->getCode() === 404)) {
197throw UnableToDeleteDirectory::atLocation($path, $exception->getMessage(), $exception);
198}
199}
200}
201
202public function createDirectory(string $path, Config $config): void
203{
204$parts = explode('/', $this->prefixer->prefixDirectoryPath($path));
205$directoryParts = [];
206
207foreach ($parts as $directory) {
208if ($directory === '.' || $directory === '') {
209return;
210}
211
212$directoryParts[] = $directory;
213$directoryPath = implode('/', $directoryParts) . '/';
214$location = $this->encodePath($directoryPath);
215
216if ($this->directoryExists($this->prefixer->stripDirectoryPrefix($directoryPath))) {
217continue;
218}
219
220try {
221$response = $this->client->request('MKCOL', $location);
222} catch (Throwable $exception) {
223throw UnableToCreateDirectory::dueToFailure($path, $exception);
224}
225
226if ($response['statusCode'] !== 201) {
227throw UnableToCreateDirectory::atLocation($path, 'Failed to create directory at: ' . $location);
228}
229}
230}
231
232public function setVisibility(string $path, string $visibility): void
233{
234throw UnableToSetVisibility::atLocation($path, 'WebDAV does not support this operation.');
235}
236
237public function visibility(string $path): FileAttributes
238{
239throw UnableToRetrieveMetadata::visibility($path, 'WebDAV does not support this operation.');
240}
241
242public function mimeType(string $path): FileAttributes
243{
244$mimeType = (string)$this->propFind($path, 'mime_type', '{DAV:}getcontenttype');
245
246return new FileAttributes($path, null, null, null, $mimeType);
247}
248
249public function lastModified(string $path): FileAttributes
250{
251$lastModified = $this->propFind($path, 'last_modified', '{DAV:}getlastmodified');
252
253return new FileAttributes($path, null, null, strtotime($lastModified));
254}
255
256public function fileSize(string $path): FileAttributes
257{
258$fileSize = (int)$this->propFind($path, 'file_size', '{DAV:}getcontentlength');
259
260return new FileAttributes($path, $fileSize);
261}
262
263public 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.
269array_shift($response);
270
271foreach ($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
276if ($this->propsIsDirectory($object)) {
277yield new DirectoryAttributes($pathKey, null, $object['last_modified'] ?? null);
278
279if (!$deep) {
280continue;
281}
282
283foreach ($this->listContents($pathKey, true) as $child) {
284yield $child;
285}
286} else {
287yield new FileAttributes(
288$pathKey,
289$object['file_size'] ?? null,
290null,
291$object['last_modified'] ?? null,
292$object['mime_type'] ?? null,
293);
294}
295}
296}
297
298private 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
307foreach ($mapping as $from => $to) {
308if (array_key_exists($from, $object)) {
309$object[$to] = $object[$from];
310}
311}
312
313array_key_exists('file_size', $object) && $object['file_size'] = (int)$object['file_size'];
314
315if (array_key_exists('{DAV:}getlastmodified', $object)) {
316$object['last_modified'] = strtotime($object['{DAV:}getlastmodified']);
317}
318
319return $object;
320}
321
322public function move(string $source, string $destination, Config $config): void
323{
324if ($source === $destination) {
325return;
326}
327
328$this->createParentDirFor($destination);
329$location = $this->encodePath($this->prefixer->prefixPath($source));
330$newLocation = $this->encodePath($this->prefixer->prefixPath($destination));
331
332try {
333$response = $this->client->request('MOVE', $location, null, [
334'Destination' => $this->client->getAbsoluteUrl($newLocation),
335]);
336
337if ($response['statusCode'] < 200 || $response['statusCode'] >= 300) {
338throw new RuntimeException('MOVE command returned unexpected status code: ' . $response['statusCode'] . "\n{$response['body']}");
339}
340} catch (Throwable $e) {
341throw UnableToMoveFile::fromLocationTo($source, $destination, $e);
342}
343}
344
345public function copy(string $source, string $destination, Config $config): void
346{
347if ($source === $destination) {
348return;
349}
350
351$this->createParentDirFor($destination);
352$location = $this->encodePath($this->prefixer->prefixPath($source));
353$newLocation = $this->encodePath($this->prefixer->prefixPath($destination));
354
355try {
356$response = $this->client->request('COPY', $location, null, [
357'Destination' => $this->client->getAbsoluteUrl($newLocation),
358]);
359
360if ($response['statusCode'] < 200 || $response['statusCode'] >= 300) {
361throw new RuntimeException('COPY command returned unexpected status code: ' . $response['statusCode']);
362}
363} catch (Throwable $e) {
364throw UnableToCopyFile::fromLocationTo($source, $destination, $e);
365}
366}
367
368private function propsIsDirectory(array $properties): bool
369{
370if (isset($properties['{DAV:}resourcetype'])) {
371/** @var ResourceType $resourceType */
372$resourceType = $properties['{DAV:}resourcetype'];
373
374return $resourceType->is('{DAV:}collection');
375}
376
377return isset($properties['{DAV:}iscollection']) && $properties['{DAV:}iscollection'] === '1';
378}
379
380private function createParentDirFor(string $path): void
381{
382$dirname = dirname($path);
383
384if ($this->directoryExists($dirname)) {
385return;
386}
387
388$this->createDirectory($dirname, new Config());
389}
390
391private function propFind(string $path, string $section, string $property)
392{
393$location = $this->encodePath($this->prefixer->prefixPath($path));
394
395try {
396$result = $this->client->propFind($location, [$property]);
397
398if (!array_key_exists($property, $result)) {
399throw new RuntimeException('Invalid response, missing key: ' . $property);
400}
401
402return $result[$property];
403} catch (Throwable $exception) {
404throw UnableToRetrieveMetadata::create($path, $section, $exception->getMessage(), $exception);
405}
406}
407}
408