Packages

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

Classes

  • NCache
  • NDummyStorage
  • NFileStorage
  • NMemcachedStorage

Interfaces

  • ICacheStorage
  • Overview
  • Package
  • 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:  * @package Nette\Caching
 11:  */
 12: 
 13: 
 14: 
 15: /**
 16:  * Cache file storage.
 17:  *
 18:  * @author     David Grudl
 19:  * @package Nette\Caching
 20:  */
 21: class NFileStorage extends NObject implements ICacheStorage
 22: {
 23:     /**
 24:      * Atomic thread safe logic:
 25:      *
 26:      * 1) reading: open(r+b), lock(SH), read
 27:      *     - delete?: delete*, close
 28:      * 2) deleting: delete*
 29:      * 3) writing: open(r+b || wb), lock(EX), truncate*, write data, write meta, close
 30:      *
 31:      * delete* = try unlink, if fails (on NTFS) { lock(EX), truncate, close, unlink } else close (on ext3)
 32:      */
 33: 
 34:     /**#@+ @internal cache file structure */
 35:     const META_HEADER_LEN = 28; // 22b signature + 6b meta-struct size + serialized meta-struct + data
 36:     // meta structure: array of
 37:     const META_TIME = 'time'; // timestamp
 38:     const META_SERIALIZED = 'serialized'; // is content serialized?
 39:     const META_EXPIRE = 'expire'; // expiration timestamp
 40:     const META_DELTA = 'delta'; // relative (sliding) expiration
 41:     const META_ITEMS = 'di'; // array of dependent items (file => timestamp)
 42:     const META_CALLBACKS = 'callbacks'; // array of callbacks (function, args)
 43:     /**#@-*/
 44: 
 45:     /**#@+ additional cache structure */
 46:     const FILE = 'file';
 47:     const HANDLE = 'handle';
 48:     /**#@-*/
 49: 
 50: 
 51:     /** @var float  probability that the clean() routine is started */
 52:     public static $gcProbability = 0.001;
 53: 
 54:     /** @var bool */
 55:     public static $useDirectories;
 56: 
 57:     /** @var string */
 58:     private $dir;
 59: 
 60:     /** @var bool */
 61:     private $useDirs;
 62: 
 63:     /** @var resource */
 64:     private $db;
 65: 
 66: 
 67: 
 68:     public function __construct($dir)
 69:     {
 70:         if (!is_dir($dir)) {
 71:             throw new NDirectoryNotFoundException("Directory '$dir' is not found or is not directory.");
 72:         }
 73: 
 74:         $this->dir = $dir;
 75:         $this->useDirs = self::$useDirectories === NULL ? !ini_get('safe_mode') : (bool) self::$useDirectories;
 76: 
 77:         if (mt_rand() / mt_getrandmax() < self::$gcProbability) {
 78:             $this->clean(array());
 79:         }
 80:     }
 81: 
 82: 
 83: 
 84:     /**
 85:      * Read from cache.
 86:      * @param  string key
 87:      * @return mixed|NULL
 88:      */
 89:     public function read($key)
 90:     {
 91:         $meta = $this->readMeta($this->getCacheFile($key), LOCK_SH);
 92:         if ($meta && $this->verify($meta)) {
 93:             return $this->readData($meta); // calls fclose()
 94: 
 95:         } else {
 96:             return NULL;
 97:         }
 98:     }
 99: 
100: 
101: 
102:     /**
103:      * Verifies dependencies.
104:      * @param  array
105:      * @return bool
106:      */
107:     private function verify($meta)
108:     {
109:         do {
110:             if (!empty($meta[self::META_DELTA])) {
111:                 // meta[file] was added by readMeta()
112:                 if (filemtime($meta[self::FILE]) + $meta[self::META_DELTA] < time()) break;
113:                 touch($meta[self::FILE]);
114: 
115:             } elseif (!empty($meta[self::META_EXPIRE]) && $meta[self::META_EXPIRE] < time()) {
116:                 break;
117:             }
118: 
119:             if (!empty($meta[self::META_CALLBACKS]) && !NCache::checkCallbacks($meta[self::META_CALLBACKS])) {
120:                 break;
121:             }
122: 
123:             if (!empty($meta[self::META_ITEMS])) {
124:                 foreach ($meta[self::META_ITEMS] as $depFile => $time) {
125:                     $m = $this->readMeta($depFile, LOCK_SH);
126:                     if ($m[self::META_TIME] !== $time) break 2;
127:                     if ($m && !$this->verify($m)) break 2;
128:                 }
129:             }
130: 
131:             return TRUE;
132:         } while (FALSE);
133: 
134:         $this->delete($meta[self::FILE], $meta[self::HANDLE]); // meta[handle] & meta[file] was added by readMeta()
135:         return FALSE;
136:     }
137: 
138: 
139: 
140:     /**
141:      * Writes item into the cache.
142:      * @param  string key
143:      * @param  mixed  data
144:      * @param  array  dependencies
145:      * @return void
146:      */
147:     public function write($key, $data, array $dp)
148:     {
149:         $meta = array(
150:             self::META_TIME => microtime(),
151:         );
152: 
153:         if (isset($dp[NCache::EXPIRATION])) {
154:             if (empty($dp[NCache::SLIDING])) {
155:                 $meta[self::META_EXPIRE] = $dp[NCache::EXPIRATION] + time(); // absolute time
156:             } else {
157:                 $meta[self::META_DELTA] = (int) $dp[NCache::EXPIRATION]; // sliding time
158:             }
159:         }
160: 
161:         if (isset($dp[NCache::ITEMS])) {
162:             foreach ((array) $dp[NCache::ITEMS] as $item) {
163:                 $depFile = $this->getCacheFile($item);
164:                 $m = $this->readMeta($depFile, LOCK_SH);
165:                 $meta[self::META_ITEMS][$depFile] = $m[self::META_TIME];
166:                 unset($m);
167:             }
168:         }
169: 
170:         if (isset($dp[NCache::CALLBACKS])) {
171:             $meta[self::META_CALLBACKS] = $dp[NCache::CALLBACKS];
172:         }
173: 
174:         $cacheFile = $this->getCacheFile($key);
175:         if ($this->useDirs && !is_dir($dir = dirname($cacheFile))) {
176:             umask(0000);
177:             if (!mkdir($dir, 0777)) {
178:                 return;
179:             }
180:         }
181:         $handle = @fopen($cacheFile, 'r+b'); // @ - file may not exist
182:         if (!$handle) {
183:             $handle = fopen($cacheFile, 'wb');
184:             if (!$handle) {
185:                 return;
186:             }
187:         }
188: 
189:         if (isset($dp[NCache::TAGS]) || isset($dp[NCache::PRIORITY])) {
190:             $db = $this->getDb();
191:             $dbFile = sqlite_escape_string($cacheFile);
192:             $query = '';
193:             if (!empty($dp[NCache::TAGS])) {
194:                 foreach ((array) $dp[NCache::TAGS] as $tag) {
195:                     $query .= "INSERT INTO cache (file, tag) VALUES ('$dbFile', '" . sqlite_escape_string($tag) . "');";
196:                 }
197:             }
198:             if (isset($dp[NCache::PRIORITY])) {
199:                 $query .= "INSERT INTO cache (file, priority) VALUES ('$dbFile', '" . (int) $dp[NCache::PRIORITY] . "');";
200:             }
201:             if (!sqlite_exec($db, "BEGIN; DELETE FROM cache WHERE file = '$dbFile'; $query COMMIT;")) {
202:                 sqlite_exec($db, "ROLLBACK");
203:                 return;
204:             }
205:         }
206: 
207:         flock($handle, LOCK_EX);
208:         ftruncate($handle, 0);
209: 
210:         if (!is_string($data)) {
211:             $data = serialize($data);
212:             $meta[self::META_SERIALIZED] = TRUE;
213:         }
214: 
215:         $head = serialize($meta) . '?>';
216:         $head = '<?php //netteCache[01]' . str_pad((string) strlen($head), 6, '0', STR_PAD_LEFT) . $head;
217:         $headLen = strlen($head);
218:         $dataLen = strlen($data);
219: 
220:         do {
221:             if (fwrite($handle, str_repeat("\x00", $headLen), $headLen) !== $headLen) {
222:                 break;
223:             }
224: 
225:             if (fwrite($handle, $data, $dataLen) !== $dataLen) {
226:                 break;
227:             }
228: 
229:             fseek($handle, 0);
230:             if (fwrite($handle, $head, $headLen) !== $headLen) {
231:                 break;
232:             }
233: 
234:             flock($handle, LOCK_UN);
235:             fclose($handle);
236:             return TRUE;
237:         } while (FALSE);
238: 
239:         $this->delete($cacheFile, $handle);
240:     }
241: 
242: 
243: 
244:     /**
245:      * Removes item from the cache.
246:      * @param  string key
247:      * @return void
248:      */
249:     public function remove($key)
250:     {
251:         $this->delete($this->getCacheFile($key));
252:     }
253: 
254: 
255: 
256:     /**
257:      * Removes items from the cache by conditions & garbage collector.
258:      * @param  array  conditions
259:      * @return void
260:      */
261:     public function clean(array $conds)
262:     {
263:         $all = !empty($conds[NCache::ALL]);
264:         $collector = empty($conds);
265: 
266:         // cleaning using file iterator
267:         if ($all || $collector) {
268:             $now = time();
269:             $base = $this->dir . DIRECTORY_SEPARATOR . 'c';
270:             $iterator = new RecursiveIteratorIterator(new RecursiveDirectoryIterator($this->dir), RecursiveIteratorIterator::CHILD_FIRST);
271:             foreach ($iterator as $entry) {
272:                 $path = (string) $entry;
273:                 if (strncmp($path, $base, strlen($base))) { // skip files out of cache
274:                     continue;
275:                 }
276:                 if ($entry->isDir()) { // collector: remove empty dirs
277:                     @rmdir($path); // @ - removing dirs is not necessary
278:                     continue;
279:                 }
280:                 if ($all) {
281:                     $this->delete($path);
282: 
283:                 } else { // collector
284:                     $meta = $this->readMeta($path, LOCK_SH);
285:                     if (!$meta) continue;
286: 
287:                     if (!empty($meta[self::META_EXPIRE]) && $meta[self::META_EXPIRE] < $now) {
288:                         $this->delete($path, $meta[self::HANDLE]);
289:                         continue;
290:                     }
291: 
292:                     flock($meta[self::HANDLE], LOCK_UN);
293:                     fclose($meta[self::HANDLE]);
294:                 }
295:             }
296: 
297:             if ($all && extension_loaded('sqlite')) {
298:                 sqlite_exec("DELETE FROM cache", $this->getDb());
299:             }
300:             return;
301:         }
302: 
303:         // cleaning using journal
304:         if (!empty($conds[NCache::TAGS])) {
305:             $db = $this->getDb();
306:             foreach ((array) $conds[NCache::TAGS] as $tag) {
307:                 $tmp[] = "'" . sqlite_escape_string($tag) . "'";
308:             }
309:             $query[] = "tag IN (" . implode(',', $tmp) . ")";
310:         }
311: 
312:         if (isset($conds[NCache::PRIORITY])) {
313:             $query[] = "priority <= " . (int) $conds[NCache::PRIORITY];
314:         }
315: 
316:         if (isset($query)) {
317:             $db = $this->getDb();
318:             $query = implode(' OR ', $query);
319:             $files = sqlite_single_query("SELECT file FROM cache WHERE $query", $db, FALSE);
320:             foreach ($files as $file) {
321:                 $this->delete($file);
322:             }
323:             sqlite_exec("DELETE FROM cache WHERE $query", $db);
324:         }
325:     }
326: 
327: 
328: 
329:     /**
330:      * Reads cache data from disk.
331:      * @param  string  file path
332:      * @param  int     lock mode
333:      * @return array|NULL
334:      */
335:     protected function readMeta($file, $lock)
336:     {
337:         $handle = @fopen($file, 'r+b'); // @ - file may not exist
338:         if (!$handle) return NULL;
339: 
340:         flock($handle, $lock);
341: 
342:         $head = stream_get_contents($handle, self::META_HEADER_LEN);
343:         if ($head && strlen($head) === self::META_HEADER_LEN) {
344:             $size = (int) substr($head, -6);
345:             $meta = stream_get_contents($handle, $size, self::META_HEADER_LEN);
346:             $meta = @unserialize($meta); // intentionally @
347:             if (is_array($meta)) {
348:                 fseek($handle, $size + self::META_HEADER_LEN); // needed by PHP < 5.2.6
349:                 $meta[self::FILE] = $file;
350:                 $meta[self::HANDLE] = $handle;
351:                 return $meta;
352:             }
353:         }
354: 
355:         flock($handle, LOCK_UN);
356:         fclose($handle);
357:         return NULL;
358:     }
359: 
360: 
361: 
362:     /**
363:      * Reads cache data from disk and closes cache file handle.
364:      * @param  array
365:      * @return mixed
366:      */
367:     protected function readData($meta)
368:     {
369:         $data = stream_get_contents($meta[self::HANDLE]);
370:         flock($meta[self::HANDLE], LOCK_UN);
371:         fclose($meta[self::HANDLE]);
372: 
373:         if (empty($meta[self::META_SERIALIZED])) {
374:             return $data;
375:         } else {
376:             return @unserialize($data); // intentionally @
377:         }
378:     }
379: 
380: 
381: 
382:     /**
383:      * Returns file name.
384:      * @param  string
385:      * @return string
386:      */
387:     protected function getCacheFile($key)
388:     {
389:         if ($this->useDirs) {
390:             $key = explode(NCache::NAMESPACE_SEPARATOR, $key, 2);
391:             return $this->dir . '/c' . (isset($key[1]) ? '-' . urlencode($key[0]) . '/_' . urlencode($key[1]) : '_' . urlencode($key[0]));
392:         } else {
393:             return $this->dir . '/c_' . urlencode($key);
394:         }
395:     }
396: 
397: 
398: 
399:     /**
400:      * Deletes and closes file.
401:      * @param  string
402:      * @param  resource
403:      * @return void
404:      */
405:     private static function delete($file, $handle = NULL)
406:     {
407:         if (@unlink($file)) { // @ - file may not already exist
408:             if ($handle) {
409:                 flock($handle, LOCK_UN);
410:                 fclose($handle);
411:             }
412:             return;
413:         }
414: 
415:         if (!$handle) {
416:             $handle = @fopen($file, 'r+'); // @ - file may not exist
417:         }
418:         if ($handle) {
419:             flock($handle, LOCK_EX);
420:             ftruncate($handle, 0);
421:             flock($handle, LOCK_UN);
422:             fclose($handle);
423:             @unlink($file); // @ - file may not already exist
424:         }
425:     }
426: 
427: 
428: 
429:     /**
430:      * Returns SQLite resource.
431:      * @return resource
432:      */
433:     protected function getDb()
434:     {
435:         if ($this->db === NULL) {
436:             if (!extension_loaded('sqlite')) {
437:                 throw new InvalidStateException("SQLite extension is required for storing tags and priorities.");
438:             }
439:             $this->db = sqlite_open($this->dir . '/cachejournal.sdb');
440:             @sqlite_exec($this->db, 'CREATE TABLE cache (file VARCHAR NOT NULL, priority, tag VARCHAR);
441:             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
442:         }
443:         return $this->db;
444:     }
445: 
446: }
447: 
Nette Framework 0.9.7 (for PHP 5.2) API documentation generated by ApiGen 2.3.0