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