/src/Nucleus/Library/FileSystem/FileSystem.php

https://bitbucket.org/jonbiard/nucleus · PHP · 723 lines · 332 code · 109 blank · 282 comment · 88 complexity · cab3c7da76d1473b19031414fadf217b MD5 · raw file

  1. <?php
  2. /**
  3. * Class FileSystem
  4. *
  5. * @package Nucleus\Library
  6. * @subpackage FileSystem
  7. *
  8. * @author Martin Biard <info@martinbiard.com>
  9. */
  10. namespace Nucleus\Library\FileSystem;
  11. use Nucleus\Library\Exception\IOException;
  12. /**
  13. * Class FileSystem
  14. *
  15. * @package Nucleus\Library
  16. * @subpackage FileSystem
  17. *
  18. * @SuppressWarnings(PHPMD.ExcessiveClassComplexity)
  19. */
  20. class FileSystem
  21. {
  22. /**
  23. * Constructor
  24. */
  25. public function __construct()
  26. {
  27. }
  28. /**
  29. * Changes the group of a file or directory
  30. *
  31. * @param string $path The file or directory path
  32. * @param string|int $group The group name or number
  33. *
  34. * @throws IOException When the group cannot be changed
  35. */
  36. public function changeGroup($path, $group)
  37. {
  38. if (!@chgrp($path, $group)) {
  39. throw new IOException("Failed to change group of: $path");
  40. }
  41. }
  42. /**
  43. * Changes the owner of a file or directory
  44. *
  45. * @param string $path The file or directory path
  46. * @param string|int $user The user name or number
  47. *
  48. * @throws IOException When the owner cannot be changed
  49. */
  50. public function changeOwner($path, $user)
  51. {
  52. if (!@chown($path, $user)) {
  53. throw new IOException("Failed to change owner of: $path");
  54. }
  55. }
  56. /**
  57. * Changes the permissions on a file or directory
  58. *
  59. * @param string $path The file or directory path
  60. * @param int $mode The file mode (see PHP's chmod())
  61. *
  62. * @throws IOException When the permissions cannot be changed
  63. */
  64. public function changePermissions($path, $mode)
  65. {
  66. if (!@chmod($path, $mode)) {
  67. throw new IOException("Failed to change permissions of: $path");
  68. }
  69. }
  70. /**
  71. * Copies a file or directory recursively
  72. *
  73. * @param string $path The source path
  74. * @param string $dest The destination path
  75. * @param bool $overwrite Whether to overwrite if destination already exists
  76. *
  77. * @throws IOException Whenever an error occurs during copy
  78. *
  79. * @SuppressWarnings(PHPMD.CyclomaticComplexity)
  80. * @SuppressWarnings(PHPMD.NPathComplexity)
  81. */
  82. public function copy($path, $dest, $overwrite = true)
  83. {
  84. if (@is_dir($path)) {
  85. if (!$overwrite && @file_exists($dest)) {
  86. throw new IOException("Not allowed to overwrite: $dest");
  87. }
  88. if (($perms = @fileperms($path)) === false) {
  89. throw new IOException("Failed to get permissions of: $path");
  90. }
  91. $perms = $perms & 511;
  92. @mkdir($dest);
  93. if (!@is_dir($dest)) {
  94. throw new IOException("Failed to create directory: $dest");
  95. }
  96. if (!@chmod($dest, $perms)) {
  97. throw new IOException("Failed to change mode of: $dest");
  98. }
  99. if (($dh = @opendir($path)) === false) {
  100. throw new IOException("Failed to open directory: $path");
  101. }
  102. while (($item = @readdir($dh)) !== false) {
  103. if ($item == '.' || $item == '..') {
  104. continue;
  105. }
  106. $this->copy("$path/$item", "$dest/$item", $overwrite);
  107. }
  108. @closedir($dh);
  109. } else {
  110. if (!$overwrite && @file_exists($dest)) {
  111. throw new IOException("Not allowed to overwrite: $dest");
  112. }
  113. if (($perms = @fileperms($path)) === false) {
  114. throw new IOException("Failed to get permissions of: $path");
  115. }
  116. $perms = $perms & 511;
  117. if (!@copy($path, $dest)) {
  118. throw new IOException("Failed to copy: $path to $dest");
  119. }
  120. if (!@chmod($dest, $perms)) {
  121. throw new IOException("Failed to change mode of: $path");
  122. }
  123. }
  124. }
  125. /**
  126. * Creates a directory
  127. *
  128. * @param string $path The file or directory path
  129. * @param int $chmod The permissions
  130. *
  131. * @throws IOException When the directory cannot be created
  132. */
  133. public function createDirectory($path, $chmod = 0777)
  134. {
  135. if (@file_exists($path) && @is_dir($path)) {
  136. return;
  137. }
  138. $old = umask(0);
  139. if (!@mkdir($path, $chmod, true)) {
  140. throw new IOException("Failed to create directory: $path");
  141. }
  142. umask($old);
  143. }
  144. /**
  145. * Deletes a file or directory recursively
  146. *
  147. * @param string $path The file or directory path
  148. *
  149. * @throws IOException When the file or directory cannot be deleted
  150. */
  151. public function delete($path)
  152. {
  153. if (!@file_exists($path)) {
  154. return;
  155. }
  156. if (@is_dir($path)) {
  157. if (($dh = @opendir($path)) === false) {
  158. throw new IOException("Failed to open directory: $path");
  159. }
  160. while (($item = @readdir($dh)) !== false) {
  161. if ($item == '.' || $item == '..') {
  162. continue;
  163. }
  164. $this->delete("$path/$item");
  165. }
  166. @closedir($dh);
  167. if (!@rmdir($path)) {
  168. throw new IOException("Failed to delete: $path");
  169. }
  170. } else {
  171. if (!@unlink($path)) {
  172. throw new IOException("Failed to delete: $path");
  173. }
  174. }
  175. }
  176. /**
  177. * Checks if a file or directory exists
  178. *
  179. * @param string $path The file or directory path
  180. *
  181. * @return bool True if the file or directory exists
  182. */
  183. public function exists($path)
  184. {
  185. return @file_exists($path);
  186. }
  187. /**
  188. * Gets the byte size of the file or directory contents
  189. *
  190. * @param string $path The file or directory path
  191. *
  192. * @return int The total amount of bytes
  193. *
  194. * @throws IOException When an error occurs during the process
  195. */
  196. public function getByteSize($path)
  197. {
  198. $bytes = 0;
  199. if (@file_exists($path)) {
  200. if (@is_dir($path)) {
  201. if (($dh = @opendir($path)) === false) {
  202. throw new IOException("Failed to open directory: $path");
  203. }
  204. while (($item = @readdir($dh)) !== false) {
  205. if ($item == '.' || $item == '..') {
  206. continue;
  207. }
  208. $bytes += $this->getByteSize("$path/$item");
  209. }
  210. @closedir($dh);
  211. } else {
  212. if (($bytes = @filesize($path)) === false) {
  213. throw new IOException("Failed to get byte size of: $path");
  214. }
  215. }
  216. }
  217. return $bytes;
  218. }
  219. /**
  220. * Gets the file's group
  221. *
  222. * @param string $path The file or directory path
  223. *
  224. * @return int The file's group ID
  225. *
  226. * @throws IOException When the file's group cannot be fetched
  227. */
  228. public function getGroup($path)
  229. {
  230. if (($filegroup = @filegroup($path)) === false) {
  231. throw new IOException("Failed to get group of: $path");
  232. }
  233. return $filegroup;
  234. }
  235. /**
  236. * Gets the inode of a file or directory
  237. *
  238. * @param string $path The file or directory path
  239. *
  240. * @return int The file's inode
  241. *
  242. * @throws IOException When the inode cannot be fetched
  243. */
  244. public function getInode($path)
  245. {
  246. if (($fileinode = @fileinode($path)) === false) {
  247. throw new IOException("Failed to get inode of: $path");
  248. }
  249. return $fileinode;
  250. }
  251. /**
  252. * Gets the owner of a file or directory
  253. *
  254. * @param string $path The file or directory path
  255. *
  256. * @return int The user ID of the owner of the file
  257. *
  258. * @throws IOException When the owner ID cannot be fetched
  259. */
  260. public function getOwner($path)
  261. {
  262. if (($fileowner = @fileowner($path)) === false) {
  263. throw new IOException("Failed to get owner of: $path");
  264. }
  265. return $fileowner;
  266. }
  267. /**
  268. * Gets the permissions of a file or directory
  269. *
  270. * @param string $path The file or directory path
  271. *
  272. * @return int The permissions of the file
  273. *
  274. * @throws IOException When the permissions cannot be fetched
  275. */
  276. public function getPermissions($path)
  277. {
  278. if (($fileperms = @fileperms($path)) === false) {
  279. throw new IOException("Failed to get permissions of: $path");
  280. }
  281. return $fileperms;
  282. }
  283. /**
  284. * Gets the last access time of a file or directory
  285. *
  286. * @param string $path The file or directory path
  287. *
  288. * @return int The last access time as a Unix timestamp
  289. *
  290. * @throws IOException When the time cannot be fetched
  291. */
  292. public function getTimeLastAccessed($path)
  293. {
  294. if (($fileatime = @fileatime($path)) === false) {
  295. throw new IOException("Failed to get last access time of: $path");
  296. }
  297. return $fileatime;
  298. }
  299. /**
  300. * Gets the last changed time of a file or directory
  301. *
  302. * @param string $path The file or directory path
  303. *
  304. * @return int The last changed time as a Unix timestamp
  305. *
  306. * @throws IOException When the time cannot be fetched
  307. */
  308. public function getTimeLastChanged($path)
  309. {
  310. if (($filectime = @filectime($path)) === false) {
  311. throw new IOException("Failed to get last changed time of: $path");
  312. }
  313. return $filectime;
  314. }
  315. /**
  316. * Gets the last modified time of a file or directory
  317. *
  318. * @param string $path The file or directory path
  319. *
  320. * @return int The last modified time as a Unix timestamp
  321. *
  322. * @throws IOException When the time cannot be fetched
  323. */
  324. public function getTimeLastModified($path)
  325. {
  326. if (($filemtime = @filemtime($path)) === false) {
  327. throw new IOException("Failed to get last modified time of: $path");
  328. }
  329. return $filemtime;
  330. }
  331. /**
  332. * Checks if the path is a directory
  333. *
  334. * @param string $path The directory path
  335. *
  336. * @return bool True if the path is a directory
  337. */
  338. public function isDirectory($path)
  339. {
  340. return @is_dir($path);
  341. }
  342. /**
  343. * Check if the file or directory is executable
  344. *
  345. * @param string $path The file or directory path
  346. *
  347. * @return bool True if the file or directory is executable
  348. */
  349. public function isExecutable($path)
  350. {
  351. return @is_executable($path);
  352. }
  353. /**
  354. * Checks if the path is a file
  355. *
  356. * @param string $path The file path
  357. *
  358. * @return bool True if the path is a file
  359. */
  360. public function isFile($path)
  361. {
  362. return @is_file($path);
  363. }
  364. /**
  365. * Check if the file or directory is readable
  366. *
  367. * @param string $path The file or directory path
  368. *
  369. * @return bool True if the file or directory is readable
  370. */
  371. public function isReadable($path)
  372. {
  373. return @is_readable($path);
  374. }
  375. /**
  376. * Check if the file is a symbolic link
  377. *
  378. * @param string $path The file or directory path
  379. *
  380. * @return bool True if the file is a symbolic link
  381. */
  382. public function isSymLink($path)
  383. {
  384. return @is_link($path);
  385. }
  386. /**
  387. * Check if the file is an uploaded file
  388. *
  389. * @param string $path The file or directory path
  390. *
  391. * @return bool True if the file is an uploaded file
  392. */
  393. public function isUploadedFile($path)
  394. {
  395. return @is_uploaded_file($path);
  396. }
  397. /**
  398. * Check if the file or directory is writeable
  399. *
  400. * @param string $path The file or directory path
  401. *
  402. * @return bool True if the file or directory is writeable
  403. */
  404. public function isWritable($path)
  405. {
  406. return @is_writable($path);
  407. }
  408. /**
  409. * Moves a file or directory
  410. *
  411. * @param string $path The source path
  412. * @param string $dest The destination path
  413. * @param bool $overwrite Whether to overwrite if destination exists
  414. *
  415. * @throws IOException When the move cannot be completed
  416. *
  417. * @SuppressWarnings(PHPMD.CyclomaticComplexity)
  418. * @SuppressWarnings(PHPMD.NPathComplexity)
  419. */
  420. public function move($path, $dest, $overwrite = true)
  421. {
  422. if (@is_dir($path)) {
  423. if (!$overwrite && @file_exists($dest)) {
  424. throw new IOException("Not allowed to overwrite: $dest");
  425. }
  426. if (($perms = @fileperms($path)) === false) {
  427. throw new IOException("Failed to get permissions of: $path");
  428. }
  429. $perms = $perms & 511;
  430. @mkdir($dest);
  431. if (!@is_dir($dest)) {
  432. throw new IOException("Failed to create directory: $dest");
  433. }
  434. if (!@chmod($dest, $perms)) {
  435. throw new IOException("Failed to change mode of: $dest");
  436. }
  437. if (($dh = @opendir($path)) === false) {
  438. throw new IOException("Failed to open directory: $path");
  439. }
  440. while (($item = @readdir($dh)) !== false) {
  441. if ($item == '.' || $item == '..') {
  442. continue;
  443. }
  444. $this->move("$path/$item", "$dest/$item", $overwrite);
  445. }
  446. @closedir($dh);
  447. if (!@rmdir($path)) {
  448. throw new IOException("Failed to delete: $path");
  449. }
  450. } else {
  451. if (!$overwrite && @file_exists($dest)) {
  452. throw new IOException("Not allowed to overwrite: $dest");
  453. }
  454. if (($perms = @fileperms($path)) === false) {
  455. throw new IOException("Failed to get permissions of: $path");
  456. }
  457. $perms = $perms & 511;
  458. if (!@rename($path, $dest)) {
  459. throw new IOException("Failed to move: $path to $dest");
  460. }
  461. if (!@chmod($dest, $perms)) {
  462. throw new IOException("Failed to change mode of: $dest");
  463. }
  464. }
  465. }
  466. /**
  467. * Opens a directory and returns a directory object
  468. *
  469. * @param string $path The directory path
  470. *
  471. * @return Directory New directory object
  472. */
  473. public function openDirectory($path)
  474. {
  475. return new Directory($path);
  476. }
  477. /**
  478. * Opens a file and returns a file object
  479. *
  480. * @param string $path The file path
  481. * @param int $mode The mode in which to open the file (see PHP's fopen())
  482. *
  483. * @return File New file object
  484. */
  485. public function openFile($path, $mode)
  486. {
  487. return new File($path, $mode);
  488. }
  489. /**
  490. * Reads a file and returns the contents as a string
  491. *
  492. * @param string $path The file or directory path
  493. * @param int $offset The offset to start the reading at
  494. * @param null|int $length The amount of characters to read
  495. *
  496. * @return string The contents of the file
  497. *
  498. * @throws IOException When the contents of the file cannot be fetched
  499. */
  500. public function readFile($path, $offset = -1, $length = null)
  501. {
  502. if ($length !== null) {
  503. $data = @file_get_contents($path, false, null, $offset, $length);
  504. } else {
  505. $data = @file_get_contents($path, false, null, $offset);
  506. }
  507. if ($data === false) {
  508. throw new IOException("Failed to read contents from file: $path");
  509. }
  510. return $data;
  511. }
  512. /**
  513. * Reads a file and returns the contents as an array of lines
  514. *
  515. * @param string $path The file or directory path
  516. * @param bool $stripNewLines Whether to strip the trailing newline character at the ends of each line or not
  517. * @param bool $skipEmptyLines Whether to skip empty lines or not
  518. *
  519. * @return array The array of lines
  520. *
  521. * @throws IOException When the contents of the file cannot be fetched
  522. */
  523. public function readFileLines($path, $stripNewLines = true, $skipEmptyLines = true)
  524. {
  525. $flags = 0;
  526. if ($stripNewLines) {
  527. $flags |= FILE_IGNORE_NEW_LINES;
  528. }
  529. if ($skipEmptyLines) {
  530. $flags |= FILE_SKIP_EMPTY_LINES;
  531. }
  532. if (($lines = @file($path, $flags)) === false) {
  533. throw new IOException("Failed to read lines from file: $path");
  534. }
  535. return $lines;
  536. }
  537. /**
  538. * Saves data to a file
  539. *
  540. * Note: Will also recursively create the directories if they do not yet exist
  541. *
  542. * @param string $path The file or directory path
  543. * @param mixed $data The file contents
  544. * @param bool $append Whether to append at the end of the file
  545. * @param bool $lock Whether to lock the file while saving
  546. *
  547. * @return int The amount of bytes saved
  548. *
  549. * @throws IOException When the directories cannot be created or writing to the file fails
  550. */
  551. public function saveFile($path, $data, $append = false, $lock = true)
  552. {
  553. $dir = dirname($path);
  554. if (!@is_dir($dir)) {
  555. if (!@mkdir($dir, 0777, true)) {
  556. throw new IOException("Failed to create directory: $dir");
  557. }
  558. }
  559. $flags = 0;
  560. if ($append) {
  561. $flags |= FILE_APPEND;
  562. }
  563. if ($lock) {
  564. $flags |= LOCK_EX;
  565. }
  566. if (($bytes = @file_put_contents($path, $data, $flags)) === false) {
  567. throw new IOException("Failed to save data to file: $path");
  568. }
  569. return $bytes;
  570. }
  571. /**
  572. * Scans a directory and returns the filenames it contains
  573. *
  574. * Valid sorting methods are: 'asc' and 'desc'
  575. *
  576. * @param string $path The file or directory path
  577. * @param string $sort The sorting method
  578. * @param string $pattern The matching pattern to filter filenames as a regular expression
  579. *
  580. * @return array The array of filenames
  581. *
  582. * @throws IOException When the contents of the directory cannot be fetched
  583. */
  584. public function scanDirectory($path, $sort = 'asc', $pattern = '.*')
  585. {
  586. $sort = strtolower($sort) === 'desc' ? SCANDIR_SORT_DESCENDING : SCANDIR_SORT_ASCENDING;
  587. if (($scan = @scandir($path, $sort)) === false) {
  588. throw new IOException("Failed to scan directory: $path");
  589. }
  590. foreach ($scan as $k => $v) {
  591. if ($v === '.' || $v === '..' || !preg_match($pattern, $v)) {
  592. unset($scan[$k]);
  593. }
  594. }
  595. return array_values($scan);
  596. }
  597. /**
  598. * Sets the access and modified time of the file or directory
  599. *
  600. * @param string $path The file or directory path
  601. * @param null|int $time The time as a Unix timestamp or null for current time
  602. *
  603. * @throws IOException When the file or directory cannot be touched
  604. */
  605. public function touch($path, $time = null)
  606. {
  607. if ($time === null) {
  608. $time = time();
  609. }
  610. if (!@touch($path, $time)) {
  611. throw new IOException("Failed to touch: $path");
  612. }
  613. }
  614. }