depage-fs
Loading...
Searching...
No Matches
Fs.php
Go to the documentation of this file.
1<?php
2
3namespace Depage\Fs;
4
5class Fs
6{
7 protected $currentPath;
8 protected $base;
9 protected $url;
10 protected $hidden;
11 protected $streamContextOptions = array();
12 protected $streamContext;
13 protected $path;
14 protected $errorHandlerDepth = 0;
15 public function __construct($params = array())
16 {
17 if (isset($params['scheme'])) $this->url['scheme'] = $params['scheme'];
18 if (isset($params['user'])) $this->url['user'] = $params['user'];
19 if (isset($params['pass'])) $this->url['pass'] = $params['pass'];
20 if (isset($params['host'])) $this->url['host'] = $params['host'];
21 if (isset($params['port'])) $this->url['port'] = $params['port'];
22
23 $this->hidden = (isset($params['hidden'])) ? $params['hidden'] : false;
24 $this->path = (isset($params['path'])) ? $params['path'] : '.';
25
26 $this->streamContext = stream_context_create($this->streamContextOptions);
27 }
28 public static function factory($url, $params = array())
29 {
30 $parsed = self::parseUrl($url);
31 if (is_array($parsed)) {
32 $params = array_merge($parsed, $params);
33 }
34 $scheme = $params['scheme'] ?? '';
35 $alias = self::schemeAlias($scheme);
36
37 $schemeClass = '\Depage\Fs\Fs' . ucfirst($alias['class']);
38 $params['scheme'] = $alias['scheme'];
39
40 return new $schemeClass($params);
41 }
42 protected static function schemeAlias($alias = '')
43 {
44 // null would be deprecated as an array key in php 8.5
45 $alias = (string) $alias;
46
47 $aliases = array(
48 '' => array('class' => 'file', 'scheme' => 'file'),
49 'file' => array('class' => 'file', 'scheme' => 'file'),
50 'ftp' => array('class' => 'ftp', 'scheme' => 'ftp'),
51 'ftps' => array('class' => 'ftp', 'scheme' => 'ftps'),
52 'ssh2.sftp' => array('class' => 'ssh', 'scheme' => 'ssh2.sftp'),
53 'ssh' => array('class' => 'ssh', 'scheme' => 'ssh2.sftp'),
54 'sftp' => array('class' => 'ssh', 'scheme' => 'ssh2.sftp'),
55 );
56
57 if (array_key_exists($alias, $aliases)) {
58 $translation = $aliases[$alias];
59 } else {
60 $translation = array('class' => '', 'scheme' => $alias);
61 }
62
63 return $translation;
64 }
65
66 public function pwd()
67 {
68 $this->preCommandHook();
69
70 try {
72 $url['path'] = $this->base . $this->currentPath;
73 $pwd = $this->buildUrl($url);
74
75 return $pwd;
76 } finally {
77 $this->postCommandHook();
78 }
79 }
80 public function ls($url)
81 {
82 $this->preCommandHook();
83
84 try {
85 $cleanUrl = $this->cleanUrl($url);
86 $path = str_replace($this->pwd(), '', $cleanUrl);
87 $ls = $this->lsRecursive($path, '');
88
89 return $ls;
90 } finally {
91 $this->postCommandHook();
92 }
93 }
94 public function lsDir($path = '')
95 {
96 $this->preCommandHook();
97
98 try {
99 $lsDir = $this->lsFilter($path, 'is_dir');
100
101 return $lsDir;
102 } finally {
103 $this->postCommandHook();
104 }
105 }
106 public function lsFiles($path = '')
107 {
108 $this->preCommandHook();
109
110 try {
111 $lsFiles = $this->lsFilter($path, 'is_file');
112
113 return $lsFiles;
114 } finally {
115 $this->postCommandHook();
116 }
117 }
118 public function exists($remotePath)
119 {
120 $this->preCommandHook();
121
122 try {
123 $remote = $this->cleanUrl($remotePath);
124 $exists = file_exists($remote);
125
126 return $exists;
127 } finally {
128 $this->postCommandHook();
129 }
130 }
131 public function fileInfo($remotePath)
132 {
133 $this->preCommandHook();
134
135 try {
136 $remote = $this->cleanUrl($remotePath);
137 $fileInfo = new \SplFileInfo($remote);
138
139 return $fileInfo;
140 } finally {
141 $this->postCommandHook();
142 }
143 }
144
145 public function cd($url)
146 {
147 $this->preCommandHook();
148
149 try {
150 $cleanUrl = $this->cleanUrl($url);
151
152 if (is_dir($cleanUrl) && is_readable($cleanUrl . '/.')) {
153 $this->currentPath = str_replace($this->pwd(), '', $cleanUrl) . '/';
154 } else {
155 throw new Exceptions\FsException('Directory not accessible "' . $this->cleanUrl($url, false) . '".');
156 }
157
158 } finally {
159 $this->postCommandHook();
160 }
161 }
162 public function mkdir($pathName, $mode = 0777, $recursive = true)
163 {
164 $this->preCommandHook();
165
166 try {
167 $cleanUrl = $this->cleanUrl($pathName);
168 if (!is_dir($cleanUrl)) {
169 $success = mkdir($cleanUrl, $mode, $recursive, $this->streamContext);
170
171 if (!$success && !is_dir($cleanUrl)) {
172 throw new Exceptions\FsException('Error while creating directory "' . $pathName . '".');
173 }
174 }
175
176 } finally {
177 $this->postCommandHook();
178 }
179 }
180 public function rm($url)
181 {
182 $this->preCommandHook();
183
184 try {
185 $cleanUrl = $this->cleanUrl($url);
186 $pwd = $this->pwd();
187
188 if (preg_match('/^' . preg_quote($cleanUrl, '/') . '\//', $pwd . '/')) {
189 throw new Exceptions\FsException('Cannot delete current or parent directory "' . $this->cleanUrl($pwd, false) . '".');
190 }
191 $this->rmRecursive($cleanUrl);
192
193 } finally {
194 $this->postCommandHook();
195 }
196 }
197 public function copy($sourcePath, $targetPath)
198 {
199 $this->preCommandHook();
200
201 try {
202 $source = $this->cleanUrl($sourcePath);
203 $target = $this->cleanUrl($targetPath);
204
205 if (file_exists($source)) {
206 if(file_exists($target) && is_dir($target)) {
207 $target .= '/' . $this->extractFileName($source);
208 }
209 \copy($source, $target, $this->streamContext);
210 } else {
211 throw new Exceptions\FsException('Cannot copy "' . $this->cleanUrl($sourcePath, false) . '" to "' . $this->cleanUrl($targetPath, false) . '" - source doesn\'t exist.');
212 }
213
214 } finally {
215 $this->postCommandHook();
216 }
217 }
218 public function mv($sourcePath, $targetPath)
219 {
220 $this->preCommandHook();
221
222 try {
223 $source = $this->cleanUrl($sourcePath);
224 $target = $this->cleanUrl($targetPath);
225
226 if (file_exists($source)) {
227 if(file_exists($target) && is_dir($target)) {
228 $target .= '/' . $this->extractFileName($source);
229 }
230 $this->rename($source, $target);
231 } else {
232 throw new Exceptions\FsException('Cannot move "' . $this->cleanUrl($sourcePath, false) . '" to "' . $this->cleanUrl($targetPath, false) . '" - source doesn\'t exist.');
233 }
234
235 } finally {
236 $this->postCommandHook();
237 }
238 }
239
240 public function get($remotePath, $local = null)
241 {
242 $this->preCommandHook();
243
244 try {
245 if ($local === null) {
246 $local = $this->extractFileName($remotePath);
247 }
248
249 $remote = $this->cleanUrl($remotePath);
250 copy($remote, $local, $this->streamContext);
251
252 } finally {
253 $this->postCommandHook();
254 }
255 }
256 public function put($local, $remotePath)
257 {
258 $this->preCommandHook();
259
260 try {
261 $remote = $this->cleanUrl($remotePath);
262 copy($local, $remote, $this->streamContext);
263
264 } finally {
265 $this->postCommandHook();
266 }
267 }
268 public function getString($remotePath)
269 {
270 $this->preCommandHook();
271
272 try {
273 $remote = $this->cleanUrl($remotePath);
274 $string = file_get_contents($remote, false, $this->streamContext);
275
276 return $string;
277 } finally {
278 $this->postCommandHook();
279 }
280 }
281 public function putString($remotePath, $string)
282 {
283 $this->preCommandHook();
284
285 try {
286 $remote = $this->cleanUrl($remotePath);
287 $this->file_put_contents($remote, $string, 0, $this->streamContext);
288
289 } finally {
290 $this->postCommandHook();
291 }
292 }
293
294 public function test(&$error = null)
295 {
296 $testFile = 'depage-fs-test-file.tmp';
297 $testString = 'depage-fs-test-string';
298 $success = false;
299
300 try {
301 if (!$this->exists($testFile)) {
302 $this->putString($testFile, $testString);
303 if ($this->getString($testFile) === $testString) {
304 $this->rm($testFile);
305 $success = !$this->exists($testFile);
306 }
307 }
308 } catch (Exceptions\FsException $exception) {
309 $error = $exception->getMessage();
310 }
311
312 return $success;
313 }
314
315 protected function preCommandHook()
316 {
317 $this->lateConnect();
318 $this->setErrorHandler(true);
319 }
320 protected function postCommandHook()
321 {
322 $this->setErrorHandler(false);
323 }
324 protected function lateConnect()
325 {
326 if (!isset($this->base)) {
327 $this->setBase($this->path);
328 }
329 }
330 protected function setBase($path)
331 {
332 $cleanPath = $this->cleanPath('/' . $path);
333 $this->base = (substr($cleanPath, -1) == '/') ? $cleanPath : $cleanPath . '/';
334 }
335
336 public function depageFsErrorHandler($errno, $errstr, $errfile, $errline)
337 {
338 if ($this->errorHandlerDepth > 0) {
339 $this->errorHandlerDepth = 0;
340 restore_error_handler();
341 }
342 throw new Exceptions\FsException($errstr);
343 }
344 protected function setErrorHandler($start)
345 {
346 if ($start) {
347 if ($this->errorHandlerDepth++ === 0) {
348 set_error_handler(array($this, 'depageFsErrorHandler'));
349 }
350 } elseif ($this->errorHandlerDepth > 0 && --$this->errorHandlerDepth === 0) {
351 restore_error_handler();
352 }
353 }
354
355 public static function parseUrl($url)
356 {
357 $parsed = parse_url($url);
358
359 // hack, parse_url (PHP 5.6.29) won't handle resource name strings
360 if (isset($parsed['fragment'])) {
361 $urlParts = explode('/', $url);
362
363 if (isset($urlParts[2]) && preg_match('/^Resource id \#([0-9]+)$/', $urlParts[2], $matches)) {
364 $urlParts[2] = $matches[1];
365 $parsed = parse_url(implode('/', $urlParts));
366 $parsed['host'] = 'Resource id #' . $parsed['host'];
367 }
368 }
369
370 // hack, parse_url matches anything after the first question mark as "query"
371 $path = (isset($parsed['path'])) ? $parsed['path'] : '';
372 $query = (isset($parsed['query'])) ? $parsed['query'] : '';
373 if (!empty($query) || preg_match('/\?$/', $url)) {
374 $parsed['path'] = $path . '?' . $query;
375 unset($parsed['query']);
376 }
377
378 return $parsed;
379 }
380 protected function cleanUrl($url, $showPass = true)
381 {
382 $parsed = self::parseUrl($url);
383 $scheme = (isset($parsed['scheme'])) ? $parsed['scheme'] : null;
384 $path = (isset($parsed['path'])) ? $parsed['path'] : null;
385
386 if ($scheme) {
387 $newUrl = $parsed;
388 $newPath = $path;
389 } else {
390 $newUrl = $this->url;
391 if (substr($url, 0, 1) == '/') {
392 $newPath = $url;
393 } else {
394 $newPath = $this->base . $this->currentPath;
395 $newPath .= (substr($path, 0, 1) == '/') ? '' : '/';
396 $newPath .= $path;
397 }
398 }
399
400 $newUrl['path'] = $this->cleanPath($newPath);
401
402 if (!preg_match(';^' . preg_quote($this->cleanPath($this->base)) . '(.*)$;', $newUrl['path'])) {
403 throw new Exceptions\FsException('Cannot leave base directory "' . $this->base . '".');
404 }
405
406 return $this->buildUrl($newUrl, $showPass);
407 }
408 protected function cleanPath($path)
409 {
410 $dirs = explode('/', $path);
411 $newDirs = array();
412
413 foreach ($dirs as $dir) {
414 if ($dir == '..') {
415 array_pop($newDirs);
416 } elseif ($dir != '.' && $dir != '') {
417 $newDirs[] = $dir;
418 }
419 }
420
421 $newPath = (substr($path, 0, 1) == '/') ? '/' : '';
422 $newPath .= implode('/', $newDirs);
423
424 return $newPath;
425 }
426 protected function buildUrl($parsed, $showPass = true)
427 {
428 $path = $parsed['scheme'] . '://';
429 $path .= !empty($parsed['user']) ? rawurlencode($parsed['user']) : '';
430
431 if (!empty($parsed['pass'])) {
432 $path .= ($showPass) ? ':' . rawurlencode($parsed['pass']) : ':...';
433 }
434
435 $path .= !empty($parsed['user']) ? '@' : '';
436 $path .= !empty($parsed['host']) ? $parsed['host'] : '';
437 $path .= !empty($parsed['port']) ? ':' . $parsed['port'] : '';
438 $path .= !empty($parsed['path']) ? $parsed['path'] : '/';
439
440 return $path;
441 }
442 protected function extractFileName($path)
443 {
444 $pathInfo = pathinfo($path);
445 $fileName = $pathInfo['filename'];
446
447 if (isset($pathInfo['extension'])) {
448 $fileName .= '.' . $pathInfo['extension'];
449 }
450
451 return $fileName;
452 }
453
454 protected function lsFilter($path, $function)
455 {
456 $ls = $this->ls($path);
457 $pwd = $this->pwd();
458 $lsFiltered = array_filter(
459 $ls,
460 function ($element) use ($function, $pwd) {
461 return $function($pwd . $element);
462 }
463 );
464 natcasesort($lsFiltered);
465 $sorted = array_values($lsFiltered);
466
467 return $sorted;
468 }
469 protected function matchNodesInPath($path, $pattern)
470 {
471 if (preg_match('/[' . preg_quote('*?[]') . ']/', $pattern)) {
472 $matches = array_filter(
473 $this->scandir($path),
474 function ($node) use ($pattern) { return fnmatch($pattern, $node); }
475 );
476 } else {
477 $matches = array($pattern);
478 }
479 return $matches;
480 }
481 protected function lsRecursive($path, $current)
482 {
483 $nodes = array();
484 $patterns = explode('/', $path);
485 $count = count($patterns);
486 $pwd = $this->pwd();
487
488 if ($count) {
489 $pattern = array_shift($patterns);
490 $matches = $this->matchNodesInPath($pwd . $current, $pattern);
491
492 foreach ($matches as $match) {
493 $next = ($current) ? $current . '/' . $match : $match;
494
495 if ($count === 1) {
496 $nodes[] = $next;
497 } elseif (is_dir($pwd . $next)) {
498 $nodes = array_merge(
499 $nodes,
500 $this->lsRecursive(implode('/', $patterns), $next)
501 );
502 }
503 }
504 }
505
506 return $nodes;
507 }
508 protected function rmRecursive($cleanUrl)
509 {
510 if (!file_exists($cleanUrl)) {
511 throw new Exceptions\FsException('"' . $this->cleanUrl($cleanUrl, false) . '" doesn\'t exist.');
512 } elseif (is_dir($cleanUrl)) {
513 foreach ($this->scandir($cleanUrl, true) as $nested) {
514 $this->rmRecursive($cleanUrl . '/' . $nested);
515 }
516 $this->rmdir($cleanUrl);
517 } elseif (is_file($cleanUrl)) {
518 unlink($cleanUrl, $this->streamContext);
519 }
520
521 clearstatcache(true, $cleanUrl);
522 }
523
524 protected function scandir($cleanUrl = '', $hidden = null)
525 {
526 if ($hidden === null) {
528 }
529
530 $scanDir = \scandir($cleanUrl, 0, $this->streamContext);
531 $filtered = array_diff($scanDir, array('.', '..'));
532
533 if (!$hidden) {
534 $filtered = array_filter(
535 $filtered,
536 function ($node) { return ($node[0] != '.'); }
537 );
538 }
539
540 natcasesort($filtered);
541 $sorted = array_values($filtered);
542
543 return $sorted;
544 }
545
548 protected function rmdir($url)
549 {
550 return \rmdir($url, $this->streamContext);
551 }
552
555 protected function rename($source, $target)
556 {
557 return \rename($source, $target, $this->streamContext);
558 }
559
562 public function file_put_contents($filename, $data, $flags = 0, $context = null)
563 {
564 return \file_put_contents($filename, $data, $flags, $context);
565 }
566}
567
568/* vim:set ft=php sw=4 sts=4 fdm=marker : */
extractFileName($path)
Definition Fs.php:442
$streamContextOptions
Definition Fs.php:11
rmRecursive($cleanUrl)
Definition Fs.php:508
buildUrl($parsed, $showPass=true)
Definition Fs.php:426
preCommandHook()
Definition Fs.php:315
fileInfo($remotePath)
Definition Fs.php:131
static schemeAlias($alias='')
Definition Fs.php:42
cd($url)
Definition Fs.php:145
putString($remotePath, $string)
Definition Fs.php:281
cleanPath($path)
Definition Fs.php:408
ls($url)
Definition Fs.php:80
__construct($params=array())
Definition Fs.php:15
lateConnect()
Definition Fs.php:324
rename($source, $target)
Hook, allows overriding of rename function.
Definition Fs.php:555
static factory($url, $params=array())
Definition Fs.php:28
lsRecursive($path, $current)
Definition Fs.php:481
$streamContext
Definition Fs.php:12
$currentPath
Definition Fs.php:7
copy($sourcePath, $targetPath)
Definition Fs.php:197
rmdir($url)
Hook, allows overriding of rmdir function.
Definition Fs.php:548
file_put_contents($filename, $data, $flags=0, $context=null)
Hook, allows overriding of file_put_contents function.
Definition Fs.php:562
scandir($cleanUrl='', $hidden=null)
Definition Fs.php:524
lsDir($path='')
Definition Fs.php:94
$errorHandlerDepth
Definition Fs.php:14
mkdir($pathName, $mode=0777, $recursive=true)
Definition Fs.php:162
depageFsErrorHandler($errno, $errstr, $errfile, $errline)
Definition Fs.php:336
getString($remotePath)
Definition Fs.php:268
postCommandHook()
Definition Fs.php:320
mv($sourcePath, $targetPath)
Definition Fs.php:218
lsFiles($path='')
Definition Fs.php:106
exists($remotePath)
Definition Fs.php:118
setErrorHandler($start)
Definition Fs.php:344
put($local, $remotePath)
Definition Fs.php:256
lsFilter($path, $function)
Definition Fs.php:454
test(&$error=null)
Definition Fs.php:294
cleanUrl($url, $showPass=true)
Definition Fs.php:380
matchNodesInPath($path, $pattern)
Definition Fs.php:469
setBase($path)
Definition Fs.php:330
rm($url)
Definition Fs.php:180
static parseUrl($url)
Definition Fs.php:355