1: <?php
2:
3: 4: 5: 6: 7: 8: 9: 10: 11:
12:
13:
14:
15: 16: 17: 18: 19:
20: class FileStorage extends Object implements ICacheStorage
21: {
22: 23: 24: 25: 26: 27: 28: 29: 30: 31:
32:
33:
34: const META_HEADER_LEN = 28, 35: 36: META_TIME = 'time', 37: META_SERIALIZED = 'serialized', 38: META_EXPIRE = 'expire', 39: META_DELTA = 'delta', 40: META_ITEMS = 'di', 41: META_CALLBACKS = 'callbacks'; 42:
43:
44: const FILE = 'file',
45: HANDLE = 'handle';
46:
47:
48:
49: public static $gcProbability = 0.001;
50:
51:
52: public static $useDirectories;
53:
54:
55: private $dir;
56:
57:
58: private $useDirs;
59:
60:
61: private $journal;
62:
63:
64:
65: public function __construct($dir, ICacheJournal $journal = NULL)
66: {
67: $this->dir = realpath($dir);
68: if ($this->dir === FALSE) {
69: throw new DirectoryNotFoundException("Directory '$dir' not found.");
70: }
71:
72: if (self::$useDirectories === NULL) {
73: 74: $uniq = uniqid('_', TRUE);
75: umask(0000);
76: if (!@mkdir("$dir/$uniq", 0777)) { 77: throw new InvalidStateException("Unable to write to directory '$dir'. Make this directory writable.");
78: }
79:
80: 81: self::$useDirectories = !ini_get('safe_mode');
82: if (!self::$useDirectories && @file_put_contents("$dir/$uniq/_", '') !== FALSE) { 83: self::$useDirectories = TRUE;
84: unlink("$dir/$uniq/_");
85: }
86: @rmdir("$dir/$uniq"); 87: }
88:
89: $this->useDirs = (bool) self::$useDirectories;
90: $this->journal = $journal;
91:
92: if (mt_rand() / mt_getrandmax() < self::$gcProbability) {
93: $this->clean(array());
94: }
95: }
96:
97:
98:
99: 100: 101: 102: 103:
104: public function read($key)
105: {
106: $meta = $this->readMetaAndLock($this->getCacheFile($key), LOCK_SH);
107: if ($meta && $this->verify($meta)) {
108: return $this->readData($meta); 109:
110: } else {
111: return NULL;
112: }
113: }
114:
115:
116:
117: 118: 119: 120: 121:
122: private function verify($meta)
123: {
124: do {
125: if (!empty($meta[self::META_DELTA])) {
126: 127: if (filemtime($meta[self::FILE]) + $meta[self::META_DELTA] < time()) break;
128: touch($meta[self::FILE]);
129:
130: } elseif (!empty($meta[self::META_EXPIRE]) && $meta[self::META_EXPIRE] < time()) {
131: break;
132: }
133:
134: if (!empty($meta[self::META_CALLBACKS]) && !Cache::checkCallbacks($meta[self::META_CALLBACKS])) {
135: break;
136: }
137:
138: if (!empty($meta[self::META_ITEMS])) {
139: foreach ($meta[self::META_ITEMS] as $depFile => $time) {
140: $m = $this->readMetaAndLock($depFile, LOCK_SH);
141: if ($m[self::META_TIME] !== $time) break 2;
142: if ($m && !$this->verify($m)) break 2;
143: }
144: }
145:
146: return TRUE;
147: } while (FALSE);
148:
149: $this->delete($meta[self::FILE], $meta[self::HANDLE]); 150: return FALSE;
151: }
152:
153:
154:
155: 156: 157: 158: 159: 160: 161:
162: public function write($key, $data, array $dp)
163: {
164: $meta = array(
165: self::META_TIME => microtime(),
166: );
167:
168: if (isset($dp[Cache::EXPIRATION])) {
169: if (empty($dp[Cache::SLIDING])) {
170: $meta[self::META_EXPIRE] = $dp[Cache::EXPIRATION] + time(); 171: } else {
172: $meta[self::META_DELTA] = (int) $dp[Cache::EXPIRATION]; 173: }
174: }
175:
176: if (isset($dp[Cache::ITEMS])) {
177: foreach ((array) $dp[Cache::ITEMS] as $item) {
178: $depFile = $this->getCacheFile($item);
179: $m = $this->readMetaAndLock($depFile, LOCK_SH);
180: $meta[self::META_ITEMS][$depFile] = $m[self::META_TIME];
181: unset($m);
182: }
183: }
184:
185: if (isset($dp[Cache::CALLBACKS])) {
186: $meta[self::META_CALLBACKS] = $dp[Cache::CALLBACKS];
187: }
188:
189: $cacheFile = $this->getCacheFile($key);
190: if ($this->useDirs && !is_dir($dir = dirname($cacheFile))) {
191: umask(0000);
192: if (!mkdir($dir, 0777)) {
193: return;
194: }
195: }
196: $handle = @fopen($cacheFile, 'r+b'); 197: if (!$handle) {
198: $handle = fopen($cacheFile, 'wb');
199: if (!$handle) {
200: return;
201: }
202: }
203:
204: if (isset($dp[Cache::TAGS]) || isset($dp[Cache::PRIORITY])) {
205: if (!$this->journal) {
206: throw new InvalidStateException('CacheJournal has not been provided.');
207: }
208: $this->journal->write($cacheFile, $dp);
209: }
210:
211: flock($handle, LOCK_EX);
212: ftruncate($handle, 0);
213:
214: if (!is_string($data)) {
215: $data = serialize($data);
216: $meta[self::META_SERIALIZED] = TRUE;
217: }
218:
219: $head = serialize($meta) . '?>';
220: $head = '<?php //netteCache[01]' . str_pad((string) strlen($head), 6, '0', STR_PAD_LEFT) . $head;
221: $headLen = strlen($head);
222: $dataLen = strlen($data);
223:
224: do {
225: if (fwrite($handle, str_repeat("\x00", $headLen), $headLen) !== $headLen) {
226: break;
227: }
228:
229: if (fwrite($handle, $data, $dataLen) !== $dataLen) {
230: break;
231: }
232:
233: fseek($handle, 0);
234: if (fwrite($handle, $head, $headLen) !== $headLen) {
235: break;
236: }
237:
238: flock($handle, LOCK_UN);
239: fclose($handle);
240: return TRUE;
241: } while (FALSE);
242:
243: $this->delete($cacheFile, $handle);
244: }
245:
246:
247:
248: 249: 250: 251: 252:
253: public function remove($key)
254: {
255: $this->delete($this->getCacheFile($key));
256: }
257:
258:
259:
260: 261: 262: 263: 264:
265: public function clean(array $conds)
266: {
267: $all = !empty($conds[Cache::ALL]);
268: $collector = empty($conds);
269:
270: 271: if ($all || $collector) {
272: $now = time();
273: foreach (Finder::find('*')->from($this->dir)->childFirst() as $entry) {
274: $path = (string) $entry;
275: if ($entry->isDir()) { 276: @rmdir($path); 277: continue;
278: }
279: if ($all) {
280: $this->delete($path);
281:
282: } else { 283: $meta = $this->readMetaAndLock($path, LOCK_SH);
284: if (!$meta) continue;
285:
286: if (!empty($meta[self::META_EXPIRE]) && $meta[self::META_EXPIRE] < $now) {
287: $this->delete($path, $meta[self::HANDLE]);
288: continue;
289: }
290:
291: flock($meta[self::HANDLE], LOCK_UN);
292: fclose($meta[self::HANDLE]);
293: }
294: }
295:
296: if ($this->journal) {
297: $this->journal->clean($conds);
298: }
299: return;
300: }
301:
302: 303: if ($this->journal) {
304: foreach ($this->journal->clean($conds) as $file) {
305: $this->delete($file);
306: }
307: }
308: }
309:
310:
311:
312: 313: 314: 315: 316: 317:
318: protected function readMetaAndLock($file, $lock)
319: {
320: $handle = @fopen($file, 'r+b'); 321: if (!$handle) return NULL;
322:
323: flock($handle, $lock);
324:
325: $head = stream_get_contents($handle, self::META_HEADER_LEN);
326: if ($head && strlen($head) === self::META_HEADER_LEN) {
327: $size = (int) substr($head, -6);
328: $meta = stream_get_contents($handle, $size, self::META_HEADER_LEN);
329: $meta = @unserialize($meta); 330: if (is_array($meta)) {
331: fseek($handle, $size + self::META_HEADER_LEN); 332: $meta[self::FILE] = $file;
333: $meta[self::HANDLE] = $handle;
334: return $meta;
335: }
336: }
337:
338: flock($handle, LOCK_UN);
339: fclose($handle);
340: return NULL;
341: }
342:
343:
344:
345: 346: 347: 348: 349:
350: protected function readData($meta)
351: {
352: $data = stream_get_contents($meta[self::HANDLE]);
353: flock($meta[self::HANDLE], LOCK_UN);
354: fclose($meta[self::HANDLE]);
355:
356: if (empty($meta[self::META_SERIALIZED])) {
357: return $data;
358: } else {
359: return @unserialize($data); 360: }
361: }
362:
363:
364:
365: 366: 367: 368: 369:
370: protected function getCacheFile($key)
371: {
372: if ($this->useDirs) {
373: return $this->dir . '/_' . str_replace('%00', '/_', urlencode($key)); 374: } else {
375: return $this->dir . '/_' . urlencode($key);
376: }
377: }
378:
379:
380:
381: 382: 383: 384: 385: 386:
387: private static function delete($file, $handle = NULL)
388: {
389: if (@unlink($file)) { 390: if ($handle) {
391: flock($handle, LOCK_UN);
392: fclose($handle);
393: }
394: return;
395: }
396:
397: if (!$handle) {
398: $handle = @fopen($file, 'r+'); 399: }
400: if ($handle) {
401: flock($handle, LOCK_EX);
402: ftruncate($handle, 0);
403: flock($handle, LOCK_UN);
404: fclose($handle);
405: @unlink($file); 406: }
407: }
408:
409: }
410: