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: 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: if (!mkdir($dir, 0777)) {
158: return;
159: }
160: }
161: $handle = @fopen($cacheFile, 'r+b');
162: if (!$handle) {
163: $handle = fopen($cacheFile, 'wb');
164: if (!$handle) {
165: return;
166: }
167: }
168:
169: $this->locks[$key] = $handle;
170: flock($handle, LOCK_EX);
171: }
172:
173:
174:
175: 176: 177: 178: 179: 180: 181:
182: public function write($key, $data, array $dp)
183: {
184: $meta = array(
185: self::META_TIME => microtime(),
186: );
187:
188: if (isset($dp[Cache::EXPIRATION])) {
189: if (empty($dp[Cache::SLIDING])) {
190: $meta[self::META_EXPIRE] = $dp[Cache::EXPIRATION] + time();
191: } else {
192: $meta[self::META_DELTA] = (int) $dp[Cache::EXPIRATION];
193: }
194: }
195:
196: if (isset($dp[Cache::ITEMS])) {
197: foreach ((array) $dp[Cache::ITEMS] as $item) {
198: $depFile = $this->getCacheFile($item);
199: $m = $this->readMetaAndLock($depFile, LOCK_SH);
200: $meta[self::META_ITEMS][$depFile] = $m[self::META_TIME];
201: unset($m);
202: }
203: }
204:
205: if (isset($dp[Cache::CALLBACKS])) {
206: $meta[self::META_CALLBACKS] = $dp[Cache::CALLBACKS];
207: }
208:
209: if (!isset($this->locks[$key])) {
210: $this->lock($key);
211: if (!isset($this->locks[$key])) {
212: return;
213: }
214: }
215: $handle = $this->locks[$key];
216: unset($this->locks[$key]);
217:
218: $cacheFile = $this->getCacheFile($key);
219:
220: if (isset($dp[Cache::TAGS]) || isset($dp[Cache::PRIORITY])) {
221: if (!$this->journal) {
222: throw new Nette\InvalidStateException('CacheJournal has not been provided.');
223: }
224: $this->journal->write($cacheFile, $dp);
225: }
226:
227: ftruncate($handle, 0);
228:
229: if (!is_string($data)) {
230: $data = serialize($data);
231: $meta[self::META_SERIALIZED] = TRUE;
232: }
233:
234: $head = serialize($meta) . '?>';
235: $head = '<?php //netteCache[01]' . str_pad((string) strlen($head), 6, '0', STR_PAD_LEFT) . $head;
236: $headLen = strlen($head);
237: $dataLen = strlen($data);
238:
239: do {
240: if (fwrite($handle, str_repeat("\x00", $headLen), $headLen) !== $headLen) {
241: break;
242: }
243:
244: if (fwrite($handle, $data, $dataLen) !== $dataLen) {
245: break;
246: }
247:
248: fseek($handle, 0);
249: if (fwrite($handle, $head, $headLen) !== $headLen) {
250: break;
251: }
252:
253: flock($handle, LOCK_UN);
254: fclose($handle);
255: return;
256: } while (FALSE);
257:
258: $this->delete($cacheFile, $handle);
259: }
260:
261:
262:
263: 264: 265: 266: 267:
268: public function remove($key)
269: {
270: unset($this->locks[$key]);
271: $this->delete($this->getCacheFile($key));
272: }
273:
274:
275:
276: 277: 278: 279: 280:
281: public function clean(array $conds)
282: {
283: $all = !empty($conds[Cache::ALL]);
284: $collector = empty($conds);
285:
286:
287: if ($all || $collector) {
288: $now = time();
289: foreach (Nette\Utils\Finder::find('_*')->from($this->dir)->childFirst() as $entry) {
290: $path = (string) $entry;
291: if ($entry->isDir()) {
292: @rmdir($path);
293: continue;
294: }
295: if ($all) {
296: $this->delete($path);
297:
298: } else {
299: $meta = $this->readMetaAndLock($path, LOCK_SH);
300: if (!$meta) {
301: continue;
302: }
303:
304: if ((!empty($meta[self::META_DELTA]) && filemtime($meta[self::FILE]) + $meta[self::META_DELTA] < $now)
305: || (!empty($meta[self::META_EXPIRE]) && $meta[self::META_EXPIRE] < $now)
306: ) {
307: $this->delete($path, $meta[self::HANDLE]);
308: continue;
309: }
310:
311: flock($meta[self::HANDLE], LOCK_UN);
312: fclose($meta[self::HANDLE]);
313: }
314: }
315:
316: if ($this->journal) {
317: $this->journal->clean($conds);
318: }
319: return;
320: }
321:
322:
323: if ($this->journal) {
324: foreach ($this->journal->clean($conds) as $file) {
325: $this->delete($file);
326: }
327: }
328: }
329:
330:
331:
332: 333: 334: 335: 336: 337:
338: protected function readMetaAndLock($file, $lock)
339: {
340: $handle = @fopen($file, 'r+b');
341: if (!$handle) {
342: return NULL;
343: }
344:
345: flock($handle, $lock);
346:
347: $head = stream_get_contents($handle, self::META_HEADER_LEN);
348: if ($head && strlen($head) === self::META_HEADER_LEN) {
349: $size = (int) substr($head, -6);
350: $meta = stream_get_contents($handle, $size, self::META_HEADER_LEN);
351: $meta = @unserialize($meta);
352: if (is_array($meta)) {
353: fseek($handle, $size + self::META_HEADER_LEN);
354: $meta[self::FILE] = $file;
355: $meta[self::HANDLE] = $handle;
356: return $meta;
357: }
358: }
359:
360: flock($handle, LOCK_UN);
361: fclose($handle);
362: return NULL;
363: }
364:
365:
366:
367: 368: 369: 370: 371:
372: protected function readData($meta)
373: {
374: $data = stream_get_contents($meta[self::HANDLE]);
375: flock($meta[self::HANDLE], LOCK_UN);
376: fclose($meta[self::HANDLE]);
377:
378: if (empty($meta[self::META_SERIALIZED])) {
379: return $data;
380: } else {
381: return @unserialize($data);
382: }
383: }
384:
385:
386:
387: 388: 389: 390: 391:
392: protected function getCacheFile($key)
393: {
394: $file = urlencode($key);
395: if ($this->useDirs && $a = strrpos($file, '%00')) {
396: $file = substr_replace($file, '/_', $a, 3);
397: }
398: return $this->dir . '/_' . $file;
399: }
400:
401:
402:
403: 404: 405: 406: 407: 408:
409: private static function delete($file, $handle = NULL)
410: {
411: if (@unlink($file)) {
412: if ($handle) {
413: flock($handle, LOCK_UN);
414: fclose($handle);
415: }
416: return;
417: }
418:
419: if (!$handle) {
420: $handle = @fopen($file, 'r+');
421: }
422: if ($handle) {
423: flock($handle, LOCK_EX);
424: ftruncate($handle, 0);
425: flock($handle, LOCK_UN);
426: fclose($handle);
427: @unlink($file);
428: }
429: }
430:
431: }
432: