1
|
|
|
<?php |
2
|
|
|
|
3
|
|
|
declare(strict_types=1); |
4
|
|
|
|
5
|
|
|
/* |
6
|
|
|
* This file is part of the Packagist Mirror. |
7
|
|
|
* |
8
|
|
|
* For the full license information, please view the LICENSE.md |
9
|
|
|
* file that was distributed with this source code. |
10
|
|
|
*/ |
11
|
|
|
|
12
|
|
|
namespace Webs\Mirror; |
13
|
|
|
|
14
|
|
|
use League\Flysystem\Filesystem as FlyFilesystem; |
15
|
|
|
use League\Flysystem\Adapter\Local; |
16
|
|
|
use League\Flysystem\Cached\CachedAdapter; |
17
|
|
|
use League\Flysystem\Cached\Storage\Memory; |
18
|
|
|
use Exception; |
19
|
|
|
use FilesystemIterator; |
20
|
|
|
|
21
|
|
|
/** |
22
|
|
|
* Middleware to access filesystem with transparent gz encode/decode. |
23
|
|
|
* |
24
|
|
|
* @author Webysther Nunes <[email protected]> |
25
|
|
|
*/ |
26
|
|
|
class Filesystem |
27
|
|
|
{ |
28
|
|
|
use GZip; |
29
|
|
|
|
30
|
|
|
/** |
31
|
|
|
* @var FlyFilesystem |
32
|
|
|
*/ |
33
|
|
|
protected $filesystem; |
34
|
|
|
|
35
|
|
|
/** |
36
|
|
|
* @var string |
37
|
|
|
*/ |
38
|
|
|
protected $directory; |
39
|
|
|
|
40
|
|
|
/** |
41
|
|
|
* Ephemeral cache for folder files count. |
42
|
|
|
* |
43
|
|
|
* @var array |
44
|
|
|
*/ |
45
|
|
|
protected $countedFolder = []; |
46
|
|
|
|
47
|
|
|
/** |
48
|
|
|
* @param string $dir Base directory |
49
|
|
|
* @param bool $initialize If true initialize the filesystem access |
50
|
|
|
*/ |
51
|
|
|
public function __construct($baseDirectory) |
52
|
|
|
{ |
53
|
|
|
$this->directory = realpath($baseDirectory).DIRECTORY_SEPARATOR; |
54
|
|
|
|
55
|
|
|
// Create the adapter |
56
|
|
|
$localAdapter = new Local($this->directory); |
57
|
|
|
|
58
|
|
|
// Create the cache store |
59
|
|
|
$cacheStore = new Memory(); |
60
|
|
|
|
61
|
|
|
// Decorate the adapter |
62
|
|
|
$adapter = new CachedAdapter($localAdapter, $cacheStore); |
63
|
|
|
|
64
|
|
|
// And use that to create the file system |
65
|
|
|
$this->filesystem = new FlyFilesystem($adapter); |
66
|
|
|
} |
67
|
|
|
|
68
|
|
|
/** |
69
|
|
|
* Add suffix gz to json file |
70
|
|
|
* |
71
|
|
|
* @param string $path |
72
|
|
|
* |
73
|
|
|
* @return string |
74
|
|
|
*/ |
75
|
|
View Code Duplication |
protected function getGzName(string $path):string |
|
|
|
|
76
|
|
|
{ |
77
|
|
|
$fullPath = $this->getFullPath($path); |
78
|
|
|
$extension = pathinfo($fullPath, PATHINFO_EXTENSION); |
79
|
|
|
|
80
|
|
|
if ($extension == 'json') { |
81
|
|
|
return $path.'.gz'; |
82
|
|
|
} |
83
|
|
|
|
84
|
|
|
return $path; |
85
|
|
|
} |
86
|
|
|
|
87
|
|
|
/** |
88
|
|
|
* Get link name from gz |
89
|
|
|
* |
90
|
|
|
* @param string $path |
91
|
|
|
* @return string |
92
|
|
|
*/ |
93
|
|
View Code Duplication |
protected function getLink(string $path):string |
|
|
|
|
94
|
|
|
{ |
95
|
|
|
$fullPath = $this->getFullPath($path); |
96
|
|
|
$extension = pathinfo($fullPath, PATHINFO_EXTENSION); |
97
|
|
|
|
98
|
|
|
if ($extension == 'gz') { |
99
|
|
|
return substr($path, 0, -3); |
100
|
|
|
} |
101
|
|
|
|
102
|
|
|
return $path; |
103
|
|
|
} |
104
|
|
|
|
105
|
|
|
/** |
106
|
|
|
* Decode from gz after read from disk. |
107
|
|
|
* |
108
|
|
|
* @see FlyFilesystem::read |
109
|
|
|
*/ |
110
|
|
|
public function read(string $path):string |
111
|
|
|
{ |
112
|
|
|
$path = $this->getGzName($path); |
113
|
|
|
$file = $this->filesystem->read($path); |
114
|
|
|
|
115
|
|
|
if ($file === false) { |
116
|
|
|
return ''; |
117
|
|
|
} |
118
|
|
|
|
119
|
|
|
return $this->decode($file); |
120
|
|
|
} |
121
|
|
|
|
122
|
|
|
/** |
123
|
|
|
* Encode to gz before write to disk with hash checking. |
124
|
|
|
* |
125
|
|
|
* @see FlyFilesystem::write |
126
|
|
|
*/ |
127
|
|
|
public function write(string $path, string $contents):Filesystem |
128
|
|
|
{ |
129
|
|
|
$file = $this->getGzName($path); |
130
|
|
|
$this->filesystem->put($file, $this->encode($contents)); |
131
|
|
|
$decoded = $this->decode($contents); |
132
|
|
|
|
133
|
|
|
if ($this->getHash($decoded) != $this->getHashFile($file)) { |
134
|
|
|
$this->filesystem->delete($file); |
135
|
|
|
throw new Exception("Write file $path hash failed"); |
136
|
|
|
} |
137
|
|
|
|
138
|
|
|
$this->symlink($file); |
139
|
|
|
|
140
|
|
|
return $this; |
141
|
|
|
} |
142
|
|
|
|
143
|
|
|
/** |
144
|
|
|
* Simple touch. |
145
|
|
|
* |
146
|
|
|
* @param string $path |
147
|
|
|
* |
148
|
|
|
* @return Filesystem |
149
|
|
|
*/ |
150
|
|
|
public function touch(string $path):Filesystem |
151
|
|
|
{ |
152
|
|
|
if ($this->has($path)) { |
153
|
|
|
return $this; |
154
|
|
|
} |
155
|
|
|
|
156
|
|
|
touch($this->getFullPath($path)); |
157
|
|
|
|
158
|
|
|
return $this; |
159
|
|
|
} |
160
|
|
|
|
161
|
|
|
/** |
162
|
|
|
* @param string $file |
163
|
|
|
* @return boolean |
164
|
|
|
*/ |
165
|
|
|
protected function isGzFile(string $file):bool |
166
|
|
|
{ |
167
|
|
|
if(substr($this->getGzName($file), -3) == '.gz'){ |
168
|
|
|
return true; |
169
|
|
|
} |
170
|
|
|
|
171
|
|
|
return false; |
172
|
|
|
} |
173
|
|
|
|
174
|
|
|
/** |
175
|
|
|
* Create a symlink. |
176
|
|
|
* |
177
|
|
|
* @param string $file |
178
|
|
|
* |
179
|
|
|
* @return Filesystem |
180
|
|
|
*/ |
181
|
|
|
protected function symlink(string $file):Filesystem |
182
|
|
|
{ |
183
|
|
|
if (!$this->hasFile($file) || !$this->isGzFile($file)) { |
184
|
|
|
return $this; |
185
|
|
|
} |
186
|
|
|
|
187
|
|
|
$path = $this->getGzName($file); |
188
|
|
|
$link = $this->getLink($path); |
189
|
|
|
|
190
|
|
|
if($this->hasLink($link)){ |
191
|
|
|
return $this; |
192
|
|
|
} |
193
|
|
|
|
194
|
|
|
symlink(basename($path), $this->getFullPath($link)); |
195
|
|
|
|
196
|
|
|
return $this; |
197
|
|
|
} |
198
|
|
|
|
199
|
|
|
/** |
200
|
|
|
* @see FlyFilesystem::has |
201
|
|
|
*/ |
202
|
|
|
public function has(string $path):bool |
203
|
|
|
{ |
204
|
|
|
return $this->hasFile($path) && $this->hasLink($path); |
205
|
|
|
} |
206
|
|
|
|
207
|
|
|
/** |
208
|
|
|
* @see FlyFilesystem::has |
209
|
|
|
*/ |
210
|
|
|
protected function hasFile(string $path):bool |
211
|
|
|
{ |
212
|
|
|
return file_exists($this->getFullPath($this->getGzName($path))); |
213
|
|
|
} |
214
|
|
|
|
215
|
|
|
/** |
216
|
|
|
* @see FlyFilesystem::has |
217
|
|
|
*/ |
218
|
|
|
protected function hasLink(string $path):bool |
219
|
|
|
{ |
220
|
|
|
return is_link($this->getFullPath($this->getLink($path))); |
221
|
|
|
} |
222
|
|
|
|
223
|
|
|
/** |
224
|
|
|
* Move to not dot name of file |
225
|
|
|
* |
226
|
|
|
* @param string $from |
227
|
|
|
* |
228
|
|
|
* @return Filesystem |
229
|
|
|
*/ |
230
|
|
|
public function move(string $from):Filesystem |
231
|
|
|
{ |
232
|
|
|
$file = $this->getGzName($from); |
233
|
|
|
$target = substr($file, 1); |
234
|
|
|
$this->filesystem->rename($from, $target); |
235
|
|
|
$this->symlink($target); |
236
|
|
|
return $this; |
237
|
|
|
} |
238
|
|
|
|
239
|
|
|
/** |
240
|
|
|
* @see FlyFilesystem::delete |
241
|
|
|
* @see FlyFilesystem::deleteDir |
242
|
|
|
*/ |
243
|
|
|
public function delete(string $fileOrDirectory):Filesystem |
244
|
|
|
{ |
245
|
|
|
$path = $this->getFullPath($fileOrDirectory); |
246
|
|
|
|
247
|
|
|
if (is_dir($path)) { |
248
|
|
|
$this->filesystem->deleteDir($fileOrDirectory); |
249
|
|
|
|
250
|
|
|
return $this; |
251
|
|
|
} |
252
|
|
|
|
253
|
|
|
$file = $this->getGzName($path); |
254
|
|
|
if (file_exists($file)) { |
255
|
|
|
unlink($file); |
256
|
|
|
} |
257
|
|
|
|
258
|
|
|
$link = $this->getLink($path); |
259
|
|
|
if (is_link($link)) { |
260
|
|
|
unlink($file); |
261
|
|
|
} |
262
|
|
|
|
263
|
|
|
return $this; |
264
|
|
|
} |
265
|
|
|
|
266
|
|
|
/** |
267
|
|
|
* Glob without file sort. |
268
|
|
|
* |
269
|
|
|
* @param string $pattern |
270
|
|
|
* |
271
|
|
|
* @return array |
272
|
|
|
*/ |
273
|
|
|
public function glob(string $pattern):array |
274
|
|
|
{ |
275
|
|
|
$return = glob($pattern, GLOB_NOSORT); |
276
|
|
|
|
277
|
|
|
if ($return === false) { |
278
|
|
|
return []; |
279
|
|
|
} |
280
|
|
|
|
281
|
|
|
return $return; |
282
|
|
|
} |
283
|
|
|
|
284
|
|
|
/** |
285
|
|
|
* Count files inside folder, if is a file, return 0. |
286
|
|
|
* |
287
|
|
|
* @param string $folder |
288
|
|
|
* |
289
|
|
|
* @return int |
290
|
|
|
*/ |
291
|
|
|
public function getCount(string $folder):int |
292
|
|
|
{ |
293
|
|
|
$path = $this->getFullPath($folder); |
294
|
|
|
$hash = $this->getHash($path); |
295
|
|
|
|
296
|
|
|
if (!is_dir($path)) { |
297
|
|
|
return 0; |
298
|
|
|
} |
299
|
|
|
|
300
|
|
|
if (array_key_exists($hash, $this->countedFolder)) { |
301
|
|
|
return $this->countedFolder[$hash]; |
302
|
|
|
} |
303
|
|
|
|
304
|
|
|
$iterator = new FilesystemIterator( |
305
|
|
|
$path, |
306
|
|
|
FilesystemIterator::SKIP_DOTS |
307
|
|
|
); |
308
|
|
|
|
309
|
|
|
$totalFiles = iterator_count($iterator); |
310
|
|
|
$this->countedFolder[$hash] = $totalFiles; |
311
|
|
|
|
312
|
|
|
return $totalFiles; |
313
|
|
|
} |
314
|
|
|
|
315
|
|
|
/** |
316
|
|
|
* Get full path. |
317
|
|
|
* |
318
|
|
|
* @param string $path |
319
|
|
|
* |
320
|
|
|
* @return string |
321
|
|
|
*/ |
322
|
|
|
protected function getFullPath(string $path):string |
323
|
|
|
{ |
324
|
|
|
if(strpos($path, $this->directory) !== false){ |
325
|
|
|
return $path; |
326
|
|
|
} |
327
|
|
|
|
328
|
|
|
return $this->directory.$path; |
329
|
|
|
} |
330
|
|
|
|
331
|
|
|
/** |
332
|
|
|
* Calculates SHA256. |
333
|
|
|
* |
334
|
|
|
* @param string $string |
335
|
|
|
* |
336
|
|
|
* @return string |
337
|
|
|
*/ |
338
|
|
|
public function getHash(string $string):string |
339
|
|
|
{ |
340
|
|
|
return hash('sha256', $string); |
341
|
|
|
} |
342
|
|
|
|
343
|
|
|
/** |
344
|
|
|
* Calculates SHA256 for file. |
345
|
|
|
* |
346
|
|
|
* @param string $path |
347
|
|
|
* |
348
|
|
|
* @return string |
349
|
|
|
*/ |
350
|
|
|
public function getHashFile(string $path):string |
351
|
|
|
{ |
352
|
|
|
// dont use hash_file because content is saved with gz |
353
|
|
|
return $this->getHash($this->read($path)); |
354
|
|
|
} |
355
|
|
|
} |
356
|
|
|
|
Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.
You can also find more detailed suggestions in the “Code” section of your repository.