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