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