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: const META_TIME = 'time';
38: const META_SERIALIZED = 'serialized';
39: const META_EXPIRE = 'expire';
40: const META_DELTA = 'delta';
41: const META_ITEMS = 'di';
42: const META_CALLBACKS = 'callbacks';
43:
44:
45:
46: const FILE = 'file';
47: const HANDLE = 'handle';
48:
49:
50:
51:
52: public static $gcProbability = 0.001;
53:
54:
55: public static $useDirectories;
56:
57:
58: private $dir;
59:
60:
61: private $useDirs;
62:
63:
64: private $db;
65:
66:
67:
68: public function __construct($dir)
69: {
70: if (!is_dir($dir)) {
71: throw new NDirectoryNotFoundException("Directory '$dir' is not found or is not directory.");
72: }
73:
74: $this->dir = $dir;
75: $this->useDirs = self::$useDirectories === NULL ? !ini_get('safe_mode') : (bool) self::$useDirectories;
76:
77: if (mt_rand() / mt_getrandmax() < self::$gcProbability) {
78: $this->clean(array());
79: }
80: }
81:
82:
83:
84: 85: 86: 87: 88:
89: public function read($key)
90: {
91: $meta = $this->readMeta($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:
107: private function verify($meta)
108: {
109: do {
110: if (!empty($meta[self::META_DELTA])) {
111:
112: if (filemtime($meta[self::FILE]) + $meta[self::META_DELTA] < time()) break;
113: touch($meta[self::FILE]);
114:
115: } elseif (!empty($meta[self::META_EXPIRE]) && $meta[self::META_EXPIRE] < time()) {
116: break;
117: }
118:
119: if (!empty($meta[self::META_CALLBACKS]) && !NCache::checkCallbacks($meta[self::META_CALLBACKS])) {
120: break;
121: }
122:
123: if (!empty($meta[self::META_ITEMS])) {
124: foreach ($meta[self::META_ITEMS] as $depFile => $time) {
125: $m = $this->readMeta($depFile, LOCK_SH);
126: if ($m[self::META_TIME] !== $time) break 2;
127: if ($m && !$this->verify($m)) break 2;
128: }
129: }
130:
131: return TRUE;
132: } while (FALSE);
133:
134: $this->delete($meta[self::FILE], $meta[self::HANDLE]);
135: return FALSE;
136: }
137:
138:
139:
140: 141: 142: 143: 144: 145: 146:
147: public function write($key, $data, array $dp)
148: {
149: $meta = array(
150: self::META_TIME => microtime(),
151: );
152:
153: if (isset($dp[NCache::EXPIRATION])) {
154: if (empty($dp[NCache::SLIDING])) {
155: $meta[self::META_EXPIRE] = $dp[NCache::EXPIRATION] + time();
156: } else {
157: $meta[self::META_DELTA] = (int) $dp[NCache::EXPIRATION];
158: }
159: }
160:
161: if (isset($dp[NCache::ITEMS])) {
162: foreach ((array) $dp[NCache::ITEMS] as $item) {
163: $depFile = $this->getCacheFile($item);
164: $m = $this->readMeta($depFile, LOCK_SH);
165: $meta[self::META_ITEMS][$depFile] = $m[self::META_TIME];
166: unset($m);
167: }
168: }
169:
170: if (isset($dp[NCache::CALLBACKS])) {
171: $meta[self::META_CALLBACKS] = $dp[NCache::CALLBACKS];
172: }
173:
174: $cacheFile = $this->getCacheFile($key);
175: if ($this->useDirs && !is_dir($dir = dirname($cacheFile))) {
176: umask(0000);
177: if (!mkdir($dir, 0777)) {
178: return;
179: }
180: }
181: $handle = @fopen($cacheFile, 'r+b');
182: if (!$handle) {
183: $handle = fopen($cacheFile, 'wb');
184: if (!$handle) {
185: return;
186: }
187: }
188:
189: if (isset($dp[NCache::TAGS]) || isset($dp[NCache::PRIORITY])) {
190: $db = $this->getDb();
191: $dbFile = sqlite_escape_string($cacheFile);
192: $query = '';
193: if (!empty($dp[NCache::TAGS])) {
194: foreach ((array) $dp[NCache::TAGS] as $tag) {
195: $query .= "INSERT INTO cache (file, tag) VALUES ('$dbFile', '" . sqlite_escape_string($tag) . "');";
196: }
197: }
198: if (isset($dp[NCache::PRIORITY])) {
199: $query .= "INSERT INTO cache (file, priority) VALUES ('$dbFile', '" . (int) $dp[NCache::PRIORITY] . "');";
200: }
201: if (!sqlite_exec($db, "BEGIN; DELETE FROM cache WHERE file = '$dbFile'; $query COMMIT;")) {
202: sqlite_exec($db, "ROLLBACK");
203: return;
204: }
205: }
206:
207: flock($handle, LOCK_EX);
208: ftruncate($handle, 0);
209:
210: if (!is_string($data)) {
211: $data = serialize($data);
212: $meta[self::META_SERIALIZED] = TRUE;
213: }
214:
215: $head = serialize($meta) . '?>';
216: $head = '<?php //netteCache[01]' . str_pad((string) strlen($head), 6, '0', STR_PAD_LEFT) . $head;
217: $headLen = strlen($head);
218: $dataLen = strlen($data);
219:
220: do {
221: if (fwrite($handle, str_repeat("\x00", $headLen), $headLen) !== $headLen) {
222: break;
223: }
224:
225: if (fwrite($handle, $data, $dataLen) !== $dataLen) {
226: break;
227: }
228:
229: fseek($handle, 0);
230: if (fwrite($handle, $head, $headLen) !== $headLen) {
231: break;
232: }
233:
234: flock($handle, LOCK_UN);
235: fclose($handle);
236: return TRUE;
237: } while (FALSE);
238:
239: $this->delete($cacheFile, $handle);
240: }
241:
242:
243:
244: 245: 246: 247: 248:
249: public function remove($key)
250: {
251: $this->delete($this->getCacheFile($key));
252: }
253:
254:
255:
256: 257: 258: 259: 260:
261: public function clean(array $conds)
262: {
263: $all = !empty($conds[NCache::ALL]);
264: $collector = empty($conds);
265:
266:
267: if ($all || $collector) {
268: $now = time();
269: $base = $this->dir . DIRECTORY_SEPARATOR . 'c';
270: $iterator = new RecursiveIteratorIterator(new RecursiveDirectoryIterator($this->dir), RecursiveIteratorIterator::CHILD_FIRST);
271: foreach ($iterator as $entry) {
272: $path = (string) $entry;
273: if (strncmp($path, $base, strlen($base))) {
274: continue;
275: }
276: if ($entry->isDir()) {
277: @rmdir($path);
278: continue;
279: }
280: if ($all) {
281: $this->delete($path);
282:
283: } else {
284: $meta = $this->readMeta($path, LOCK_SH);
285: if (!$meta) continue;
286:
287: if (!empty($meta[self::META_EXPIRE]) && $meta[self::META_EXPIRE] < $now) {
288: $this->delete($path, $meta[self::HANDLE]);
289: continue;
290: }
291:
292: flock($meta[self::HANDLE], LOCK_UN);
293: fclose($meta[self::HANDLE]);
294: }
295: }
296:
297: if ($all && extension_loaded('sqlite')) {
298: sqlite_exec("DELETE FROM cache", $this->getDb());
299: }
300: return;
301: }
302:
303:
304: if (!empty($conds[NCache::TAGS])) {
305: $db = $this->getDb();
306: foreach ((array) $conds[NCache::TAGS] as $tag) {
307: $tmp[] = "'" . sqlite_escape_string($tag) . "'";
308: }
309: $query[] = "tag IN (" . implode(',', $tmp) . ")";
310: }
311:
312: if (isset($conds[NCache::PRIORITY])) {
313: $query[] = "priority <= " . (int) $conds[NCache::PRIORITY];
314: }
315:
316: if (isset($query)) {
317: $db = $this->getDb();
318: $query = implode(' OR ', $query);
319: $files = sqlite_single_query("SELECT file FROM cache WHERE $query", $db, FALSE);
320: foreach ($files as $file) {
321: $this->delete($file);
322: }
323: sqlite_exec("DELETE FROM cache WHERE $query", $db);
324: }
325: }
326:
327:
328:
329: 330: 331: 332: 333: 334:
335: protected function readMeta($file, $lock)
336: {
337: $handle = @fopen($file, 'r+b');
338: if (!$handle) return NULL;
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: if ($this->useDirs) {
390: $key = explode(NCache::NAMESPACE_SEPARATOR, $key, 2);
391: return $this->dir . '/c' . (isset($key[1]) ? '-' . urlencode($key[0]) . '/_' . urlencode($key[1]) : '_' . urlencode($key[0]));
392: } else {
393: return $this->dir . '/c_' . urlencode($key);
394: }
395: }
396:
397:
398:
399: 400: 401: 402: 403: 404:
405: private static function delete($file, $handle = NULL)
406: {
407: if (@unlink($file)) {
408: if ($handle) {
409: flock($handle, LOCK_UN);
410: fclose($handle);
411: }
412: return;
413: }
414:
415: if (!$handle) {
416: $handle = @fopen($file, 'r+');
417: }
418: if ($handle) {
419: flock($handle, LOCK_EX);
420: ftruncate($handle, 0);
421: flock($handle, LOCK_UN);
422: fclose($handle);
423: @unlink($file);
424: }
425: }
426:
427:
428:
429: 430: 431: 432:
433: protected function getDb()
434: {
435: if ($this->db === NULL) {
436: if (!extension_loaded('sqlite')) {
437: throw new InvalidStateException("SQLite extension is required for storing tags and priorities.");
438: }
439: $this->db = sqlite_open($this->dir . '/cachejournal.sdb');
440: @sqlite_exec($this->db, 'CREATE TABLE cache (file VARCHAR NOT NULL, priority, tag VARCHAR);
441: CREATE INDEX IDX_FILE ON cache (file); CREATE INDEX IDX_PRI ON cache (priority); CREATE INDEX IDX_TAG ON cache (tag);');
442: }
443: return $this->db;
444: }
445:
446: }
447: