1: <?php
2:
3: /**
4: * This file is part of the Nette Framework (https://nette.org)
5: *
6: * Copyright (c) 2004 David Grudl (http://davidgrudl.com)
7: *
8: * For the full copyright and license information, please view
9: * the file license.txt that was distributed with this source code.
10: * @package Nette\IO
11: */
12:
13:
14:
15: /**
16: * Thread safe / atomic file manipulation. Stream safe://
17: *
18: * <code>
19: * file_put_contents('safe://myfile.txt', $content);
20: *
21: * $content = file_get_contents('safe://myfile.txt');
22: *
23: * unlink('safe://myfile.txt');
24: * </code>
25: *
26: * @author David Grudl
27: * @package Nette\IO
28: */
29: final class NSafeStream
30: {
31: /**
32: * Name of stream protocol - safe://
33: */
34: const PROTOCOL = 'safe';
35:
36: /**
37: * Current file handle.
38: */
39: private $handle;
40:
41: /**
42: * Renaming of temporary file.
43: */
44: private $filePath;
45: private $tempFile;
46:
47: /**
48: * Starting position in file (for appending).
49: */
50: private $startPos = 0;
51:
52: /**
53: * Write-error detected?
54: */
55: private $writeError = FALSE;
56:
57:
58:
59: /**
60: * Registers protocol 'safe://'.
61: * @return bool
62: */
63: public static function register()
64: {
65: return stream_wrapper_register(self::PROTOCOL, __CLASS__);
66: }
67:
68:
69:
70: /**
71: * Opens file.
72: * @param string file name with stream protocol
73: * @param string mode - see fopen()
74: * @param int STREAM_USE_PATH, STREAM_REPORT_ERRORS
75: * @param string full path
76: * @return bool TRUE on success or FALSE on failure
77: */
78: public function stream_open($path, $mode, $options, &$opened_path)
79: {
80: $path = substr($path, strlen(self::PROTOCOL)+3); // trim protocol safe://
81:
82: $flag = trim($mode, 'rwax+'); // text | binary mode
83: $mode = trim($mode, 'tb'); // mode
84: $use_path = (bool) (STREAM_USE_PATH & $options); // use include_path?
85:
86: $append = FALSE;
87:
88: switch ($mode) {
89: case 'r':
90: case 'r+':
91: // enter critical section: open and lock EXISTING file for reading/writing
92: $handle = @fopen($path, $mode.$flag, $use_path); // intentionally @
93: if (!$handle) return FALSE;
94: if (flock($handle, $mode == 'r' ? LOCK_SH : LOCK_EX)) {
95: $this->handle = $handle;
96: return TRUE;
97: }
98: fclose($handle);
99: return FALSE;
100:
101: case 'a':
102: case 'a+': $append = TRUE;
103: case 'w':
104: case 'w+':
105: // try enter critical section: open and lock EXISTING file for rewriting
106: $handle = @fopen($path, 'r+'.$flag, $use_path); // intentionally @
107:
108: if ($handle) {
109: if (flock($handle, LOCK_EX)) {
110: if ($append) {
111: fseek($handle, 0, SEEK_END);
112: $this->startPos = ftell($handle);
113: } else {
114: ftruncate($handle, 0);
115: }
116: $this->handle = $handle;
117: return TRUE;
118: }
119: fclose($handle);
120: }
121: // file doesn't exists, continue...
122: $mode{0} = 'x'; // x || x+
123:
124: case 'x':
125: case 'x+':
126: if (file_exists($path)) return FALSE;
127:
128: // create temporary file in the same directory
129: $tmp = '~~' . time() . '.tmp';
130:
131: // enter critical section: create temporary file
132: $handle = @fopen($path . $tmp, $mode . $flag, $use_path); // intentionally @
133: if ($handle) {
134: if (flock($handle, LOCK_EX)) {
135: $this->handle = $handle;
136: if (!@rename($path . $tmp, $path)) { // intentionally @
137: // rename later - for windows
138: $this->tempFile = realpath($path . $tmp);
139: $this->filePath = substr($this->tempFile, 0, -strlen($tmp));
140: }
141: return TRUE;
142: }
143: fclose($handle);
144: unlink($path . $tmp);
145: }
146: return FALSE;
147:
148: default:
149: trigger_error("Unsupported mode $mode", E_USER_WARNING);
150: return FALSE;
151: } // switch
152:
153: } // stream_open
154:
155:
156:
157: /**
158: * Closes file.
159: * @return void
160: */
161: public function stream_close()
162: {
163: if ($this->writeError) {
164: ftruncate($this->handle, $this->startPos);
165: }
166:
167: flock($this->handle, LOCK_UN);
168: fclose($this->handle);
169:
170: // are we working with temporary file?
171: if ($this->tempFile) {
172: // try to rename temp file, otherwise delete temp file
173: if (!@rename($this->tempFile, $this->filePath)) { // intentionally @
174: unlink($this->tempFile);
175: }
176: }
177: }
178:
179:
180:
181: /**
182: * Reads up to length bytes from the file.
183: * @param int length
184: * @return string
185: */
186: public function stream_read($length)
187: {
188: return fread($this->handle, $length);
189: }
190:
191:
192:
193: /**
194: * Writes the string to the file.
195: * @param string data to write
196: * @return int number of bytes that were successfully stored
197: */
198: public function stream_write($data)
199: {
200: $len = strlen($data);
201: $res = fwrite($this->handle, $data, $len);
202:
203: if ($res !== $len) { // disk full?
204: $this->writeError = TRUE;
205: }
206:
207: return $res;
208: }
209:
210:
211:
212: /**
213: * Returns the position of the file.
214: * @return int
215: */
216: public function stream_tell()
217: {
218: return ftell($this->handle);
219: }
220:
221:
222:
223: /**
224: * Returns TRUE if the file pointer is at end-of-file.
225: * @return bool
226: */
227: public function stream_eof()
228: {
229: return feof($this->handle);
230: }
231:
232:
233:
234: /**
235: * Sets the file position indicator for the file.
236: * @param int position
237: * @param int see fseek()
238: * @return int Return TRUE on success
239: */
240: public function stream_seek($offset, $whence)
241: {
242: return fseek($this->handle, $offset, $whence) === 0; // ???
243: }
244:
245:
246:
247: /**
248: * Gets information about a file referenced by $this->handle.
249: * @return array
250: */
251: public function stream_stat()
252: {
253: return fstat($this->handle);
254: }
255:
256:
257:
258: /**
259: * Gets information about a file referenced by filename.
260: * @param string file name
261: * @param int STREAM_URL_STAT_LINK, STREAM_URL_STAT_QUIET
262: * @return array
263: */
264: public function url_stat($path, $flags)
265: {
266: // This is not thread safe
267: $path = substr($path, strlen(self::PROTOCOL)+3);
268: return ($flags & STREAM_URL_STAT_LINK) ? @lstat($path) : @stat($path); // intentionally @
269: }
270:
271:
272:
273: /**
274: * Deletes a file.
275: * On Windows unlink is not allowed till file is opened
276: * @param string file name with stream protocol
277: * @return bool TRUE on success or FALSE on failure
278: */
279: public function unlink($path)
280: {
281: $path = substr($path, strlen(self::PROTOCOL)+3);
282: return unlink($path);
283: }
284:
285: }
286: