PageRenderTime 48ms CodeModel.GetById 19ms RepoModel.GetById 1ms app.codeStats 0ms

/classes/fDirectory.php

https://bitbucket.org/wbond/flourish/
PHP | 579 lines | 304 code | 80 blank | 195 comment | 40 complexity | 9103bb91436814d649fe9a437283713a MD5 | raw file
  1. <?php
  2. /**
  3. * Represents a directory on the filesystem, also provides static directory-related methods
  4. *
  5. * @copyright Copyright (c) 2007-2011 Will Bond, others
  6. * @author Will Bond [wb] <will@flourishlib.com>
  7. * @author Will Bond, iMarc LLC [wb-imarc] <will@imarc.net>
  8. * @license http://flourishlib.com/license
  9. *
  10. * @package Flourish
  11. * @link http://flourishlib.com/fDirectory
  12. *
  13. * @version 1.0.0b14
  14. * @changes 1.0.0b14 Fixed a bug in ::delete() where a non-existent method was being called on fFilesystem, added a permission check to ::delete() [wb, 2011-08-23]
  15. * @changes 1.0.0b13 Added the ::clear() method [wb, 2011-01-10]
  16. * @changes 1.0.0b12 Fixed ::scanRecursive() to not add duplicate entries for certain nested directory structures [wb, 2010-08-10]
  17. * @changes 1.0.0b11 Fixed ::scan() to properly add trailing /s for directories [wb, 2010-03-16]
  18. * @changes 1.0.0b10 BackwardsCompatibilityBreak - Fixed ::scan() and ::scanRecursive() to strip the current directory's path before matching, added support for glob style matching [wb, 2010-03-05]
  19. * @changes 1.0.0b9 Changed the way directories deleted in a filesystem transaction are handled, including improvements to the exception that is thrown [wb+wb-imarc, 2010-03-05]
  20. * @changes 1.0.0b8 Backwards Compatibility Break - renamed ::getFilesize() to ::getSize(), added ::move() [wb, 2009-12-16]
  21. * @changes 1.0.0b7 Fixed ::__construct() to throw an fValidationException when the directory does not exist [wb, 2009-08-21]
  22. * @changes 1.0.0b6 Fixed a bug where deleting a directory would prevent any future operations in the same script execution on a file or directory with the same path [wb, 2009-08-20]
  23. * @changes 1.0.0b5 Added the ability to skip checks in ::__construct() for better performance in conjunction with fFilesystem::createObject() [wb, 2009-08-06]
  24. * @changes 1.0.0b4 Refactored ::scan() to use the new fFilesystem::createObject() method [wb, 2009-01-21]
  25. * @changes 1.0.0b3 Added the $regex_filter parameter to ::scan() and ::scanRecursive(), fixed bug in ::scanRecursive() [wb, 2009-01-05]
  26. * @changes 1.0.0b2 Removed some unnecessary error suppresion operators [wb, 2008-12-11]
  27. * @changes 1.0.0b The initial implementation [wb, 2007-12-21]
  28. */
  29. class fDirectory
  30. {
  31. // The following constants allow for nice looking callbacks to static methods
  32. const create = 'fDirectory::create';
  33. const makeCanonical = 'fDirectory::makeCanonical';
  34. /**
  35. * Creates a directory on the filesystem and returns an object representing it
  36. *
  37. * The directory creation is done recursively, so if any of the parent
  38. * directories do not exist, they will be created.
  39. *
  40. * This operation will be reverted by a filesystem transaction being rolled back.
  41. *
  42. * @throws fValidationException When no directory was specified, or the directory already exists
  43. *
  44. * @param string $directory The path to the new directory
  45. * @param numeric $mode The mode (permissions) to use when creating the directory. This should be an octal number (requires a leading zero). This has no effect on the Windows platform.
  46. * @return fDirectory
  47. */
  48. static public function create($directory, $mode=0777)
  49. {
  50. if (empty($directory)) {
  51. throw new fValidationException('No directory name was specified');
  52. }
  53. if (file_exists($directory)) {
  54. throw new fValidationException(
  55. 'The directory specified, %s, already exists',
  56. $directory
  57. );
  58. }
  59. $parent_directory = fFilesystem::getPathInfo($directory, 'dirname');
  60. if (!file_exists($parent_directory)) {
  61. fDirectory::create($parent_directory, $mode);
  62. }
  63. if (!is_writable($parent_directory)) {
  64. throw new fEnvironmentException(
  65. 'The directory specified, %s, is inside of a directory that is not writable',
  66. $directory
  67. );
  68. }
  69. mkdir($directory, $mode);
  70. $directory = new fDirectory($directory);
  71. fFilesystem::recordCreate($directory);
  72. return $directory;
  73. }
  74. /**
  75. * Makes sure a directory has a `/` or `\` at the end
  76. *
  77. * @param string $directory The directory to check
  78. * @return string The directory name in canonical form
  79. */
  80. static public function makeCanonical($directory)
  81. {
  82. if (substr($directory, -1) != '/' && substr($directory, -1) != '\\') {
  83. $directory .= DIRECTORY_SEPARATOR;
  84. }
  85. return $directory;
  86. }
  87. /**
  88. * A backtrace from when the file was deleted
  89. *
  90. * @var array
  91. */
  92. protected $deleted = NULL;
  93. /**
  94. * The full path to the directory
  95. *
  96. * @var string
  97. */
  98. protected $directory;
  99. /**
  100. * Creates an object to represent a directory on the filesystem
  101. *
  102. * If multiple fDirectory objects are created for a single directory,
  103. * they will reflect changes in each other including rename and delete
  104. * actions.
  105. *
  106. * @throws fValidationException When no directory was specified, when the directory does not exist or when the path specified is not a directory
  107. *
  108. * @param string $directory The path to the directory
  109. * @param boolean $skip_checks If file checks should be skipped, which improves performance, but may cause undefined behavior - only skip these if they are duplicated elsewhere
  110. * @return fDirectory
  111. */
  112. public function __construct($directory, $skip_checks=FALSE)
  113. {
  114. if (!$skip_checks) {
  115. if (empty($directory)) {
  116. throw new fValidationException('No directory was specified');
  117. }
  118. if (!is_readable($directory)) {
  119. throw new fValidationException(
  120. 'The directory specified, %s, does not exist or is not readable',
  121. $directory
  122. );
  123. }
  124. if (!is_dir($directory)) {
  125. throw new fValidationException(
  126. 'The directory specified, %s, is not a directory',
  127. $directory
  128. );
  129. }
  130. }
  131. $directory = self::makeCanonical(realpath($directory));
  132. $this->directory =& fFilesystem::hookFilenameMap($directory);
  133. $this->deleted =& fFilesystem::hookDeletedMap($directory);
  134. // If the directory is listed as deleted and we are not inside a transaction,
  135. // but we've gotten to here, then the directory exists, so we can wipe the backtrace
  136. if ($this->deleted !== NULL && !fFilesystem::isInsideTransaction()) {
  137. fFilesystem::updateDeletedMap($directory, NULL);
  138. }
  139. }
  140. /**
  141. * All requests that hit this method should be requests for callbacks
  142. *
  143. * @internal
  144. *
  145. * @param string $method The method to create a callback for
  146. * @return callback The callback for the method requested
  147. */
  148. public function __get($method)
  149. {
  150. return array($this, $method);
  151. }
  152. /**
  153. * Returns the full filesystem path for the directory
  154. *
  155. * @return string The full filesystem path
  156. */
  157. public function __toString()
  158. {
  159. return $this->getPath();
  160. }
  161. /**
  162. * Removes all files and directories inside of the directory
  163. *
  164. * @return void
  165. */
  166. public function clear()
  167. {
  168. if ($this->deleted) {
  169. return;
  170. }
  171. foreach ($this->scan() as $file) {
  172. $file->delete();
  173. }
  174. }
  175. /**
  176. * Will delete a directory and all files and directories inside of it
  177. *
  178. * This operation will not be performed until the filesystem transaction
  179. * has been committed, if a transaction is in progress. Any non-Flourish
  180. * code (PHP or system) will still see this directory and all contents as
  181. * existing until that point.
  182. *
  183. * @return void
  184. */
  185. public function delete()
  186. {
  187. if ($this->deleted) {
  188. return;
  189. }
  190. if (!$this->getParent()->isWritable()) {
  191. throw new fEnvironmentException(
  192. 'The directory, %s, can not be deleted because the directory containing it is not writable',
  193. $this->directory
  194. );
  195. }
  196. $files = $this->scan();
  197. foreach ($files as $file) {
  198. $file->delete();
  199. }
  200. // Allow filesystem transactions
  201. if (fFilesystem::isInsideTransaction()) {
  202. return fFilesystem::recordDelete($this);
  203. }
  204. rmdir($this->directory);
  205. fFilesystem::updateDeletedMap($this->directory, debug_backtrace());
  206. fFilesystem::updateFilenameMapForDirectory($this->directory, '*DELETED at ' . time() . ' with token ' . uniqid('', TRUE) . '* ' . $this->directory);
  207. }
  208. /**
  209. * Gets the name of the directory
  210. *
  211. * @return string The name of the directory
  212. */
  213. public function getName()
  214. {
  215. return fFilesystem::getPathInfo($this->directory, 'basename');
  216. }
  217. /**
  218. * Gets the parent directory
  219. *
  220. * @return fDirectory The object representing the parent directory
  221. */
  222. public function getParent()
  223. {
  224. $this->tossIfDeleted();
  225. $dirname = fFilesystem::getPathInfo($this->directory, 'dirname');
  226. if ($dirname == $this->directory) {
  227. throw new fEnvironmentException(
  228. 'The current directory does not have a parent directory'
  229. );
  230. }
  231. return new fDirectory($dirname);
  232. }
  233. /**
  234. * Gets the directory's current path
  235. *
  236. * If the web path is requested, uses translations set with
  237. * fFilesystem::addWebPathTranslation()
  238. *
  239. * @param boolean $translate_to_web_path If the path should be the web path
  240. * @return string The path for the directory
  241. */
  242. public function getPath($translate_to_web_path=FALSE)
  243. {
  244. $this->tossIfDeleted();
  245. if ($translate_to_web_path) {
  246. return fFilesystem::translateToWebPath($this->directory);
  247. }
  248. return $this->directory;
  249. }
  250. /**
  251. * Gets the disk usage of the directory and all files and folders contained within
  252. *
  253. * This method may return incorrect results if files over 2GB exist and the
  254. * server uses a 32 bit operating system
  255. *
  256. * @param boolean $format If the filesize should be formatted for human readability
  257. * @param integer $decimal_places The number of decimal places to format to (if enabled)
  258. * @return integer|string If formatted, a string with filesize in b/kb/mb/gb/tb, otherwise an integer
  259. */
  260. public function getSize($format=FALSE, $decimal_places=1)
  261. {
  262. $this->tossIfDeleted();
  263. $size = 0;
  264. $children = $this->scan();
  265. foreach ($children as $child) {
  266. $size += $child->getSize();
  267. }
  268. if (!$format) {
  269. return $size;
  270. }
  271. return fFilesystem::formatFilesize($size, $decimal_places);
  272. }
  273. /**
  274. * Check to see if the current directory is writable
  275. *
  276. * @return boolean If the directory is writable
  277. */
  278. public function isWritable()
  279. {
  280. $this->tossIfDeleted();
  281. return is_writable($this->directory);
  282. }
  283. /**
  284. * Moves the current directory into a different directory
  285. *
  286. * Please note that ::rename() will rename a directory in its current
  287. * parent directory or rename it into a different parent directory.
  288. *
  289. * If the current directory's name already exists in the new parent
  290. * directory and the overwrite flag is set to false, the name will be
  291. * changed to a unique name.
  292. *
  293. * This operation will be reverted if a filesystem transaction is in
  294. * progress and is later rolled back.
  295. *
  296. * @throws fValidationException When the new parent directory passed is not a directory, is not readable or is a sub-directory of this directory
  297. *
  298. * @param fDirectory|string $new_parent_directory The directory to move this directory into
  299. * @param boolean $overwrite If the current filename already exists in the new directory, `TRUE` will cause the file to be overwritten, `FALSE` will cause the new filename to change
  300. * @return fDirectory The directory object, to allow for method chaining
  301. */
  302. public function move($new_parent_directory, $overwrite)
  303. {
  304. if (!$new_parent_directory instanceof fDirectory) {
  305. $new_parent_directory = new fDirectory($new_parent_directory);
  306. }
  307. if (strpos($new_parent_directory->getPath(), $this->getPath()) === 0) {
  308. throw new fValidationException('It is not possible to move a directory into one of its sub-directories');
  309. }
  310. return $this->rename($new_parent_directory->getPath() . $this->getName(), $overwrite);
  311. }
  312. /**
  313. * Renames the current directory
  314. *
  315. * This operation will NOT be performed until the filesystem transaction
  316. * has been committed, if a transaction is in progress. Any non-Flourish
  317. * code (PHP or system) will still see this directory (and all contained
  318. * files/dirs) as existing with the old paths until that point.
  319. *
  320. * @param string $new_dirname The new full path to the directory or a new name in the current parent directory
  321. * @param boolean $overwrite If the new dirname already exists, TRUE will cause the file to be overwritten, FALSE will cause the new filename to change
  322. * @return void
  323. */
  324. public function rename($new_dirname, $overwrite)
  325. {
  326. $this->tossIfDeleted();
  327. if (!$this->getParent()->isWritable()) {
  328. throw new fEnvironmentException(
  329. 'The directory, %s, can not be renamed because the directory containing it is not writable',
  330. $this->directory
  331. );
  332. }
  333. // If the dirname does not contain any folder traversal, rename the dir in the current parent directory
  334. if (preg_match('#^[^/\\\\]+$#D', $new_dirname)) {
  335. $new_dirname = $this->getParent()->getPath() . $new_dirname;
  336. }
  337. $info = fFilesystem::getPathInfo($new_dirname);
  338. if (!file_exists($info['dirname'])) {
  339. throw new fProgrammerException(
  340. 'The new directory name specified, %s, is inside of a directory that does not exist',
  341. $new_dirname
  342. );
  343. }
  344. if (file_exists($new_dirname)) {
  345. if (!is_writable($new_dirname)) {
  346. throw new fEnvironmentException(
  347. 'The new directory name specified, %s, already exists, but is not writable',
  348. $new_dirname
  349. );
  350. }
  351. if (!$overwrite) {
  352. $new_dirname = fFilesystem::makeUniqueName($new_dirname);
  353. }
  354. } else {
  355. $parent_dir = new fDirectory($info['dirname']);
  356. if (!$parent_dir->isWritable()) {
  357. throw new fEnvironmentException(
  358. 'The new directory name specified, %s, is inside of a directory that is not writable',
  359. $new_dirname
  360. );
  361. }
  362. }
  363. rename($this->directory, $new_dirname);
  364. // Make the dirname absolute
  365. $new_dirname = fDirectory::makeCanonical(realpath($new_dirname));
  366. // Allow filesystem transactions
  367. if (fFilesystem::isInsideTransaction()) {
  368. fFilesystem::rename($this->directory, $new_dirname);
  369. }
  370. fFilesystem::updateFilenameMapForDirectory($this->directory, $new_dirname);
  371. }
  372. /**
  373. * Performs a [http://php.net/scandir scandir()] on a directory, removing the `.` and `..` entries
  374. *
  375. * If the `$filter` looks like a valid PCRE pattern - matching delimeters
  376. * (a delimeter can be any non-alphanumeric, non-backslash, non-whitespace
  377. * character) followed by zero or more of the flags `i`, `m`, `s`, `x`,
  378. * `e`, `A`, `D`, `S`, `U`, `X`, `J`, `u` - then
  379. * [http://php.net/preg_match `preg_match()`] will be used.
  380. *
  381. * Otherwise the `$filter` will do a case-sensitive match with `*` matching
  382. * zero or more characters and `?` matching a single character.
  383. *
  384. * On all OSes (even Windows), directories will be separated by `/`s when
  385. * comparing with the `$filter`.
  386. *
  387. * @param string $filter A PCRE or glob pattern to filter files/directories by path - directories can be detected by checking for a trailing / (even on Windows)
  388. * @return array The fFile (or fImage) and fDirectory objects for the files/directories in this directory
  389. */
  390. public function scan($filter=NULL)
  391. {
  392. $this->tossIfDeleted();
  393. $files = array_diff(scandir($this->directory), array('.', '..'));
  394. $objects = array();
  395. if ($filter && !preg_match('#^([^a-zA-Z0-9\\\\\s]).*\1[imsxeADSUXJu]*$#D', $filter)) {
  396. $filter = '#^' . strtr(
  397. preg_quote($filter, '#'),
  398. array(
  399. '\\*' => '.*',
  400. '\\?' => '.'
  401. )
  402. ) . '$#D';
  403. }
  404. natcasesort($files);
  405. foreach ($files as $file) {
  406. if ($filter) {
  407. $test_path = (is_dir($this->directory . $file)) ? $file . '/' : $file;
  408. if (!preg_match($filter, $test_path)) {
  409. continue;
  410. }
  411. }
  412. $objects[] = fFilesystem::createObject($this->directory . $file);
  413. }
  414. return $objects;
  415. }
  416. /**
  417. * Performs a **recursive** [http://php.net/scandir scandir()] on a directory, removing the `.` and `..` entries
  418. *
  419. * @param string $filter A PCRE or glob pattern to filter files/directories by path - see ::scan() for details
  420. * @return array The fFile (or fImage) and fDirectory objects for the files/directories (listed recursively) in this directory
  421. */
  422. public function scanRecursive($filter=NULL)
  423. {
  424. $this->tossIfDeleted();
  425. $objects = $this->scan();
  426. for ($i=0; $i < sizeof($objects); $i++) {
  427. if ($objects[$i] instanceof fDirectory) {
  428. array_splice($objects, $i+1, 0, $objects[$i]->scan());
  429. }
  430. }
  431. if ($filter) {
  432. if (!preg_match('#^([^a-zA-Z0-9\\\\\s*?^$]).*\1[imsxeADSUXJu]*$#D', $filter)) {
  433. $filter = '#^' . strtr(
  434. preg_quote($filter, '#'),
  435. array(
  436. '\\*' => '.*',
  437. '\\?' => '.'
  438. )
  439. ) . '$#D';
  440. }
  441. $new_objects = array();
  442. $strip_length = strlen($this->getPath());
  443. foreach ($objects as $object) {
  444. $test_path = substr($object->getPath(), $strip_length);
  445. $test_path = str_replace(DIRECTORY_SEPARATOR, '/', $test_path);
  446. if (!preg_match($filter, $test_path)) {
  447. continue;
  448. }
  449. $new_objects[] = $object;
  450. }
  451. $objects = $new_objects;
  452. }
  453. return $objects;
  454. }
  455. /**
  456. * Throws an exception if the directory has been deleted
  457. *
  458. * @return void
  459. */
  460. protected function tossIfDeleted()
  461. {
  462. if ($this->deleted) {
  463. throw new fProgrammerException(
  464. "The action requested can not be performed because the directory has been deleted\n\nBacktrace for fDirectory::delete() call:\n%s",
  465. fCore::backtrace(0, $this->deleted)
  466. );
  467. }
  468. }
  469. }
  470. /**
  471. * Copyright (c) 2007-2011 Will Bond <will@flourishlib.com>, others
  472. *
  473. * Permission is hereby granted, free of charge, to any person obtaining a copy
  474. * of this software and associated documentation files (the "Software"), to deal
  475. * in the Software without restriction, including without limitation the rights
  476. * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
  477. * copies of the Software, and to permit persons to whom the Software is
  478. * furnished to do so, subject to the following conditions:
  479. *
  480. * The above copyright notice and this permission notice shall be included in
  481. * all copies or substantial portions of the Software.
  482. *
  483. * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  484. * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  485. * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  486. * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  487. * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  488. * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
  489. * THE SOFTWARE.
  490. */