1: <?php
2:
3: /**
4: * This file is part of the Nette Framework.
5: *
6: * Copyright (c) 2004, 2010 David Grudl (http://davidgrudl.com)
7: *
8: * This source file is subject to the "Nette license", and/or
9: * GPL license. For more information please see http://nette.org
10: */
11:
12: namespace Nette;
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: fclose($this->handle);
170:
171: // are we working with temporary file?
172: if ($this->tempFile) {
173: // try to rename temp file, otherwise delete temp file
174: if (!@rename($this->tempFile, $this->filePath)) { // intentionally @
175: unlink($this->tempFile);
176: }
177: }
178: }
179:
180:
181:
182: /**
183: * Reads up to length bytes from the file.
184: * @param int length
185: * @return string
186: */
187: public function stream_read($length)
188: {
189: return fread($this->handle, $length);
190: }
191:
192:
193:
194: /**
195: * Writes the string to the file.
196: * @param string data to write
197: * @return int number of bytes that were successfully stored
198: */
199: public function stream_write($data)
200: {
201: $len = strlen($data);
202: $res = fwrite($this->handle, $data, $len);
203:
204: if ($res !== $len) { // disk full?
205: $this->writeError = TRUE;
206: }
207:
208: return $res;
209: }
210:
211:
212:
213: /**
214: * Returns the position of the file.
215: * @return int
216: */
217: public function stream_tell()
218: {
219: return ftell($this->handle);
220: }
221:
222:
223:
224: /**
225: * Returns TRUE if the file pointer is at end-of-file.
226: * @return bool
227: */
228: public function stream_eof()
229: {
230: return feof($this->handle);
231: }
232:
233:
234:
235: /**
236: * Sets the file position indicator for the file.
237: * @param int position
238: * @param int see fseek()
239: * @return int Return TRUE on success
240: */
241: public function stream_seek($offset, $whence)
242: {
243: return fseek($this->handle, $offset, $whence) === 0; // ???
244: }
245:
246:
247:
248: /**
249: * Gets information about a file referenced by $this->handle.
250: * @return array
251: */
252: public function stream_stat()
253: {
254: return fstat($this->handle);
255: }
256:
257:
258:
259: /**
260: * Gets information about a file referenced by filename.
261: * @param string file name
262: * @param int STREAM_URL_STAT_LINK, STREAM_URL_STAT_QUIET
263: * @return array
264: */
265: public function url_stat($path, $flags)
266: {
267: // This is not thread safe
268: $path = substr($path, strlen(self::PROTOCOL)+3);
269: return ($flags & STREAM_URL_STAT_LINK) ? @lstat($path) : @stat($path); // intentionally @
270: }
271:
272:
273:
274: /**
275: * Deletes a file.
276: * On Windows unlink is not allowed till file is opened
277: * @param string file name with stream protocol
278: * @return bool TRUE on success or FALSE on failure
279: */
280: public function unlink($path)
281: {
282: $path = substr($path, strlen(self::PROTOCOL)+3);
283: return unlink($path);
284: }
285:
286: }
287: