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