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