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