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 META_HEADER_LEN = 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: if (!mkdir($dir, 0777)) {
155: return;
156: }
157: }
158: $handle = @fopen($cacheFile, 'r+b');
159: if (!$handle) {
160: $handle = fopen($cacheFile, 'wb');
161: if (!$handle) {
162: return;
163: }
164: }
165:
166: $this->locks[$key] = $handle;
167: flock($handle, LOCK_EX);
168: }
169:
170:
171:
172: 173: 174: 175: 176: 177: 178:
179: public function write($key, $data, array $dp)
180: {
181: $meta = array(
182: self::META_TIME => microtime(),
183: );
184:
185: if (isset($dp[NCache::EXPIRATION])) {
186: if (empty($dp[NCache::SLIDING])) {
187: $meta[self::META_EXPIRE] = $dp[NCache::EXPIRATION] + time();
188: } else {
189: $meta[self::META_DELTA] = (int) $dp[NCache::EXPIRATION];
190: }
191: }
192:
193: if (isset($dp[NCache::ITEMS])) {
194: foreach ((array) $dp[NCache::ITEMS] as $item) {
195: $depFile = $this->getCacheFile($item);
196: $m = $this->readMetaAndLock($depFile, LOCK_SH);
197: $meta[self::META_ITEMS][$depFile] = $m[self::META_TIME];
198: unset($m);
199: }
200: }
201:
202: if (isset($dp[NCache::CALLBACKS])) {
203: $meta[self::META_CALLBACKS] = $dp[NCache::CALLBACKS];
204: }
205:
206: if (!isset($this->locks[$key])) {
207: $this->lock($key);
208: if (!isset($this->locks[$key])) {
209: return;
210: }
211: }
212: $handle = $this->locks[$key];
213: unset($this->locks[$key]);
214:
215: $cacheFile = $this->getCacheFile($key);
216:
217: if (isset($dp[NCache::TAGS]) || isset($dp[NCache::PRIORITY])) {
218: if (!$this->journal) {
219: throw new InvalidStateException('CacheJournal has not been provided.');
220: }
221: $this->journal->write($cacheFile, $dp);
222: }
223:
224: ftruncate($handle, 0);
225:
226: if (!is_string($data)) {
227: $data = serialize($data);
228: $meta[self::META_SERIALIZED] = TRUE;
229: }
230:
231: $head = serialize($meta) . '?>';
232: $head = '<?php //netteCache[01]' . str_pad((string) strlen($head), 6, '0', STR_PAD_LEFT) . $head;
233: $headLen = strlen($head);
234: $dataLen = strlen($data);
235:
236: do {
237: if (fwrite($handle, str_repeat("\x00", $headLen), $headLen) !== $headLen) {
238: break;
239: }
240:
241: if (fwrite($handle, $data, $dataLen) !== $dataLen) {
242: break;
243: }
244:
245: fseek($handle, 0);
246: if (fwrite($handle, $head, $headLen) !== $headLen) {
247: break;
248: }
249:
250: flock($handle, LOCK_UN);
251: fclose($handle);
252: return;
253: } while (FALSE);
254:
255: $this->delete($cacheFile, $handle);
256: }
257:
258:
259:
260: 261: 262: 263: 264:
265: public function remove($key)
266: {
267: unset($this->locks[$key]);
268: $this->delete($this->getCacheFile($key));
269: }
270:
271:
272:
273: 274: 275: 276: 277:
278: public function clean(array $conds)
279: {
280: $all = !empty($conds[NCache::ALL]);
281: $collector = empty($conds);
282:
283:
284: if ($all || $collector) {
285: $now = time();
286: foreach (NFinder::find('_*')->from($this->dir)->childFirst() as $entry) {
287: $path = (string) $entry;
288: if ($entry->isDir()) {
289: @rmdir($path);
290: continue;
291: }
292: if ($all) {
293: $this->delete($path);
294:
295: } else {
296: $meta = $this->readMetaAndLock($path, LOCK_SH);
297: if (!$meta) {
298: continue;
299: }
300:
301: if ((!empty($meta[self::META_DELTA]) && filemtime($meta[self::FILE]) + $meta[self::META_DELTA] < $now)
302: || (!empty($meta[self::META_EXPIRE]) && $meta[self::META_EXPIRE] < $now)
303: ) {
304: $this->delete($path, $meta[self::HANDLE]);
305: continue;
306: }
307:
308: flock($meta[self::HANDLE], LOCK_UN);
309: fclose($meta[self::HANDLE]);
310: }
311: }
312:
313: if ($this->journal) {
314: $this->journal->clean($conds);
315: }
316: return;
317: }
318:
319:
320: if ($this->journal) {
321: foreach ($this->journal->clean($conds) as $file) {
322: $this->delete($file);
323: }
324: }
325: }
326:
327:
328:
329: 330: 331: 332: 333: 334:
335: protected function readMetaAndLock($file, $lock)
336: {
337: $handle = @fopen($file, 'r+b');
338: if (!$handle) {
339: return NULL;
340: }
341:
342: flock($handle, $lock);
343:
344: $head = stream_get_contents($handle, self::META_HEADER_LEN);
345: if ($head && strlen($head) === self::META_HEADER_LEN) {
346: $size = (int) substr($head, -6);
347: $meta = stream_get_contents($handle, $size, self::META_HEADER_LEN);
348: $meta = @unserialize($meta);
349: if (is_array($meta)) {
350: fseek($handle, $size + self::META_HEADER_LEN);
351: $meta[self::FILE] = $file;
352: $meta[self::HANDLE] = $handle;
353: return $meta;
354: }
355: }
356:
357: flock($handle, LOCK_UN);
358: fclose($handle);
359: return NULL;
360: }
361:
362:
363:
364: 365: 366: 367: 368:
369: protected function readData($meta)
370: {
371: $data = stream_get_contents($meta[self::HANDLE]);
372: flock($meta[self::HANDLE], LOCK_UN);
373: fclose($meta[self::HANDLE]);
374:
375: if (empty($meta[self::META_SERIALIZED])) {
376: return $data;
377: } else {
378: return @unserialize($data);
379: }
380: }
381:
382:
383:
384: 385: 386: 387: 388:
389: protected function getCacheFile($key)
390: {
391: $file = urlencode($key);
392: if ($this->useDirs && $a = strrpos($file, '%00')) {
393: $file = substr_replace($file, '/_', $a, 3);
394: }
395: return $this->dir . '/_' . $file;
396: }
397:
398:
399:
400: 401: 402: 403: 404: 405:
406: private static function delete($file, $handle = NULL)
407: {
408: if (@unlink($file)) {
409: if ($handle) {
410: flock($handle, LOCK_UN);
411: fclose($handle);
412: }
413: return;
414: }
415:
416: if (!$handle) {
417: $handle = @fopen($file, 'r+');
418: }
419: if ($handle) {
420: flock($handle, LOCK_EX);
421: ftruncate($handle, 0);
422: flock($handle, LOCK_UN);
423: fclose($handle);
424: @unlink($file);
425: }
426: }
427:
428: }
429: