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