├── CHANGELOG.md ├── AUTHORS ├── src ├── FilesystemInterface.php ├── Exception │ ├── ExceptionInterface.php │ ├── FileNotFoundException.php │ ├── InvalidJsonPathException.php │ ├── RTExceptionInterface.php │ ├── RTException.php │ └── RuntimeException.php └── Filesystem.php ├── composer.json └── LICENSE /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /AUTHORS: -------------------------------------------------------------------------------- 1 | Shahzada Modassir 2 | Shahzadi Afsara 3 | -------------------------------------------------------------------------------- /src/FilesystemInterface.php: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/Exception/ExceptionInterface.php: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/Exception/FileNotFoundException.php: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/Exception/InvalidJsonPathException.php: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/Exception/RTExceptionInterface.php: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/Exception/RTException.php: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/Exception/RuntimeException.php: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "filesys/filesystem", 3 | "description": "A lightweight PHP filesystem library for efficient file and directory operations.", 4 | "type": "library", 5 | "keywords": ["filesystem", "fs"], 6 | "license": "MIT", 7 | "autoload": { 8 | "psr-4": { 9 | "Filesystem\\": "src/" 10 | } 11 | }, 12 | "require": { 13 | "filesys/path": ">=4.0 < 5.0" 14 | }, 15 | "authors": [ 16 | { 17 | "name": "Shahzada Modassir", 18 | "email": "lazervel@gmail.com", 19 | "homepage": "https://github.com/shahzadamodassir" 20 | } 21 | ], 22 | "funding": [ 23 | { 24 | "type": "Github", 25 | "url": "https://github.com/shahzadamodassir" 26 | } 27 | ], 28 | "minimum-stability": "stable" 29 | } 30 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 The Lazervel Framework 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/Filesystem.php: -------------------------------------------------------------------------------- 1 | isDir($directory)) { 27 | return false; 28 | } 29 | 30 | $output = $this->exec('scandir', $directory, $order); 31 | 32 | $traversal ? ($output=$this->exec('array_diff', $output, ['.', '..'])) : $output; 33 | return $output; 34 | } 35 | 36 | /** 37 | * 38 | * 39 | * @param string $phpfunc 40 | * @param mixed $args 41 | * 42 | * @return mixed 43 | */ 44 | private function exec(string $phpfunc, ...$args) 45 | { 46 | $this->throwIfFunctionNotExists($phpfunc); 47 | set_error_handler(self::errorHandler(...)); 48 | try {return $phpfunc(...$args);} finally {restore_error_handler();} 49 | } 50 | 51 | /** 52 | * 53 | * 54 | * @param string $dirname 55 | * @return int 56 | */ 57 | public function dirsize(string $dirname) : int 58 | { 59 | return $this->filesize($this->scan($dirname, 0, 1, false)); 60 | } 61 | 62 | /** 63 | * 64 | * 65 | * @param string|iterable $files 66 | */ 67 | public function create($files) 68 | { 69 | foreach($this->toIterable($files) as $file) { 70 | $dir = Path::dirname($file); 71 | !$this->isDir($dir) && $this->mkdir($dir); 72 | $this->touch($file); 73 | } 74 | } 75 | 76 | /** 77 | * 78 | * 79 | * @param string|iterable $files 80 | * @return void 81 | */ 82 | public function remove($files) : void 83 | { 84 | $files = $files instanceof \Traversable ? iterator_to_array($files) : $this->toIterable($files); 85 | $this->delete($files, false); 86 | } 87 | 88 | /** 89 | * 90 | * 91 | * @param string|iterable $files 92 | * @return int 93 | */ 94 | public function filesize($files) : int 95 | { 96 | // Throw RTException error if file does not exists. 97 | $this->throwIfFileNotFound($files); 98 | 99 | $filesize = 0; 100 | 101 | foreach($this->toIterable($files) as $file) { 102 | $filesize += $this->exec('filesize', $file); 103 | } 104 | 105 | return $filesize; 106 | } 107 | 108 | 109 | 110 | /** 111 | * 112 | * 113 | * @param int $type 114 | * @param string $msg 115 | * 116 | * @return void 117 | */ 118 | private static function errorHandler(int $type, string $msg) : void 119 | { 120 | 121 | } 122 | 123 | /** 124 | * 125 | * 126 | * @param string $directory 127 | * @param bool $traversal 128 | * @param int $order 129 | * @param int $filter 130 | * @param bool $isTree 131 | * 132 | * @return array|false 133 | */ 134 | public function scan(string $directory, int $order = 0, int $filter = 0, bool $isTree = true) 135 | { 136 | // Check if the directory exists and return false; 137 | if (!$this->isDir($directory)) { 138 | return false; 139 | } 140 | 141 | if ($filter > 3 || $filter < 0) { 142 | throw new RTException(\sprintf('Cannot scan directory Invalid filter value [%d].', $filter)); 143 | } 144 | 145 | $output = []; 146 | $isBreak = $filter === 3 && $filter--; 147 | $filters = ['exists', 'isFile', 'isDir']; 148 | $fMethod = $filters[$filter]; 149 | 150 | $this->scanRecursive($directory, $order, [$this, $fMethod], $isBreak, $isTree, $output, $directory); 151 | return $output; 152 | } 153 | 154 | /** 155 | * 156 | * 157 | * @param string|iterable $files 158 | * @return bool 159 | */ 160 | public function isEmptyDir($files) : bool 161 | { 162 | return !$this->isDir($files) || @empty($this->scandir($files, true)); 163 | } 164 | 165 | /** 166 | * 167 | * 168 | * @param string|iterable $files 169 | * @return iterable 170 | */ 171 | private function toIterable($files) 172 | { 173 | return \is_iterable($files) ? $files : [$files]; 174 | } 175 | 176 | /** 177 | * 178 | * 179 | * @param string $from 180 | * @param string $toDir 181 | * @param bool $overwrite 182 | * 183 | * @return void 184 | */ 185 | public function move(string $from, string $toDir, $overwrite = false) : void 186 | { 187 | if (!$this->isDir($toDir)) { 188 | throw new RTException( 189 | sprintf('Cannot move [%s] because given Argument #2 not a directory [%s].', $from, $toDir) 190 | ); 191 | } 192 | 193 | $this->rename($from, Path::separate([$toDir, Path::basename($from)], Path::FSEP), $overwrite); 194 | } 195 | 196 | /** 197 | * 198 | * 199 | * @param string|iterable $files 200 | * @param int|null $time 201 | * @param int|null $atime 202 | * 203 | * @return void 204 | */ 205 | public function touch($files, ?int $time = null, ?int $atime = null) : void 206 | { 207 | foreach($this->toIterable($files) as $file) { 208 | if (!($time ? $this->exec('touch', $file, $time, $atime) : $this->exec('touch', $file))) { 209 | throw new RTException(\sprintf('Failed to touch file [%s] at [%s].', Path::basename($file), $file)); 210 | } 211 | } 212 | } 213 | 214 | /** 215 | * 216 | * 217 | * @param string $primaryFn 218 | * @param callable $fallbackFn 219 | * 220 | * @return string 221 | */ 222 | private function fnForce(string $primaryFn, callable $fallbackFn) : string 223 | { 224 | return \function_exists($primaryFn) ? $primaryFn : $fallbackFn; 225 | } 226 | 227 | /** 228 | * 229 | * 230 | * @param string|iterable $files 231 | * @return bool 232 | */ 233 | public function isExecutable($files) : bool 234 | { 235 | return $this->bluePrint('executable', $files); 236 | } 237 | 238 | /** 239 | * 240 | * 241 | * @param string $from 242 | * @param string $to 243 | * @param bool $overwrite 244 | * 245 | * @return void 246 | */ 247 | public function rename(string $from, string $to, bool $overwrite = false) : void 248 | { 249 | if (!$overwrite && $this->isReadable($to)) { 250 | throw new RTException(\sprintf('Cannot rename because to: [%s] already exists.', $to)); 251 | } 252 | 253 | if (!$this->exec('rename', $from, $to)) { 254 | if ($this->isDir($from)) { 255 | return; 256 | } 257 | 258 | throw new RTException(\sprintf('Failed cannot rename from [%s] to [%s].', $from, $to)); 259 | } 260 | } 261 | 262 | /** 263 | * 264 | * 265 | * @param string|iterable $files 266 | * @return bool 267 | */ 268 | public function isDir($files) : bool 269 | { 270 | return $this->bluePrint('dir', $files); 271 | } 272 | 273 | /** 274 | * 275 | * 276 | * @param string|iterable $files 277 | * @return bool 278 | */ 279 | public function isReadable($files) : bool 280 | { 281 | return $this->bluePrint('readable', $files); 282 | } 283 | 284 | /** 285 | * 286 | * 287 | * @param string|iterable $files 288 | * @return bool 289 | */ 290 | public function hasDir($files) : bool 291 | { 292 | if (!$this->isDir($files)) { 293 | return false; 294 | } 295 | 296 | foreach($this->toIterable($files) as $file) { 297 | if ($this->isDir($file)) { 298 | return true; 299 | } 300 | } 301 | return false; 302 | } 303 | 304 | /** 305 | * 306 | * 307 | * @param string|iterable $files 308 | * @return bool 309 | */ 310 | public function exists($files) : bool 311 | { 312 | return $this->bluePrint('file_exists', $files, false); 313 | } 314 | 315 | /** 316 | * 317 | * 318 | * @param string|iterable $files 319 | * @return bool 320 | */ 321 | public function isLink($files) : bool 322 | { 323 | return $this->bluePrint('link', $files); 324 | } 325 | 326 | /** 327 | * 328 | * 329 | * @param string|iterable $files 330 | * @return bool 331 | */ 332 | public function isFile($files) : bool 333 | { 334 | return $this->bluePrint('file', $files); 335 | } 336 | 337 | /** 338 | * 339 | * 340 | * @param string|iterable $files 341 | * @return bool 342 | */ 343 | public function isWritable($files) : bool 344 | { 345 | return $this->bluePrint('writable', $files); 346 | } 347 | 348 | /** 349 | * 350 | * 351 | * @param string $filename 352 | * @param string|resource $content 353 | * @param bool $lock 354 | * 355 | * @return void 356 | */ 357 | public function put(string $filename, $content, bool $lock = false) : void 358 | { 359 | $dirname = Path::dirname($filename); 360 | 361 | if (!$this->isDir($dirname)) { 362 | $this->mkdir($dirname); 363 | } 364 | 365 | if (false === $this->exec('file_put_contents', $filename, $content, ($lock ? \LOCK_EX : 0))) { 366 | throw new RTException(\sprintf('Failed to write file [%s] at [%s].'), Path::basename($filename), $filename); 367 | } 368 | } 369 | 370 | /** 371 | * 372 | * 373 | * @param string $from 374 | * @param string $to 375 | * @param bool $overwrite 376 | * 377 | * @return void 378 | */ 379 | public function copy(string $from, string $to, bool $overwrite = false) : void 380 | { 381 | Path::isLocal($from) && $this->throwIfFileNotFound($from); 382 | 383 | $this->mkdir(Path::dirname($to)); 384 | 385 | $notFopened = false; 386 | $copyable = true; 387 | 388 | if (!$overwrite && 389 | null === parse_url($from, \PHP_URL_HOST) && $this->isFile($to)) { 390 | $copyable = filemtime($from) > filemtime($to); 391 | } 392 | 393 | if (!$copyable) { 394 | return; 395 | } 396 | 397 | if (!($fromSource = $this->exec('fopen', $from, 'r'))) { 398 | $notFopened = '`from`'; 399 | } 400 | 401 | if (!($toSource = $this->exec('fopen', $to, 'w', false, \stream_context_create(['ftp' => ['overwrite' => true]])))) { 402 | $notFopened = '`to`'; 403 | } 404 | 405 | if ($notFopened) { 406 | throw new RTException( 407 | \sprintf('Failed to copy file [%s] to [%s] because source %s file could not be opened', $from, $to, $notFopened) 408 | ); 409 | } 410 | 411 | $cbytes = stream_copy_to_stream($fromSource, $toSource); 412 | fclose($fromSource); 413 | fclose($toSource); 414 | unset($fromSource, $toSource); 415 | 416 | if (!$this->isFile($to)) { 417 | throw new RTException(\sprintf('Failed to copy file [%s] to [%s].', $from, $to)); 418 | } 419 | 420 | if (Path::isLocal($from)) { 421 | $this->exec('chmod', $to, fileperms($to) | (fileperms($to) & 0111)); 422 | $this->touch($to, filemtime($from)); 423 | 424 | if ($cbytes !== $fbytes = $this->filesize($from)) { 425 | throw new RTException(\sprintf('Failed to copy the whole content of [%s] to [%s] (%g of %g bytes copied).', $from, $to, $cbytes, $fbytes)); 426 | } 427 | } 428 | } 429 | 430 | /** 431 | * 432 | * 433 | * @param string|iterable $files 434 | * @param bool $force 435 | * 436 | * @return void 437 | */ 438 | public function delete($files, bool $recursive = false, bool $force = true) : void 439 | { 440 | foreach($this->toIterable($files) as $file) { 441 | $force && !$this->isWritable($file) && $this->exec('chmod', $file, 0777); 442 | 443 | if ($this->isLink($file)) { 444 | if (!($this->exec('unlink', $file) || $this->exec('rmdir', $file)) && $this->exists($file)) { 445 | throw new RTException(\sprintf('Failed to remove symlink [%s].', $file)); 446 | } 447 | } 448 | 449 | elseif ($this->isDir($file)) { 450 | 451 | if (!$recursive) { 452 | $tmp = \dirname($file).'\.!!'.base64_encode(random_bytes(3)); 453 | $this->exists($tmp) && $this->delete($tmp, true, $force); 454 | if (!$this->exists($tmp) && $this->exec('rename', $file, $tmp)) { 455 | $file = $tmp; 456 | } 457 | } 458 | 459 | $this->delete($this->scan($file, 0, 3), true, $force); 460 | if (!$this->exec('rmdir', $file) && $this->exists($file)) { 461 | throw new RTException(\sprintf('Failed to remove directory [%s]', $file)); 462 | } 463 | } 464 | 465 | else if (!$this->exec('unlink', $file)) { 466 | throw new RTException(\sprintf('Failed to remove file [%s] at [%s].', \basename($file), $file)); 467 | } 468 | } 469 | } 470 | 471 | /** 472 | * 473 | * 474 | * @param string $directory 475 | * @param bool $traversal 476 | * @param int $order 477 | * @param iterable $filter 478 | * @param bool $isTree 479 | * @param array $output 480 | * @param string $parent 481 | * @param array $tree 482 | * @param array $tmp 483 | * 484 | * @return void 485 | */ 486 | protected function scanRecursive(string $directory, int $order, iterable $filter, bool $isBreak, 487 | bool $isTree, array &$output, string $parent, array &$tree = [], array $tmp = []) : void 488 | { 489 | $files = $this->scandir($directory, true, $order); 490 | 491 | if ($isBreak) { 492 | $output = Path::filePaths([$parent], $files); 493 | return; 494 | } 495 | 496 | foreach($files as $file) { 497 | $path = $parent.Path::FSEP.$file; 498 | 499 | // Handle if isFile $path 500 | // Store files in tmp if not isTree Otherwise Store files in tree 501 | if ($this->isFile($path)) { 502 | $isTree ? \array_push($tree, $file) : \array_push($tmp, $path); 503 | 504 | // Otherwise 505 | // Store directories in tmp and update tmp 506 | } else { 507 | \array_push($tmp, $path); 508 | $subfiles = []; 509 | $this->scanRecursive($path, $order, $isBreak, $filter, $isTree, $output, $path, $subfiles, $tmp); 510 | $isTree ? (($tree[$file] = $subfiles) && ($tmp = $tree)) : \array_push($tmp, ...$subfiles); 511 | } 512 | 513 | // Store the final files or directories in $output 514 | !$isTree && $filter($path) ? \array_push($output, $path) : $isTree && ($output = $tmp); 515 | } 516 | } 517 | 518 | /** 519 | * 520 | * 521 | * @param string $suffix 522 | * @param string|iterable $files 523 | * @param string|null $prefix 524 | * 525 | * @return bool 526 | */ 527 | private function bluePrint(string $suffix, $files, $prefix = null) : bool 528 | { 529 | $prefix = $prefix ?? 'is_'; 530 | 531 | foreach($this->toIterable($files) as $file) { 532 | if (\strlen($file) > self::MAX_PATHLEN) { 533 | throw new RTException(\sprintf('Could not check because path length exceeds [%d] characters.', self::MAX_PATHLEN)); 534 | } 535 | 536 | if (!$this->exec($prefix.$suffix, $file)) return false; 537 | } 538 | 539 | return true; 540 | } 541 | 542 | /** 543 | * 544 | * 545 | * @param string $func 546 | * @return void 547 | * 548 | * @throws RTException when file does not exists. 549 | */ 550 | public static function throwIfFunctionNotExists(string $func) : void 551 | { 552 | if (!\function_exists($func)) { 553 | throw new RTException(\sprintf('Unable to perform filesystem operation because the "%s()" function has been disabled.'), $func); 554 | } 555 | } 556 | 557 | /** 558 | * 559 | * 560 | * @param string|iterable $dirs 561 | * @param int $perms 562 | * 563 | * @return void 564 | */ 565 | public function mkdir($dirs, int $perms = 0777) : void 566 | { 567 | foreach($this->toIterable($dirs) as $dir) { 568 | if ($this->isDir($dir)) { 569 | continue; 570 | } 571 | 572 | if (!$this->exec('mkdir', $dir, $perms, true) && !$this->isDir($dir)) { 573 | throw new RTException(\sprintf('Failed to create directory [%s] at [%s].'), Path::lastDir($dir), $dir); 574 | } 575 | } 576 | } 577 | 578 | /** 579 | * 580 | * 581 | * @param string|iterable $files 582 | * @return void 583 | */ 584 | private function throwIfFileNotFound($files) : void 585 | { 586 | foreach($this->toIterable($files) as $file) { 587 | if (!$this->isFile($file)) { 588 | throw new FileNotFoundException(\sprintf('File not found at the specified file [%s] at path [%s].', Path::basename($file), $file)); 589 | } 590 | } 591 | } 592 | 593 | /** 594 | * 595 | * 596 | * @param string|iterable $files 597 | * @return void 598 | */ 599 | public function empty($files) 600 | { 601 | foreach($this->toIterable($files) as $file) { 602 | $this->isFile($file) ? \file_put_contents($file, null, \LOCK_EX) : $this->remove($this->scan($file, 0, 3)); 603 | } 604 | } 605 | } 606 | ?> --------------------------------------------------------------------------------