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