1
|
|
|
<?php |
2
|
|
|
|
3
|
|
|
/** |
4
|
|
|
* This file is part of Spiral Framework package. |
5
|
|
|
* |
6
|
|
|
* For the full copyright and license information, please view the LICENSE |
7
|
|
|
* file that was distributed with this source code. |
8
|
|
|
*/ |
9
|
|
|
|
10
|
|
|
declare(strict_types=1); |
11
|
|
|
|
12
|
|
|
namespace Spiral\Storage\Storage; |
13
|
|
|
|
14
|
|
|
use JetBrains\PhpStorm\ExpectedValues; |
15
|
|
|
use League\Flysystem\FilesystemException; |
16
|
|
|
use League\Flysystem\FilesystemOperator; |
17
|
|
|
use Spiral\Storage\Exception\FileOperationException; |
18
|
|
|
use Spiral\Storage\FileInterface; |
19
|
|
|
use Spiral\Storage\StorageInterface; |
20
|
|
|
use Spiral\Storage\Visibility; |
21
|
|
|
|
22
|
|
|
/** |
23
|
|
|
* @mixin WritableInterface |
24
|
|
|
*/ |
25
|
|
|
trait WritableTrait |
26
|
|
|
{ |
27
|
|
|
/** |
28
|
|
|
* {@inheritDoc} |
29
|
|
|
*/ |
30
|
|
|
public function create(string $pathname, array $config = []): FileInterface |
31
|
|
|
{ |
32
|
|
|
return $this->write($pathname, '', $config); |
33
|
|
|
} |
34
|
|
|
|
35
|
|
|
/** |
36
|
|
|
* {@inheritDoc} |
37
|
|
|
*/ |
38
|
|
|
public function write(string $pathname, $content, array $config = []): FileInterface |
39
|
|
|
{ |
40
|
|
|
assert(\is_resource($content) || $this->isStringable($content)); |
41
|
|
|
|
42
|
|
|
$fs = $this->getOperator(); |
43
|
|
|
|
44
|
|
|
try { |
45
|
|
|
switch (true) { |
46
|
|
|
case \is_object($content): |
|
|
|
|
47
|
|
|
case \is_string($content): |
48
|
|
|
$fs->write($pathname, (string)$content, $config); |
49
|
|
|
break; |
50
|
|
|
|
51
|
|
|
case \is_resource($content): |
52
|
|
|
$fs->writeStream($pathname, $content, $config); |
53
|
|
|
break; |
54
|
|
|
|
55
|
|
|
default: |
56
|
|
|
$message = 'Content must be a resource stream or stringable type, but %s passed'; |
57
|
|
|
throw new \InvalidArgumentException(\sprintf($message, \get_debug_type($content))); |
58
|
|
|
} |
59
|
|
|
} catch (FilesystemException $e) { |
60
|
|
|
throw new FileOperationException($e->getMessage(), (int)$e->getCode(), $e); |
61
|
|
|
} |
62
|
|
|
|
63
|
|
|
return $this->file($pathname); |
|
|
|
|
64
|
|
|
} |
65
|
|
|
|
66
|
|
|
/** |
67
|
|
|
* {@inheritDoc} |
68
|
|
|
*/ |
69
|
|
|
public function setVisibility( |
70
|
|
|
string $pathname, |
71
|
|
|
#[ExpectedValues(valuesFromClass: Visibility::class)] |
72
|
|
|
string $visibility |
73
|
|
|
): FileInterface { |
74
|
|
|
$fs = $this->getOperator(); |
75
|
|
|
|
76
|
|
|
try { |
77
|
|
|
$fs->setVisibility($pathname, $this->toFlysystemVisibility($visibility)); |
78
|
|
|
} catch (FilesystemException $e) { |
79
|
|
|
throw new FileOperationException($e->getMessage(), (int)$e->getCode(), $e); |
80
|
|
|
} |
81
|
|
|
|
82
|
|
|
return $this->file($pathname); |
83
|
|
|
} |
84
|
|
|
|
85
|
|
|
/** |
86
|
|
|
* {@inheritDoc} |
87
|
|
|
*/ |
88
|
|
|
public function copy( |
89
|
|
|
string $source, |
90
|
|
|
string $destination, |
91
|
|
|
StorageInterface $storage = null, |
92
|
|
|
array $config = [] |
93
|
|
|
): FileInterface { |
94
|
|
|
$fs = $this->getOperator(); |
95
|
|
|
|
96
|
|
|
if ($storage === null || $storage === $this) { |
97
|
|
|
try { |
98
|
|
|
$fs->copy($source, $destination, $config); |
99
|
|
|
} catch (FilesystemException $e) { |
100
|
|
|
throw new FileOperationException($e->getMessage(), (int)$e->getCode(), $e); |
101
|
|
|
} |
102
|
|
|
|
103
|
|
|
return $this->file($destination); |
104
|
|
|
} |
105
|
|
|
|
106
|
|
|
return $storage->write($destination, $this->getStream($source), $config); |
|
|
|
|
107
|
|
|
} |
108
|
|
|
|
109
|
|
|
/** |
110
|
|
|
* {@inheritDoc} |
111
|
|
|
*/ |
112
|
|
|
public function move( |
113
|
|
|
string $source, |
114
|
|
|
string $destination, |
115
|
|
|
StorageInterface $storage = null, |
116
|
|
|
array $config = [] |
117
|
|
|
): FileInterface { |
118
|
|
|
$fs = $this->getOperator(); |
119
|
|
|
|
120
|
|
|
if ($storage === null || $storage === $this) { |
121
|
|
|
try { |
122
|
|
|
$fs->move($source, $destination, $config); |
123
|
|
|
} catch (FilesystemException $e) { |
124
|
|
|
throw new FileOperationException($e->getMessage(), (int)$e->getCode(), $e); |
125
|
|
|
} |
126
|
|
|
|
127
|
|
|
return $this->file($destination); |
128
|
|
|
} |
129
|
|
|
|
130
|
|
|
$result = $storage->write($destination, $this->getStream($source), $config); |
131
|
|
|
|
132
|
|
|
$fs->delete($source); |
133
|
|
|
|
134
|
|
|
return $result; |
135
|
|
|
} |
136
|
|
|
|
137
|
|
|
/** |
138
|
|
|
* {@inheritDoc} |
139
|
|
|
*/ |
140
|
|
|
public function delete(string $pathname, bool $clean = false): void |
141
|
|
|
{ |
142
|
|
|
$fs = $this->getOperator(); |
143
|
|
|
|
144
|
|
|
try { |
145
|
|
|
$fs->delete($pathname); |
146
|
|
|
|
147
|
|
|
if ($clean) { |
148
|
|
|
$this->deleteEmptyDirectories($this->getParentDirectory($pathname)); |
149
|
|
|
} |
150
|
|
|
} catch (FilesystemException $e) { |
151
|
|
|
throw new FileOperationException($e->getMessage(), (int)$e->getCode(), $e); |
152
|
|
|
} |
153
|
|
|
} |
154
|
|
|
/** |
155
|
|
|
* @return FilesystemOperator |
156
|
|
|
*/ |
157
|
|
|
abstract protected function getOperator(): FilesystemOperator; |
158
|
|
|
|
159
|
|
|
/** |
160
|
|
|
* @param string $visibility |
161
|
|
|
* @return string |
162
|
|
|
*/ |
163
|
|
|
#[ExpectedValues(valuesFromClass: \League\Flysystem\Visibility::class)] |
164
|
|
|
private function toFlysystemVisibility( |
165
|
|
|
#[ExpectedValues(valuesFromClass: Visibility::class)] |
166
|
|
|
string $visibility |
167
|
|
|
): string { |
168
|
|
|
return ($visibility === Visibility::VISIBILITY_PUBLIC) |
169
|
|
|
? \League\Flysystem\Visibility::PUBLIC |
170
|
|
|
: \League\Flysystem\Visibility::PRIVATE; |
171
|
|
|
} |
172
|
|
|
|
173
|
|
|
/** |
174
|
|
|
* Internal helper method that returns directory name of passed path. |
175
|
|
|
* |
176
|
|
|
* Please note that the use of the PHP {@see \dirname()} function depends |
177
|
|
|
* on the operating system and it MAY NOT return correct parent directory |
178
|
|
|
* in the case of slash character (`/` or `\`) incompatible with the |
179
|
|
|
* current runtime. |
180
|
|
|
* |
181
|
|
|
* @internal This is an internal method, please do not use it in your code. |
182
|
|
|
* @psalm-internal Spiral\Storage\Storage |
183
|
|
|
* |
184
|
|
|
* @param string $path |
185
|
|
|
* @return string |
186
|
|
|
*/ |
187
|
|
|
private function getParentDirectory(string $path): string |
188
|
|
|
{ |
189
|
|
|
return \dirname(\str_replace(['\\', '/'], \DIRECTORY_SEPARATOR, $path)); |
190
|
|
|
} |
191
|
|
|
|
192
|
|
|
/** |
193
|
|
|
* Internal helper method that returns bool {@see true} if the passed |
194
|
|
|
* directory is the root for the file. |
195
|
|
|
* |
196
|
|
|
* @internal This is an internal method, please do not use it in your code. |
197
|
|
|
* @psalm-internal Spiral\Storage\Storage |
198
|
|
|
* |
199
|
|
|
* @param string $directory |
200
|
|
|
* @return bool |
201
|
|
|
*/ |
202
|
|
|
private function hasParentDirectory(string $directory): bool |
203
|
|
|
{ |
204
|
|
|
return $directory !== '' && $directory !== '.'; |
205
|
|
|
} |
206
|
|
|
|
207
|
|
|
/** |
208
|
|
|
* Internal helper method that recursively deletes empty directories. |
209
|
|
|
* |
210
|
|
|
* @internal This is an internal method, please do not use it in your code. |
211
|
|
|
* @psalm-internal Spiral\Storage\Storage |
212
|
|
|
* |
213
|
|
|
* @param string $directory |
214
|
|
|
* @throws FileOperationException |
215
|
|
|
*/ |
216
|
|
|
private function deleteEmptyDirectories(string $directory): void |
217
|
|
|
{ |
218
|
|
|
if (!$this->hasParentDirectory($directory)) { |
219
|
|
|
return; |
220
|
|
|
} |
221
|
|
|
|
222
|
|
|
$fs = $this->getOperator(); |
223
|
|
|
|
224
|
|
|
try { |
225
|
|
|
if (!$this->hasFiles($directory)) { |
226
|
|
|
$fs->deleteDirectory($directory); |
227
|
|
|
|
228
|
|
|
$this->deleteEmptyDirectories($this->getParentDirectory($directory)); |
229
|
|
|
} |
230
|
|
|
} catch (FilesystemException $e) { |
231
|
|
|
throw new FileOperationException($e->getMessage(), (int)$e->getCode(), $e); |
232
|
|
|
} |
233
|
|
|
} |
234
|
|
|
|
235
|
|
|
/** |
236
|
|
|
* Internal helper method that returns bool {@see true} if directory |
237
|
|
|
* not empty. |
238
|
|
|
* |
239
|
|
|
* Note: Be careful, this method can be quite slow as it asks for a |
240
|
|
|
* list of files from filesystem. |
241
|
|
|
* |
242
|
|
|
* @internal This is an internal method, please do not use it in your code. |
243
|
|
|
* @psalm-internal Spiral\Storage\Storage |
244
|
|
|
* |
245
|
|
|
* @param string $directory |
246
|
|
|
* @return bool |
247
|
|
|
* @throws FilesystemException |
248
|
|
|
*/ |
249
|
|
|
private function hasFiles(string $directory): bool |
250
|
|
|
{ |
251
|
|
|
$fs = $this->getOperator(); |
252
|
|
|
|
253
|
|
|
foreach ($fs->listContents($directory) as $_) { |
254
|
|
|
return true; |
255
|
|
|
} |
256
|
|
|
|
257
|
|
|
return false; |
258
|
|
|
} |
259
|
|
|
|
260
|
|
|
/** |
261
|
|
|
* Internal helper method that returns bool {@see true} if passed argument |
262
|
|
|
* can be converted to string. |
263
|
|
|
* |
264
|
|
|
* @param string|\Stringable $value |
265
|
|
|
* @return bool |
266
|
|
|
*/ |
267
|
|
|
private function isStringable($value): bool |
268
|
|
|
{ |
269
|
|
|
if (\is_string($value)) { |
270
|
|
|
return true; |
271
|
|
|
} |
272
|
|
|
|
273
|
|
|
if (!\is_object($value)) { |
274
|
|
|
return false; |
275
|
|
|
} |
276
|
|
|
|
277
|
|
|
if (\PHP_VERSION_ID >= 80000) { |
278
|
|
|
return $value instanceof \Stringable; |
279
|
|
|
} |
280
|
|
|
|
281
|
|
|
return \method_exists($value, '__toString'); |
282
|
|
|
} |
283
|
|
|
} |
284
|
|
|
|
This check looks for unreachable code. It uses sophisticated control flow analysis techniques to find statements which will never be executed.
Unreachable code is most often the result of
return
,die
orexit
statements that have been added for debug purposes.In the above example, the last
return false
will never be executed, because a return statement has already been met in every possible execution path.