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