1: <?php
2:
3: 4: 5: 6: 7: 8: 9: 10:
11:
12: namespace Nette\Caching;
13:
14: use Nette;
15:
16:
17:
18: 19: 20: 21: 22:
23: class FileStorage extends Nette\Object implements ICacheStorage
24: {
25: 26: 27: 28: 29: 30: 31: 32: 33: 34:
35:
36:
37: const META_HEADER_LEN = 28;
38:
39: const META_TIME = 'time';
40: const META_SERIALIZED = 'serialized';
41: const META_EXPIRE = 'expire';
42: const META_DELTA = 'delta';
43: const META_ITEMS = 'di';
44: const META_CALLBACKS = 'callbacks';
45:
46:
47:
48: const FILE = 'file';
49: const HANDLE = 'handle';
50:
51:
52:
53:
54: public static $gcProbability = 0.001;
55:
56:
57: public static $useDirectories;
58:
59:
60: private $dir;
61:
62:
63: private $useDirs;
64:
65:
66: private $db;
67:
68:
69:
70: public function __construct($dir)
71: {
72: if (!is_dir($dir)) {
73: throw new DirectoryNotFoundException("Directory '$dir' is not found or is not directory.");
74: }
75:
76: $this->dir = $dir;
77: $this->useDirs = self::$useDirectories === NULL ? !ini_get('safe_mode') : (bool) self::$useDirectories;
78:
79: if (mt_rand() / mt_getrandmax() < self::$gcProbability) {
80: $this->clean(array());
81: }
82: }
83:
84:
85:
86: 87: 88: 89: 90:
91: public function read($key)
92: {
93: $meta = $this->readMeta($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:
109: private function verify($meta)
110: {
111: do {
112: if (!empty($meta[self::META_DELTA])) {
113:
114: if (filemtime($meta[self::FILE]) + $meta[self::META_DELTA] < time()) break;
115: touch($meta[self::FILE]);
116:
117: } elseif (!empty($meta[self::META_EXPIRE]) && $meta[self::META_EXPIRE] < time()) {
118: break;
119: }
120:
121: if (!empty($meta[self::META_CALLBACKS]) && !Cache::checkCallbacks($meta[self::META_CALLBACKS])) {
122: break;
123: }
124:
125: if (!empty($meta[self::META_ITEMS])) {
126: foreach ($meta[self::META_ITEMS] as $depFile => $time) {
127: $m = $this->readMeta($depFile, LOCK_SH);
128: if ($m[self::META_TIME] !== $time) break 2;
129: if ($m && !$this->verify($m)) break 2;
130: }
131: }
132:
133: return TRUE;
134: } while (FALSE);
135:
136: $this->delete($meta[self::FILE], $meta[self::HANDLE]);
137: return FALSE;
138: }
139:
140:
141:
142: 143: 144: 145: 146: 147: 148:
149: public function write($key, $data, array $dp)
150: {
151: $meta = array(
152: self::META_TIME => microtime(),
153: );
154:
155: if (isset($dp[Cache::EXPIRATION])) {
156: if (empty($dp[Cache::SLIDING])) {
157: $meta[self::META_EXPIRE] = $dp[Cache::EXPIRATION] + time();
158: } else {
159: $meta[self::META_DELTA] = (int) $dp[Cache::EXPIRATION];
160: }
161: }
162:
163: if (isset($dp[Cache::ITEMS])) {
164: foreach ((array) $dp[Cache::ITEMS] as $item) {
165: $depFile = $this->getCacheFile($item);
166: $m = $this->readMeta($depFile, LOCK_SH);
167: $meta[self::META_ITEMS][$depFile] = $m[self::META_TIME];
168: unset($m);
169: }
170: }
171:
172: if (isset($dp[Cache::CALLBACKS])) {
173: $meta[self::META_CALLBACKS] = $dp[Cache::CALLBACKS];
174: }
175:
176: $cacheFile = $this->getCacheFile($key);
177: if ($this->useDirs && !is_dir($dir = dirname($cacheFile))) {
178: umask(0000);
179: if (!mkdir($dir, 0777)) {
180: return;
181: }
182: }
183: $handle = @fopen($cacheFile, 'r+b');
184: if (!$handle) {
185: $handle = fopen($cacheFile, 'wb');
186: if (!$handle) {
187: return;
188: }
189: }
190:
191: if (isset($dp[Cache::TAGS]) || isset($dp[Cache::PRIORITY])) {
192: $db = $this->getDb();
193: $dbFile = sqlite_escape_string($cacheFile);
194: $query = '';
195: if (!empty($dp[Cache::TAGS])) {
196: foreach ((array) $dp[Cache::TAGS] as $tag) {
197: $query .= "INSERT INTO cache (file, tag) VALUES ('$dbFile', '" . sqlite_escape_string($tag) . "');";
198: }
199: }
200: if (isset($dp[Cache::PRIORITY])) {
201: $query .= "INSERT INTO cache (file, priority) VALUES ('$dbFile', '" . (int) $dp[Cache::PRIORITY] . "');";
202: }
203: if (!sqlite_exec($db, "BEGIN; DELETE FROM cache WHERE file = '$dbFile'; $query COMMIT;")) {
204: sqlite_exec($db, "ROLLBACK");
205: return;
206: }
207: }
208:
209: flock($handle, LOCK_EX);
210: ftruncate($handle, 0);
211:
212: if (!is_string($data)) {
213: $data = serialize($data);
214: $meta[self::META_SERIALIZED] = TRUE;
215: }
216:
217: $head = serialize($meta) . '?>';
218: $head = '<?php //netteCache[01]' . str_pad((string) strlen($head), 6, '0', STR_PAD_LEFT) . $head;
219: $headLen = strlen($head);
220: $dataLen = strlen($data);
221:
222: do {
223: if (fwrite($handle, str_repeat("\x00", $headLen), $headLen) !== $headLen) {
224: break;
225: }
226:
227: if (fwrite($handle, $data, $dataLen) !== $dataLen) {
228: break;
229: }
230:
231: fseek($handle, 0);
232: if (fwrite($handle, $head, $headLen) !== $headLen) {
233: break;
234: }
235:
236: flock($handle, LOCK_UN);
237: fclose($handle);
238: return TRUE;
239: } while (FALSE);
240:
241: $this->delete($cacheFile, $handle);
242: }
243:
244:
245:
246: 247: 248: 249: 250:
251: public function remove($key)
252: {
253: $this->delete($this->getCacheFile($key));
254: }
255:
256:
257:
258: 259: 260: 261: 262:
263: public function clean(array $conds)
264: {
265: $all = !empty($conds[Cache::ALL]);
266: $collector = empty($conds);
267:
268:
269: if ($all || $collector) {
270: $now = time();
271: $base = $this->dir . DIRECTORY_SEPARATOR . 'c';
272: $iterator = new \RecursiveIteratorIterator(new \RecursiveDirectoryIterator($this->dir), \RecursiveIteratorIterator::CHILD_FIRST);
273: foreach ($iterator as $entry) {
274: $path = (string) $entry;
275: if (strncmp($path, $base, strlen($base))) {
276: continue;
277: }
278: if ($entry->isDir()) {
279: @rmdir($path);
280: continue;
281: }
282: if ($all) {
283: $this->delete($path);
284:
285: } else {
286: $meta = $this->readMeta($path, LOCK_SH);
287: if (!$meta) continue;
288:
289: if (!empty($meta[self::META_EXPIRE]) && $meta[self::META_EXPIRE] < $now) {
290: $this->delete($path, $meta[self::HANDLE]);
291: continue;
292: }
293:
294: flock($meta[self::HANDLE], LOCK_UN);
295: fclose($meta[self::HANDLE]);
296: }
297: }
298:
299: if ($all && extension_loaded('sqlite')) {
300: sqlite_exec("DELETE FROM cache", $this->getDb());
301: }
302: return;
303: }
304:
305:
306: if (!empty($conds[Cache::TAGS])) {
307: $db = $this->getDb();
308: foreach ((array) $conds[Cache::TAGS] as $tag) {
309: $tmp[] = "'" . sqlite_escape_string($tag) . "'";
310: }
311: $query[] = "tag IN (" . implode(',', $tmp) . ")";
312: }
313:
314: if (isset($conds[Cache::PRIORITY])) {
315: $query[] = "priority <= " . (int) $conds[Cache::PRIORITY];
316: }
317:
318: if (isset($query)) {
319: $db = $this->getDb();
320: $query = implode(' OR ', $query);
321: $files = sqlite_single_query("SELECT file FROM cache WHERE $query", $db, FALSE);
322: foreach ($files as $file) {
323: $this->delete($file);
324: }
325: sqlite_exec("DELETE FROM cache WHERE $query", $db);
326: }
327: }
328:
329:
330:
331: 332: 333: 334: 335: 336:
337: protected function readMeta($file, $lock)
338: {
339: $handle = @fopen($file, 'r+b');
340: if (!$handle) return NULL;
341:
342: flock($handle, $lock);
343:
344: $head = stream_get_contents($handle, self::META_HEADER_LEN);
345: if ($head && strlen($head) === self::META_HEADER_LEN) {
346: $size = (int) substr($head, -6);
347: $meta = stream_get_contents($handle, $size, self::META_HEADER_LEN);
348: $meta = @unserialize($meta);
349: if (is_array($meta)) {
350: fseek($handle, $size + self::META_HEADER_LEN);
351: $meta[self::FILE] = $file;
352: $meta[self::HANDLE] = $handle;
353: return $meta;
354: }
355: }
356:
357: flock($handle, LOCK_UN);
358: fclose($handle);
359: return NULL;
360: }
361:
362:
363:
364: 365: 366: 367: 368:
369: protected function readData($meta)
370: {
371: $data = stream_get_contents($meta[self::HANDLE]);
372: flock($meta[self::HANDLE], LOCK_UN);
373: fclose($meta[self::HANDLE]);
374:
375: if (empty($meta[self::META_SERIALIZED])) {
376: return $data;
377: } else {
378: return @unserialize($data);
379: }
380: }
381:
382:
383:
384: 385: 386: 387: 388:
389: protected function getCacheFile($key)
390: {
391: if ($this->useDirs) {
392: $key = explode(Cache::NAMESPACE_SEPARATOR, $key, 2);
393: return $this->dir . '/c' . (isset($key[1]) ? '-' . urlencode($key[0]) . '/_' . urlencode($key[1]) : '_' . urlencode($key[0]));
394: } else {
395: return $this->dir . '/c_' . urlencode($key);
396: }
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:
431: 432: 433: 434:
435: protected function getDb()
436: {
437: if ($this->db === NULL) {
438: if (!extension_loaded('sqlite')) {
439: throw new \InvalidStateException("SQLite extension is required for storing tags and priorities.");
440: }
441: $this->db = sqlite_open($this->dir . '/cachejournal.sdb');
442: @sqlite_exec($this->db, 'CREATE TABLE cache (file VARCHAR NOT NULL, priority, tag VARCHAR);
443: CREATE INDEX IDX_FILE ON cache (file); CREATE INDEX IDX_PRI ON cache (priority); CREATE INDEX IDX_TAG ON cache (tag);');
444: }
445: return $this->db;
446: }
447:
448: }
449: