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