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