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 implements Nette\Caching\IStorage
18: {
19: use Nette\SmartObject;
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: if (!is_dir($dir)) {
69: throw new Nette\DirectoryNotFoundException("Directory '$dir' not found.");
70: }
71:
72: $this->dir = $dir;
73: $this->useDirs = (bool) static::$useDirectories;
74: $this->journal = $journal;
75:
76: if (mt_rand() / mt_getrandmax() < static::$gcProbability) {
77: $this->clean([]);
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, 'c+b');
151: if ($handle) {
152: $this->locks[$key] = $handle;
153: flock($handle, LOCK_EX);
154: }
155: }
156:
157:
158: 159: 160: 161: 162: 163:
164: public function write($key, $data, array $dp)
165: {
166: $meta = [
167: self::META_TIME => microtime(),
168: ];
169:
170: if (isset($dp[Cache::EXPIRATION])) {
171: if (empty($dp[Cache::SLIDING])) {
172: $meta[self::META_EXPIRE] = $dp[Cache::EXPIRATION] + time();
173: } else {
174: $meta[self::META_DELTA] = (int) $dp[Cache::EXPIRATION];
175: }
176: }
177:
178: if (isset($dp[Cache::ITEMS])) {
179: foreach ((array) $dp[Cache::ITEMS] as $item) {
180: $depFile = $this->getCacheFile($item);
181: $m = $this->readMetaAndLock($depFile, LOCK_SH);
182: $meta[self::META_ITEMS][$depFile] = $m[self::META_TIME];
183: unset($m);
184: }
185: }
186:
187: if (isset($dp[Cache::CALLBACKS])) {
188: $meta[self::META_CALLBACKS] = $dp[Cache::CALLBACKS];
189: }
190:
191: if (!isset($this->locks[$key])) {
192: $this->lock($key);
193: if (!isset($this->locks[$key])) {
194: return;
195: }
196: }
197: $handle = $this->locks[$key];
198: unset($this->locks[$key]);
199:
200: $cacheFile = $this->getCacheFile($key);
201:
202: if (isset($dp[Cache::TAGS]) || isset($dp[Cache::PRIORITY])) {
203: if (!$this->journal) {
204: throw new Nette\InvalidStateException('CacheJournal has not been provided.');
205: }
206: $this->journal->write($cacheFile, $dp);
207: }
208:
209: ftruncate($handle, 0);
210:
211: if (!is_string($data)) {
212: $data = serialize($data);
213: $meta[self::META_SERIALIZED] = true;
214: }
215:
216: $head = serialize($meta) . '?>';
217: $head = '<?php //netteCache[01]' . str_pad((string) strlen($head), 6, '0', STR_PAD_LEFT) . $head;
218: $headLen = strlen($head);
219:
220: do {
221: if (fwrite($handle, str_repeat("\x00", $headLen)) !== $headLen) {
222: break;
223: }
224:
225: if (fwrite($handle, $data) !== strlen($data)) {
226: break;
227: }
228:
229: fseek($handle, 0);
230: if (fwrite($handle, $head) !== $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: $namespaces = isset($conditions[Cache::NAMESPACES]) ? $conditions[Cache::NAMESPACES] : null;
265:
266:
267: if ($all || $collector) {
268: $now = time();
269: foreach (Nette\Utils\Finder::find('_*')->from($this->dir)->childFirst() as $entry) {
270: $path = (string) $entry;
271: if ($entry->isDir()) {
272: @rmdir($path);
273: continue;
274: }
275: if ($all) {
276: $this->delete($path);
277:
278: } else {
279: $meta = $this->readMetaAndLock($path, LOCK_SH);
280: if (!$meta) {
281: continue;
282: }
283:
284: if ((!empty($meta[self::META_DELTA]) && filemtime($meta[self::FILE]) + $meta[self::META_DELTA] < $now)
285: || (!empty($meta[self::META_EXPIRE]) && $meta[self::META_EXPIRE] < $now)
286: ) {
287: $this->delete($path, $meta[self::HANDLE]);
288: continue;
289: }
290:
291: flock($meta[self::HANDLE], LOCK_UN);
292: fclose($meta[self::HANDLE]);
293: }
294: }
295:
296: if ($this->journal) {
297: $this->journal->clean($conditions);
298: }
299: return;
300:
301: } elseif ($namespaces) {
302: foreach ($namespaces as $namespace) {
303: $dir = $this->dir . '/_' . urlencode($namespace);
304: if (is_dir($dir)) {
305: foreach (Nette\Utils\Finder::findFiles('_*')->in($dir) as $entry) {
306: $this->delete((string) $entry);
307: }
308: @rmdir($dir);
309: }
310: }
311: }
312:
313:
314: if ($this->journal) {
315: foreach ($this->journal->clean($conditions) as $file) {
316: $this->delete($file);
317: }
318: }
319: }
320:
321:
322: 323: 324: 325: 326: 327:
328: protected function readMetaAndLock($file, $lock)
329: {
330: $handle = @fopen($file, 'r+b');
331: if (!$handle) {
332: return null;
333: }
334:
335: flock($handle, $lock);
336:
337: $head = stream_get_contents($handle, self::META_HEADER_LEN);
338: if ($head && strlen($head) === self::META_HEADER_LEN) {
339: $size = (int) substr($head, -6);
340: $meta = stream_get_contents($handle, $size, self::META_HEADER_LEN);
341: $meta = unserialize($meta);
342: $meta[self::FILE] = $file;
343: $meta[self::HANDLE] = $handle;
344: return $meta;
345: }
346:
347: flock($handle, LOCK_UN);
348: fclose($handle);
349: return null;
350: }
351:
352:
353: 354: 355: 356: 357:
358: protected function readData($meta)
359: {
360: $data = stream_get_contents($meta[self::HANDLE]);
361: flock($meta[self::HANDLE], LOCK_UN);
362: fclose($meta[self::HANDLE]);
363:
364: if (empty($meta[self::META_SERIALIZED])) {
365: return $data;
366: } else {
367: return unserialize($data);
368: }
369: }
370:
371:
372: 373: 374: 375: 376:
377: protected function getCacheFile($key)
378: {
379: $file = urlencode($key);
380: if ($this->useDirs && $a = strrpos($file, '%00')) {
381: $file = substr_replace($file, '/_', $a, 3);
382: }
383: return $this->dir . '/_' . $file;
384: }
385:
386:
387: 388: 389: 390: 391: 392:
393: private static function delete($file, $handle = null)
394: {
395: if (@unlink($file)) {
396: if ($handle) {
397: flock($handle, LOCK_UN);
398: fclose($handle);
399: }
400: return;
401: }
402:
403: if (!$handle) {
404: $handle = @fopen($file, 'r+');
405: }
406: if ($handle) {
407: flock($handle, LOCK_EX);
408: ftruncate($handle, 0);
409: flock($handle, LOCK_UN);
410: fclose($handle);
411: @unlink($file);
412: }
413: }
414: }
415: