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