PageRenderTime 47ms CodeModel.GetById 10ms RepoModel.GetById 0ms app.codeStats 0ms

/core/src/main/php/io/File.class.php

http://github.com/xp-framework/xp-framework
PHP | 726 lines | 342 code | 56 blank | 328 comment | 66 complexity | fb293fcaeacce3763df6035771a1fce9 MD5 | raw file
Possible License(s): BSD-3-Clause
  1. <?php
  2. /* This class is part of the XP framework
  3. *
  4. * $Id$
  5. */
  6. // Mode constants for open() method
  7. define('FILE_MODE_READ', 'rb'); // Read
  8. define('FILE_MODE_READWRITE', 'rb+'); // Read/Write
  9. define('FILE_MODE_WRITE', 'wb'); // Write
  10. define('FILE_MODE_REWRITE', 'wb+'); // Read/Write, truncate on open
  11. define('FILE_MODE_APPEND', 'ab'); // Append (Read-only)
  12. define('FILE_MODE_READAPPEND','ab+'); // Append (Read/Write)
  13. uses(
  14. 'io.Stream',
  15. 'io.IOException',
  16. 'io.FileNotFoundException',
  17. 'io.streams.FileInputStream',
  18. 'io.streams.FileOutputStream'
  19. );
  20. /**
  21. * Instances of the file class serve as an opaque handle to the underlying machine-
  22. * specific structure representing an open file.
  23. *
  24. * @test xp://net.xp_framework.unittest.io.FileTest
  25. * @test xp://net.xp_framework.unittest.io.FileIntegrationTest
  26. * @purpose Represent a file
  27. */
  28. class File extends Stream {
  29. public
  30. $uri= '',
  31. $filename= '',
  32. $path= '',
  33. $extension= '',
  34. $mode= FILE_MODE_READ;
  35. public
  36. $_fd= NULL;
  37. /**
  38. * Constructor. Supports creation via one of the following ways:
  39. * <code>
  40. * // via resource
  41. * $f= new File(fopen('lang/Type.class.php', FILE_MODE_READ));
  42. *
  43. * // via filename
  44. * $f= new File('lang/Type.class.php');
  45. *
  46. * // via dir- and filename
  47. * $f->new File('lang', 'Type.class.php');
  48. *
  49. * // via folder and filename
  50. * $f->new File(new Folder('lang'), 'Type.class.php');
  51. * </code>
  52. *
  53. * @param var base either a resource or a filename
  54. * @param string uri
  55. */
  56. public function __construct($base, $uri= NULL) {
  57. if (is_resource($base)) {
  58. $this->uri= NULL;
  59. $this->_fd= $base;
  60. } else if (NULL === $uri) {
  61. $this->setURI($base);
  62. } else if (is('io.Folder', $base)) {
  63. $this->setURI($base->getURI().$uri);
  64. } else {
  65. $this->setURI(rtrim($base, DIRECTORY_SEPARATOR).DIRECTORY_SEPARATOR.$uri);
  66. }
  67. }
  68. /**
  69. * Retrieve input stream
  70. *
  71. * @return io.streams.InputStream
  72. */
  73. public function getInputStream() {
  74. return new FileInputStream($this);
  75. }
  76. /**
  77. * Retrieve output stream
  78. *
  79. * @return io.streams.OutputStream
  80. */
  81. public function getOutputStream() {
  82. return new FileOutputStream($this);
  83. }
  84. /**
  85. * Retrieve internal file handle
  86. *
  87. * @return resource
  88. */
  89. public function getHandle() {
  90. return $this->_fd;
  91. }
  92. /**
  93. * Returns the URI of the file
  94. *
  95. * @return string uri
  96. */
  97. public function getURI() {
  98. return $this->uri;
  99. }
  100. /**
  101. * Returns the filename of the file
  102. *
  103. * @return string filename
  104. */
  105. public function getFileName() {
  106. return $this->filename;
  107. }
  108. /**
  109. * Get Path
  110. *
  111. * @return string
  112. */
  113. public function getPath() {
  114. return $this->path;
  115. }
  116. /**
  117. * Get Extension
  118. *
  119. * @return string
  120. */
  121. public function getExtension() {
  122. return $this->extension;
  123. }
  124. /**
  125. * Set this file's URI
  126. *
  127. * @param string uri
  128. * @throws lang.IllegalArgumentException in case an invalid file name was given
  129. */
  130. public function setURI($uri) {
  131. static $allowed= array('xar://*', 'php://stderr', 'php://stdout', 'php://stdin', 'res://*');
  132. // Check validity, handle scheme and non-scheme URIs
  133. $uri= (string)$uri;
  134. if (0 === strlen($uri) || FALSE !== strpos($uri, "\0")) {
  135. throw new IllegalArgumentException('Invalid filename "'.addcslashes($uri, "\0..\17").'"');
  136. } else if (FALSE !== ($s= strpos($uri, '://'))) {
  137. if (!in_array($uri, $allowed) && !in_array(substr($uri, 0, 6).'*', $allowed)) {
  138. throw new IllegalArgumentException('Invalid scheme URI "'.$uri.'"');
  139. }
  140. $this->uri= $uri;
  141. $p= max($s+ 3, FALSE === ($pq= strpos($uri, '?', $s)) ? -1 : $pq+ 1);
  142. } else {
  143. $this->uri= realpath($uri);
  144. if ('' == $this->uri && $uri != $this->uri) $this->uri= $uri;
  145. $p= 0;
  146. }
  147. // Split into components. Always allow forward slashes!
  148. $p= max(
  149. $p,
  150. FALSE === ($ps= strrpos($uri, '/', $s)) ? -1 : $ps,
  151. FALSE === ($pd= strrpos($uri, DIRECTORY_SEPARATOR, $s)) ? -1 : $pd
  152. );
  153. $this->path= substr($uri, 0, $p);
  154. $this->filename= ltrim(substr($uri, $p), '/'.DIRECTORY_SEPARATOR);
  155. $this->extension= (FALSE === ($pe= strrpos($this->filename, '.', $s))) ? NULL : substr($this->filename, $pe+ 1);
  156. }
  157. /**
  158. * Open the file
  159. *
  160. * @param string mode one of the FILE_MODE_* constants
  161. * @return bool TRUE if file could be opened
  162. * @throws io.FileNotFoundException in case the file is not found
  163. * @throws io.IOException in case the file cannot be opened (e.g., lacking permissions)
  164. */
  165. public function open($mode= FILE_MODE_READ) {
  166. $this->mode= $mode;
  167. if (
  168. 0 != strncmp('php://', $this->uri, 6) &&
  169. (FILE_MODE_READ == $mode) &&
  170. (!$this->exists())
  171. ) throw new FileNotFoundException('File "'.$this->uri.'" not found');
  172. $this->_fd= fopen($this->uri, $this->mode);
  173. if (!$this->_fd) {
  174. $e= new IOException('Cannot open '.$this->uri.' mode '.$this->mode);
  175. xp::gc(__FILE__);
  176. throw $e;
  177. }
  178. return TRUE;
  179. }
  180. /**
  181. * Returns whether this file is open
  182. *
  183. * @return bool TRUE, if the file is open
  184. */
  185. public function isOpen() {
  186. return is_resource($this->_fd);
  187. }
  188. /**
  189. * Returns whether this file eixtss
  190. *
  191. * @return bool TRUE in case the file exists
  192. */
  193. public function exists() {
  194. return file_exists($this->uri);
  195. }
  196. /**
  197. * Retrieve the file's size in bytes
  198. *
  199. * @return int size filesize in bytes
  200. * @throws io.IOException in case of an error
  201. */
  202. public function size() {
  203. if (FALSE === ($size= filesize($this->uri))) {
  204. $e= new IOException('Cannot get filesize for '.$this->uri);
  205. xp::gc(__FILE__);
  206. throw $e;
  207. }
  208. return $size;
  209. }
  210. /**
  211. * Truncate the file to the specified length
  212. *
  213. * @param bool TRUE if method succeeded
  214. * @throws io.IOException in case of an error
  215. */
  216. public function truncate($size= 0) {
  217. if (FALSE === ($return= ftruncate($this->_fd, $size))) {
  218. $e= new IOException('Cannot truncate file '.$this->uri);
  219. xp::gc(__FILE__);
  220. throw $e;
  221. }
  222. return $return;
  223. }
  224. /**
  225. * Retrieve last access time
  226. *
  227. * Note:
  228. * The atime of a file is supposed to change whenever the data blocks of a file
  229. * are being read. This can be costly performancewise when an application
  230. * regularly accesses a very large number of files or directories. Some Unix
  231. * filesystems can be mounted with atime updates disabled to increase the
  232. * performance of such applications; USENET news spools are a common example.
  233. * On such filesystems this function will be useless.
  234. *
  235. * @return int The date the file was last accessed as a unix-timestamp
  236. * @throws io.IOException in case of an error
  237. */
  238. public function lastAccessed() {
  239. if (FALSE === ($atime= fileatime($this->uri))) {
  240. $e= new IOException('Cannot get atime for '.$this->uri);
  241. xp::gc(__FILE__);
  242. throw $e;
  243. }
  244. return $atime;
  245. }
  246. /**
  247. * Retrieve last modification time
  248. *
  249. * @return int The date the file was last modified as a unix-timestamp
  250. * @throws io.IOException in case of an error
  251. */
  252. public function lastModified() {
  253. if (FALSE === ($mtime= filemtime($this->uri))) {
  254. $e= new IOException('Cannot get mtime for '.$this->uri);
  255. xp::gc(__FILE__);
  256. throw $e;
  257. }
  258. return $mtime;
  259. }
  260. /**
  261. * Set last modification time
  262. *
  263. * @param int time default -1 Unix-timestamp
  264. * @return bool success
  265. * @throws io.IOException in case of an error
  266. */
  267. public function touch($time= -1) {
  268. if (-1 == $time) $time= time();
  269. if (FALSE === touch($this->uri, $time)) {
  270. $e= new IOException('Cannot set mtime for '.$this->uri);
  271. xp::gc(__FILE__);
  272. throw $e;
  273. }
  274. return TRUE;
  275. }
  276. /**
  277. * Retrieve when the file was created
  278. *
  279. * @return int The date the file was created as a unix-timestamp
  280. * @throws io.IOException in case of an error
  281. */
  282. public function createdAt() {
  283. if (FALSE === ($mtime= filectime($this->uri))) {
  284. $e= new IOException('Cannot get mtime for '.$this->uri);
  285. xp::gc(__FILE__);
  286. throw $e;
  287. }
  288. return $mtime;
  289. }
  290. /**
  291. * Read one line and chop off trailing CR and LF characters
  292. *
  293. * Returns a string of up to length - 1 bytes read from the file.
  294. * Reading ends when length - 1 bytes have been read, on a newline (which is
  295. * included in the return value), or on EOF (whichever comes first).
  296. *
  297. * @param int bytes default 4096 Max. amount of bytes to be read
  298. * @return string Data read
  299. * @throws io.IOException in case of an error
  300. */
  301. public function readLine($bytes= 4096) {
  302. $bytes= $this->gets($bytes);
  303. return FALSE === $bytes ? FALSE : chop($bytes);
  304. }
  305. /**
  306. * Read one char
  307. *
  308. * @return string the character read
  309. * @throws io.IOException in case of an error
  310. */
  311. public function readChar() {
  312. if (FALSE === ($result= fgetc($this->_fd)) && !feof($this->_fd)) {
  313. $e= new IOException('Cannot read 1 byte from '.$this->uri);
  314. xp::gc(__FILE__);
  315. throw $e;
  316. }
  317. return $result;
  318. }
  319. /**
  320. * Read a line
  321. *
  322. * This function is identical to readLine except that trailing CR and LF characters
  323. * will be included in its return value
  324. *
  325. * @param int bytes default 4096 Max. amount of bytes to read
  326. * @return string Data read
  327. * @throws io.IOException in case of an error
  328. */
  329. public function gets($bytes= 4096) {
  330. if (0 === $bytes) return '';
  331. if (FALSE === ($result= fgets($this->_fd, $bytes)) && !feof($this->_fd)) {
  332. $e= new IOException('Cannot read '.$bytes.' bytes from '.$this->uri);
  333. xp::gc(__FILE__);
  334. throw $e;
  335. }
  336. return $result;
  337. }
  338. /**
  339. * Read (binary-safe) up to a given amount of bytes
  340. *
  341. * @param int bytes default 4096 Max. amount of bytes to read
  342. * @return string Data read
  343. * @throws io.IOException in case of an error
  344. */
  345. public function read($bytes= 4096) {
  346. if (0 === $bytes) return '';
  347. if (FALSE === ($result= fread($this->_fd, $bytes)) && !feof($this->_fd)) {
  348. $e= new IOException('Cannot read '.$bytes.' bytes from '.$this->uri);
  349. xp::gc(__FILE__);
  350. throw $e;
  351. }
  352. return '' === $result ? FALSE : $result;
  353. }
  354. /**
  355. * Write
  356. *
  357. * @param string string data to write
  358. * @return int number of bytes written
  359. * @throws io.IOException in case of an error
  360. */
  361. public function write($string) {
  362. if (!$this->_fd || FALSE === ($result= fwrite($this->_fd, $string))) {
  363. $e= new IOException('Cannot write '.strlen($string).' bytes to '.$this->uri);
  364. xp::gc(__FILE__);
  365. throw $e;
  366. }
  367. return $result;
  368. }
  369. /**
  370. * Write a line and append a LF (\n) character
  371. *
  372. * @param string string data default '' to write
  373. * @return int number of bytes written
  374. * @throws io.IOException in case of an error
  375. */
  376. public function writeLine($string= '') {
  377. if (!$this->_fd || FALSE === ($result= fwrite($this->_fd, $string."\n"))) {
  378. $e= new IOException('Cannot write '.(strlen($string)+ 1).' bytes to '.$this->uri);
  379. xp::gc(__FILE__);
  380. throw $e;
  381. }
  382. return $result;
  383. }
  384. /**
  385. * Returns whether the file pointer is at the end of the file
  386. *
  387. * Hint:
  388. * Use isOpen() to check if the file is open
  389. *
  390. * @see php://feof
  391. * @return bool TRUE when the end of the file is reached
  392. * @throws io.IOException in case of an error (e.g., the file's not been opened)
  393. */
  394. public function eof() {
  395. $result= feof($this->_fd);
  396. if (xp::errorAt(__FILE__, __LINE__ - 1)) {
  397. $e= new IOException('Cannot determine eof of '.$this->uri);
  398. xp::gc(__FILE__);
  399. throw $e;
  400. }
  401. return $result;
  402. }
  403. /**
  404. * Sets the file position indicator for fp to the beginning of the
  405. * file stream.
  406. *
  407. * This function is identical to a call of $f->seek(0, SEEK_SET)
  408. *
  409. * @return bool TRUE if rewind suceeded
  410. * @throws io.IOException in case of an error
  411. */
  412. public function rewind() {
  413. if (FALSE === ($result= rewind($this->_fd))) {
  414. $e= new IOException('Cannot rewind file pointer');
  415. xp::gc(__FILE__);
  416. throw $e;
  417. }
  418. return TRUE;
  419. }
  420. /**
  421. * Move file pointer to a new position
  422. *
  423. * @param int position default 0 The new position
  424. * @param int mode default SEEK_SET
  425. * @see php://fseek
  426. * @throws io.IOException in case of an error
  427. * @return bool success
  428. */
  429. public function seek($position= 0, $mode= SEEK_SET) {
  430. if (0 != ($result= fseek($this->_fd, $position, $mode))) {
  431. $e= new IOException('Seek error, position '.$position.' in mode '.$mode);
  432. xp::gc(__FILE__);
  433. throw $e;
  434. }
  435. return TRUE;
  436. }
  437. /**
  438. * Retrieve file pointer position
  439. *
  440. * @return int position
  441. * @throws io.IOException in case of an error
  442. */
  443. public function tell() {
  444. if (FALSE === ($result= ftell($this->_fd))) {
  445. $e= new IOException('Cannot retrieve file pointer\'s position');
  446. xp::gc(__FILE__);
  447. throw $e;
  448. }
  449. return $result;
  450. }
  451. /**
  452. * Private wrapper function for locking
  453. *
  454. * Warning:
  455. * flock() will not work on NFS and many other networked file systems. Check your
  456. * operating system documentation for more details. On some operating systems flock()
  457. * is implemented at the process level. When using a multithreaded server API like
  458. * ISAPI you may not be able to rely on flock() to protect files against other PHP
  459. * scripts running in parallel threads of the same server instance! flock() is not
  460. * supported on antiquated filesystems like FAT and its derivates and will therefore
  461. * always return FALSE under this environments (this is especially true for Windows 98
  462. * users).
  463. *
  464. * The optional second argument is set to TRUE if the lock would block (EWOULDBLOCK
  465. * errno condition).
  466. *
  467. * @param int op operation (one of the predefined LOCK_* constants)
  468. * @throws io.IOException in case of an error
  469. * @return bool success
  470. * @see php://flock
  471. */
  472. protected function _lock($mode) {
  473. if (FALSE === flock($this->_fd, $mode)) {
  474. $os= '';
  475. foreach (array(
  476. LOCK_NB => 'LOCK_NB',
  477. LOCK_UN => 'LOCK_UN',
  478. LOCK_EX => 'LOCK_EX',
  479. LOCK_SH => 'LOCK_SH'
  480. ) as $o => $s) {
  481. if ($mode >= $o) {
  482. $os.= ' | '.$s;
  483. $mode-= $o;
  484. }
  485. }
  486. $e= new IOException('Cannot lock file '.$this->uri.' w/ '.substr($os, 3));
  487. xp::gc(__FILE__);
  488. throw $e;
  489. }
  490. return TRUE;
  491. }
  492. /**
  493. * Acquire a shared lock (reader)
  494. *
  495. * @param bool block default FALSE
  496. * @see xp://io.File#_lock
  497. * @return bool success
  498. */
  499. public function lockShared($block= FALSE) {
  500. return $this->_lock(LOCK_SH + ($block ? 0 : LOCK_NB));
  501. }
  502. /**
  503. * Acquire an exclusive lock (writer)
  504. *
  505. * @param bool block default FALSE
  506. * @see xp://io.File#_lock
  507. * @return bool success
  508. */
  509. public function lockExclusive($block= FALSE) {
  510. return $this->_lock(LOCK_EX + ($block ? 0 : LOCK_NB));
  511. }
  512. /**
  513. * Release a lock (shared or exclusive)
  514. *
  515. * @see xp://io.File#_lock
  516. * @return bool success
  517. */
  518. public function unLock() {
  519. return $this->_lock(LOCK_UN);
  520. }
  521. /**
  522. * Close this file
  523. *
  524. * @return bool success
  525. * @throws io.IOException if close fails
  526. */
  527. public function close() {
  528. if (!is_resource($this->_fd)) {
  529. throw new IOException('Cannot close non-opened file '.$this->uri);
  530. }
  531. if (FALSE === fclose($this->_fd)) {
  532. $e= new IOException('Cannot close file '.$this->uri);
  533. xp::gc(__FILE__);
  534. throw $e;
  535. }
  536. $this->_fd= NULL;
  537. return TRUE;
  538. }
  539. /**
  540. * Delete this file
  541. *
  542. * Warning: Open files cannot be deleted. Use the close() method to
  543. * close the file first
  544. *
  545. * @return bool success
  546. * @throws io.IOException in case of an error (e.g., lack of permissions)
  547. * @throws lang.IllegalStateException in case the file is still open
  548. */
  549. public function unlink() {
  550. if (is_resource($this->_fd)) {
  551. throw new IllegalStateException('File still open');
  552. }
  553. if (FALSE === unlink($this->uri)) {
  554. $e= new IOException('Cannot delete file '.$this->uri);
  555. xp::gc(__FILE__);
  556. throw $e;
  557. }
  558. return TRUE;
  559. }
  560. /**
  561. * Move this file
  562. *
  563. * Warning: Open files cannot be moved. Use the close() method to
  564. * close the file first
  565. *
  566. * @param var target where to move the file to, either a string, a File or a Folder
  567. * @return bool success
  568. * @throws io.IOException in case of an error (e.g., lack of permissions)
  569. * @throws lang.IllegalStateException in case the file is still open
  570. */
  571. public function move($target) {
  572. if (is_resource($this->_fd)) {
  573. throw new IllegalStateException('File still open');
  574. }
  575. if ($target instanceof self) {
  576. $uri= $target->getURI();
  577. } else if ($target instanceof Folder) {
  578. $uri= $target->getURI().$this->getFilename();
  579. } else {
  580. $uri= $target;
  581. }
  582. if (FALSE === rename($this->uri, $uri)) {
  583. $e= new IOException('Cannot move file '.$this->uri.' to '.$uri);
  584. xp::gc(__FILE__);
  585. throw $e;
  586. }
  587. $this->setURI($uri);
  588. return TRUE;
  589. }
  590. /**
  591. * Copy this file
  592. *
  593. * Warning: Open files cannot be copied. Use the close() method to
  594. * close the file first
  595. *
  596. * @param var target where to copy the file to, either a string, a File or a Folder
  597. * @return bool success
  598. * @throws io.IOException in case of an error (e.g., lack of permissions)
  599. * @throws lang.IllegalStateException in case the file is still open
  600. */
  601. public function copy($target) {
  602. if (is_resource($this->_fd)) {
  603. throw new IllegalStateException('File still open');
  604. }
  605. if ($target instanceof self) {
  606. $uri= $target->getURI();
  607. } else if ($target instanceof Folder) {
  608. $uri= $target->getURI().$this->getFilename();
  609. } else {
  610. $uri= $target;
  611. }
  612. if (FALSE === copy($this->uri, $uri)) {
  613. $e= new IOException('Cannot copy file '.$this->uri.' to '.$uri);
  614. xp::gc(__FILE__);
  615. throw $e;
  616. }
  617. return TRUE;
  618. }
  619. /**
  620. * Change permissions for the file
  621. *
  622. * @see php://chmod
  623. * @param var mode
  624. * @return bool success
  625. */
  626. public function setPermissions($mode) {
  627. return chmod($this->uri, $mode);
  628. }
  629. /**
  630. * Get permission mask of the file
  631. *
  632. * @see php://stat
  633. * @return int
  634. */
  635. public function getPermissions() {
  636. $stat= stat($this->uri);
  637. return $stat['mode'];
  638. }
  639. /**
  640. * Returns whether a given value is equal to this file
  641. *
  642. * @param var cmp
  643. * @return bool
  644. */
  645. public function equals($cmp) {
  646. return $cmp instanceof self && $cmp->hashCode() === $this->hashCode();
  647. }
  648. /**
  649. * Returns a hashcode
  650. *
  651. * @return string
  652. */
  653. public function hashCode() {
  654. return NULL === $this->uri ? (string)$this->_fd : md5($this->uri);
  655. }
  656. /**
  657. * Returns a string representation of this object
  658. *
  659. * @return string
  660. */
  661. public function toString() {
  662. return sprintf(
  663. '%s(uri= %s, mode= %s)',
  664. $this->getClassName(),
  665. $this->uri,
  666. $this->mode
  667. );
  668. }
  669. }
  670. ?>