/Nette/Utils/SafeStream.php

https://github.com/langpavel/nette · PHP · 309 lines · 144 code · 69 blank · 96 comment · 19 complexity · 7478ebb520f6d01c63b3f85cdf3d71ff MD5 · raw file

  1. <?php
  2. /**
  3. * This file is part of the Nette Framework (http://nette.org)
  4. *
  5. * Copyright (c) 2004, 2011 David Grudl (http://davidgrudl.com)
  6. *
  7. * For the full copyright and license information, please view
  8. * the file license.txt that was distributed with this source code.
  9. */
  10. namespace Nette\Utils;
  11. use Nette;
  12. /**
  13. * Provides atomicity and isolation for thread safe file manipulation using stream safe://
  14. *
  15. * <code>
  16. * file_put_contents('safe://myfile.txt', $content);
  17. *
  18. * $content = file_get_contents('safe://myfile.txt');
  19. *
  20. * unlink('safe://myfile.txt');
  21. * </code>
  22. *
  23. * @author David Grudl
  24. * @internal
  25. */
  26. final class SafeStream
  27. {
  28. /** Name of stream protocol - safe:// */
  29. const PROTOCOL = 'safe';
  30. /** @var resource orignal file handle */
  31. private $handle;
  32. /** @var resource temporary file handle */
  33. private $tempHandle;
  34. /** @var string orignal file path */
  35. private $file;
  36. /** @var string temporary file path */
  37. private $tempFile;
  38. /** @var bool */
  39. private $deleteFile;
  40. /** @var bool error detected? */
  41. private $writeError = FALSE;
  42. /**
  43. * Registers protocol 'safe://'.
  44. * @return bool
  45. */
  46. public static function register()
  47. {
  48. return stream_wrapper_register(self::PROTOCOL, __CLASS__);
  49. }
  50. /**
  51. * Opens file.
  52. * @param string file name with stream protocol
  53. * @param string mode - see fopen()
  54. * @param int STREAM_USE_PATH, STREAM_REPORT_ERRORS
  55. * @param string full path
  56. * @return bool TRUE on success or FALSE on failure
  57. */
  58. public function stream_open($path, $mode, $options, &$opened_path)
  59. {
  60. $path = substr($path, strlen(self::PROTOCOL)+3); // trim protocol safe://
  61. $flag = trim($mode, 'rwax+'); // text | binary mode
  62. $mode = trim($mode, 'tb'); // mode
  63. $use_path = (bool) (STREAM_USE_PATH & $options); // use include_path?
  64. // open file
  65. if ($mode === 'r') { // provides only isolation
  66. return $this->checkAndLock($this->tempHandle = fopen($path, 'r'.$flag, $use_path), LOCK_SH);
  67. } elseif ($mode === 'r+') {
  68. if (!$this->checkAndLock($this->handle = fopen($path, 'r'.$flag, $use_path), LOCK_EX)) {
  69. return FALSE;
  70. }
  71. } elseif ($mode[0] === 'x') {
  72. if (!$this->checkAndLock($this->handle = fopen($path, 'x'.$flag, $use_path), LOCK_EX)) {
  73. return FALSE;
  74. }
  75. $this->deleteFile = TRUE;
  76. } elseif ($mode[0] === 'w' || $mode[0] === 'a') {
  77. if ($this->checkAndLock($this->handle = @fopen($path, 'x'.$flag, $use_path), LOCK_EX)) { // intentionally @
  78. $this->deleteFile = TRUE;
  79. } elseif (!$this->checkAndLock($this->handle = fopen($path, 'a+'.$flag, $use_path), LOCK_EX)) {
  80. return FALSE;
  81. }
  82. } else {
  83. trigger_error("Unknown mode $mode", E_USER_WARNING);
  84. return FALSE;
  85. }
  86. // create temporary file in the same directory to provide atomicity
  87. $tmp = '~~' . lcg_value() . '.tmp';
  88. if (!$this->tempHandle = fopen($path . $tmp, (strpos($mode, '+') ? 'x+' : 'x').$flag, $use_path)) {
  89. $this->clean();
  90. return FALSE;
  91. }
  92. $this->tempFile = realpath($path . $tmp);
  93. $this->file = substr($this->tempFile, 0, -strlen($tmp));
  94. // copy to temporary file
  95. if ($mode === 'r+' || $mode[0] === 'a') {
  96. $stat = fstat($this->handle);
  97. fseek($this->handle, 0);
  98. if (stream_copy_to_stream($this->handle, $this->tempHandle) !== $stat['size']) {
  99. $this->clean();
  100. return FALSE;
  101. }
  102. if ($mode[0] === 'a') { // emulate append mode
  103. fseek($this->tempHandle, 0, SEEK_END);
  104. }
  105. }
  106. return TRUE;
  107. }
  108. /**
  109. * Checks handle and locks file.
  110. * @return bool
  111. */
  112. private function checkAndLock($handle, $lock)
  113. {
  114. if (!$handle) {
  115. return FALSE;
  116. } elseif (!flock($handle, $lock)) {
  117. fclose($handle);
  118. return FALSE;
  119. }
  120. return TRUE;
  121. }
  122. /**
  123. * Error destructor.
  124. */
  125. private function clean()
  126. {
  127. flock($this->handle, LOCK_UN);
  128. fclose($this->handle);
  129. if ($this->deleteFile) {
  130. unlink($this->file);
  131. }
  132. if ($this->tempHandle) {
  133. fclose($this->tempHandle);
  134. unlink($this->tempFile);
  135. }
  136. }
  137. /**
  138. * Closes file.
  139. * @return void
  140. */
  141. public function stream_close()
  142. {
  143. if (!$this->tempFile) { // 'r' mode
  144. flock($this->tempHandle, LOCK_UN);
  145. fclose($this->tempHandle);
  146. return;
  147. }
  148. flock($this->handle, LOCK_UN);
  149. fclose($this->handle);
  150. fclose($this->tempHandle);
  151. if ($this->writeError /*5.2*|| !(substr(PHP_OS, 0, 3) === 'WIN' ? unlink($this->file) : TRUE)*/
  152. || !rename($this->tempFile, $this->file) // try to rename temp file
  153. ) {
  154. unlink($this->tempFile); // otherwise delete temp file
  155. if ($this->deleteFile) {
  156. unlink($this->file);
  157. }
  158. }
  159. }
  160. /**
  161. * Reads up to length bytes from the file.
  162. * @param int length
  163. * @return string
  164. */
  165. public function stream_read($length)
  166. {
  167. return fread($this->tempHandle, $length);
  168. }
  169. /**
  170. * Writes the string to the file.
  171. * @param string data to write
  172. * @return int number of bytes that were successfully stored
  173. */
  174. public function stream_write($data)
  175. {
  176. $len = strlen($data);
  177. $res = fwrite($this->tempHandle, $data, $len);
  178. if ($res !== $len) { // disk full?
  179. $this->writeError = TRUE;
  180. }
  181. return $res;
  182. }
  183. /**
  184. * Returns the position of the file.
  185. * @return int
  186. */
  187. public function stream_tell()
  188. {
  189. return ftell($this->tempHandle);
  190. }
  191. /**
  192. * Returns TRUE if the file pointer is at end-of-file.
  193. * @return bool
  194. */
  195. public function stream_eof()
  196. {
  197. return feof($this->tempHandle);
  198. }
  199. /**
  200. * Sets the file position indicator for the file.
  201. * @param int position
  202. * @param int see fseek()
  203. * @return int Return TRUE on success
  204. */
  205. public function stream_seek($offset, $whence)
  206. {
  207. return fseek($this->tempHandle, $offset, $whence) === 0; // ???
  208. }
  209. /**
  210. * Gets information about a file referenced by $this->tempHandle.
  211. * @return array
  212. */
  213. public function stream_stat()
  214. {
  215. return fstat($this->tempHandle);
  216. }
  217. /**
  218. * Gets information about a file referenced by filename.
  219. * @param string file name
  220. * @param int STREAM_URL_STAT_LINK, STREAM_URL_STAT_QUIET
  221. * @return array
  222. */
  223. public function url_stat($path, $flags)
  224. {
  225. // This is not thread safe
  226. $path = substr($path, strlen(self::PROTOCOL)+3);
  227. return ($flags & STREAM_URL_STAT_LINK) ? @lstat($path) : @stat($path); // intentionally @
  228. }
  229. /**
  230. * Deletes a file.
  231. * On Windows unlink is not allowed till file is opened
  232. * @param string file name with stream protocol
  233. * @return bool TRUE on success or FALSE on failure
  234. */
  235. public function unlink($path)
  236. {
  237. $path = substr($path, strlen(self::PROTOCOL)+3);
  238. return unlink($path);
  239. }
  240. }