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