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