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