Namespaces

  • Nette
    • Application
    • Caching
    • Collections
    • Config
    • Forms
    • IO
    • Loaders
    • Mail
    • Reflection
    • Security
    • Templates
    • Web
  • None
  • PHP

Classes

  • Cache
  • DummyStorage
  • FileStorage
  • MemcachedStorage

Interfaces

  • ICacheStorage
  • Overview
  • Namespace
  • Class
  • Tree
  • Other releases
  1: <?php
  2: 
  3: /**
  4:  * This file is part of the Nette Framework (https://nette.org)
  5:  *
  6:  * Copyright (c) 2004 David Grudl (http://davidgrudl.com)
  7:  *
  8:  * For the full copyright and license information, please view
  9:  * the file license.txt that was distributed with this source code.
 10:  */
 11: 
 12: namespace Nette\Caching;
 13: 
 14: use Nette;
 15: 
 16: 
 17: 
 18: /**
 19:  * Cache file storage.
 20:  *
 21:  * @author     David Grudl
 22:  */
 23: class FileStorage extends Nette\Object implements ICacheStorage
 24: {
 25:     /**
 26:      * Atomic thread safe logic:
 27:      *
 28:      * 1) reading: open(r+b), lock(SH), read
 29:      *     - delete?: delete*, close
 30:      * 2) deleting: delete*
 31:      * 3) writing: open(r+b || wb), lock(EX), truncate*, write data, write meta, close
 32:      *
 33:      * delete* = try unlink, if fails (on NTFS) { lock(EX), truncate, close, unlink } else close (on ext3)
 34:      */
 35: 
 36:     /**#@+ @internal cache file structure */
 37:     const META_HEADER_LEN = 28; // 22b signature + 6b meta-struct size + serialized meta-struct + data
 38:     // meta structure: array of
 39:     const META_TIME = 'time'; // timestamp
 40:     const META_SERIALIZED = 'serialized'; // is content serialized?
 41:     const META_EXPIRE = 'expire'; // expiration timestamp
 42:     const META_DELTA = 'delta'; // relative (sliding) expiration
 43:     const META_ITEMS = 'di'; // array of dependent items (file => timestamp)
 44:     const META_CALLBACKS = 'callbacks'; // array of callbacks (function, args)
 45:     /**#@-*/
 46: 
 47:     /**#@+ additional cache structure */
 48:     const FILE = 'file';
 49:     const HANDLE = 'handle';
 50:     /**#@-*/
 51: 
 52: 
 53:     /** @var float  probability that the clean() routine is started */
 54:     public static $gcProbability = 0.001;
 55: 
 56:     /** @var bool */
 57:     public static $useDirectories;
 58: 
 59:     /** @var string */
 60:     private $dir;
 61: 
 62:     /** @var bool */
 63:     private $useDirs;
 64: 
 65:     /** @var resource */
 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:      * Read from cache.
 88:      * @param  string key
 89:      * @return mixed|NULL
 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); // calls fclose()
 96: 
 97:         } else {
 98:             return NULL;
 99:         }
100:     }
101: 
102: 
103: 
104:     /**
105:      * Verifies dependencies.
106:      * @param  array
107:      * @return bool
108:      */
109:     private function verify($meta)
110:     {
111:         do {
112:             if (!empty($meta[self::META_DELTA])) {
113:                 // meta[file] was added by readMeta()
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]); // meta[handle] & meta[file] was added by readMeta()
137:         return FALSE;
138:     }
139: 
140: 
141: 
142:     /**
143:      * Writes item into the cache.
144:      * @param  string key
145:      * @param  mixed  data
146:      * @param  array  dependencies
147:      * @return void
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(); // absolute time
158:             } else {
159:                 $meta[self::META_DELTA] = (int) $dp[Cache::EXPIRATION]; // sliding time
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'); // @ - file may not exist
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:      * Removes item from the cache.
248:      * @param  string key
249:      * @return void
250:      */
251:     public function remove($key)
252:     {
253:         $this->delete($this->getCacheFile($key));
254:     }
255: 
256: 
257: 
258:     /**
259:      * Removes items from the cache by conditions & garbage collector.
260:      * @param  array  conditions
261:      * @return void
262:      */
263:     public function clean(array $conds)
264:     {
265:         $all = !empty($conds[Cache::ALL]);
266:         $collector = empty($conds);
267: 
268:         // cleaning using file iterator
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))) { // skip files out of cache
276:                     continue;
277:                 }
278:                 if ($entry->isDir()) { // collector: remove empty dirs
279:                     @rmdir($path); // @ - removing dirs is not necessary
280:                     continue;
281:                 }
282:                 if ($all) {
283:                     $this->delete($path);
284: 
285:                 } else { // collector
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:         // cleaning using journal
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:      * Reads cache data from disk.
333:      * @param  string  file path
334:      * @param  int     lock mode
335:      * @return array|NULL
336:      */
337:     protected function readMeta($file, $lock)
338:     {
339:         $handle = @fopen($file, 'r+b'); // @ - file may not exist
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); // intentionally @
349:             if (is_array($meta)) {
350:                 fseek($handle, $size + self::META_HEADER_LEN); // needed by PHP < 5.2.6
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:      * Reads cache data from disk and closes cache file handle.
366:      * @param  array
367:      * @return mixed
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); // intentionally @
379:         }
380:     }
381: 
382: 
383: 
384:     /**
385:      * Returns file name.
386:      * @param  string
387:      * @return string
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:      * Deletes and closes file.
403:      * @param  string
404:      * @param  resource
405:      * @return void
406:      */
407:     private static function delete($file, $handle = NULL)
408:     {
409:         if (@unlink($file)) { // @ - file may not already exist
410:             if ($handle) {
411:                 flock($handle, LOCK_UN);
412:                 fclose($handle);
413:             }
414:             return;
415:         }
416: 
417:         if (!$handle) {
418:             $handle = @fopen($file, 'r+'); // @ - file may not exist
419:         }
420:         if ($handle) {
421:             flock($handle, LOCK_EX);
422:             ftruncate($handle, 0);
423:             flock($handle, LOCK_UN);
424:             fclose($handle);
425:             @unlink($file); // @ - file may not already exist
426:         }
427:     }
428: 
429: 
430: 
431:     /**
432:      * Returns SQLite resource.
433:      * @return resource
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);'); // @ - table may already exist
444:         }
445:         return $this->db;
446:     }
447: 
448: }
449: 
Nette Framework 0.9.7 API documentation generated by ApiGen 2.3.0