Packages

  • Nette
    • Application
      • Diagnostics
      • Responses
      • Routers
      • UI
    • Caching
      • Storages
    • ComponentModel
    • Config
      • Adapters
      • Extensions
    • Database
      • Diagnostics
      • Drivers
      • Reflection
      • Table
    • DI
      • Diagnostics
    • Diagnostics
    • Forms
      • Controls
      • Rendering
    • Http
    • Iterators
    • Latte
      • Macros
    • Loaders
    • Localization
    • Mail
    • Reflection
    • Security
      • Diagnostics
    • Templating
    • Utils
      • PhpGenerator
  • NetteModule
  • None
  • PHP

Classes

  • NDevNullStorage
  • NFileJournal
  • NFileStorage
  • NMemcachedStorage
  • NMemoryStorage
  • NPhpFileStorage

Interfaces

  • ICacheJournal
  • Overview
  • Package
  • Class
  • Tree
  1: <?php
  2: 
  3: /**
  4:  * This file is part of the Nette Framework (http://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\Storages
 11:  */
 12: 
 13: 
 14: 
 15: /**
 16:  * Cache file storage.
 17:  *
 18:  * @author     David Grudl
 19:  * @package Nette\Caching\Storages
 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:         META_TIME = 'time', // timestamp
 38:         META_SERIALIZED = 'serialized', // is content serialized?
 39:         META_EXPIRE = 'expire', // expiration timestamp
 40:         META_DELTA = 'delta', // relative (sliding) expiration
 41:         META_ITEMS = 'di', // array of dependent items (file => timestamp)
 42:         META_CALLBACKS = 'callbacks'; // array of callbacks (function, args)
 43: 
 44:     /** additional cache structure */
 45:     const FILE = 'file',
 46:         HANDLE = 'handle';
 47: 
 48: 
 49:     /** @var float  probability that the clean() routine is started */
 50:     public static $gcProbability = 0.001;
 51: 
 52:     /** @var bool */
 53:     public static $useDirectories = TRUE;
 54: 
 55:     /** @var string */
 56:     private $dir;
 57: 
 58:     /** @var bool */
 59:     private $useDirs;
 60: 
 61:     /** @var ICacheJournal */
 62:     private $journal;
 63: 
 64:     /** @var array */
 65:     private $locks;
 66: 
 67: 
 68: 
 69:     public function __construct($dir, ICacheJournal $journal = NULL)
 70:     {
 71:         $this->dir = realpath($dir);
 72:         if ($this->dir === FALSE) {
 73:             throw new DirectoryNotFoundException("Directory '$dir' not found.");
 74:         }
 75: 
 76:         $this->useDirs = (bool) self::$useDirectories;
 77:         $this->journal = $journal;
 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->readMetaAndLock($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 readMetaAndLock()
114:                 if (filemtime($meta[self::FILE]) + $meta[self::META_DELTA] < time()) {
115:                     break;
116:                 }
117:                 touch($meta[self::FILE]);
118: 
119:             } elseif (!empty($meta[self::META_EXPIRE]) && $meta[self::META_EXPIRE] < time()) {
120:                 break;
121:             }
122: 
123:             if (!empty($meta[self::META_CALLBACKS]) && !NCache::checkCallbacks($meta[self::META_CALLBACKS])) {
124:                 break;
125:             }
126: 
127:             if (!empty($meta[self::META_ITEMS])) {
128:                 foreach ($meta[self::META_ITEMS] as $depFile => $time) {
129:                     $m = $this->readMetaAndLock($depFile, LOCK_SH);
130:                     if ($m[self::META_TIME] !== $time || ($m && !$this->verify($m))) {
131:                         break 2;
132:                     }
133:                 }
134:             }
135: 
136:             return TRUE;
137:         } while (FALSE);
138: 
139:         $this->delete($meta[self::FILE], $meta[self::HANDLE]); // meta[handle] & meta[file] was added by readMetaAndLock()
140:         return FALSE;
141:     }
142: 
143: 
144: 
145:     /**
146:      * Prevents item reading and writing. Lock is released by write() or remove().
147:      * @param  string key
148:      * @return void
149:      */
150:     public function lock($key)
151:     {
152:         $cacheFile = $this->getCacheFile($key);
153:         if ($this->useDirs && !is_dir($dir = dirname($cacheFile))) {
154:             if (!mkdir($dir, 0777)) {
155:                 return;
156:             }
157:         }
158:         $handle = @fopen($cacheFile, 'r+b'); // @ - file may not exist
159:         if (!$handle) {
160:             $handle = fopen($cacheFile, 'wb');
161:             if (!$handle) {
162:                 return;
163:             }
164:         }
165: 
166:         $this->locks[$key] = $handle;
167:         flock($handle, LOCK_EX);
168:     }
169: 
170: 
171: 
172:     /**
173:      * Writes item into the cache.
174:      * @param  string key
175:      * @param  mixed  data
176:      * @param  array  dependencies
177:      * @return void
178:      */
179:     public function write($key, $data, array $dp)
180:     {
181:         $meta = array(
182:             self::META_TIME => microtime(),
183:         );
184: 
185:         if (isset($dp[NCache::EXPIRATION])) {
186:             if (empty($dp[NCache::SLIDING])) {
187:                 $meta[self::META_EXPIRE] = $dp[NCache::EXPIRATION] + time(); // absolute time
188:             } else {
189:                 $meta[self::META_DELTA] = (int) $dp[NCache::EXPIRATION]; // sliding time
190:             }
191:         }
192: 
193:         if (isset($dp[NCache::ITEMS])) {
194:             foreach ((array) $dp[NCache::ITEMS] as $item) {
195:                 $depFile = $this->getCacheFile($item);
196:                 $m = $this->readMetaAndLock($depFile, LOCK_SH);
197:                 $meta[self::META_ITEMS][$depFile] = $m[self::META_TIME]; // may be NULL
198:                 unset($m);
199:             }
200:         }
201: 
202:         if (isset($dp[NCache::CALLBACKS])) {
203:             $meta[self::META_CALLBACKS] = $dp[NCache::CALLBACKS];
204:         }
205: 
206:         if (!isset($this->locks[$key])) {
207:             $this->lock($key);
208:             if (!isset($this->locks[$key])) {
209:                 return;
210:             }
211:         }
212:         $handle = $this->locks[$key];
213:         unset($this->locks[$key]);
214: 
215:         $cacheFile = $this->getCacheFile($key);
216: 
217:         if (isset($dp[NCache::TAGS]) || isset($dp[NCache::PRIORITY])) {
218:             if (!$this->journal) {
219:                 throw new InvalidStateException('CacheJournal has not been provided.');
220:             }
221:             $this->journal->write($cacheFile, $dp);
222:         }
223: 
224:         ftruncate($handle, 0);
225: 
226:         if (!is_string($data)) {
227:             $data = serialize($data);
228:             $meta[self::META_SERIALIZED] = TRUE;
229:         }
230: 
231:         $head = serialize($meta) . '?>';
232:         $head = '<?php //netteCache[01]' . str_pad((string) strlen($head), 6, '0', STR_PAD_LEFT) . $head;
233:         $headLen = strlen($head);
234:         $dataLen = strlen($data);
235: 
236:         do {
237:             if (fwrite($handle, str_repeat("\x00", $headLen), $headLen) !== $headLen) {
238:                 break;
239:             }
240: 
241:             if (fwrite($handle, $data, $dataLen) !== $dataLen) {
242:                 break;
243:             }
244: 
245:             fseek($handle, 0);
246:             if (fwrite($handle, $head, $headLen) !== $headLen) {
247:                 break;
248:             }
249: 
250:             flock($handle, LOCK_UN);
251:             fclose($handle);
252:             return;
253:         } while (FALSE);
254: 
255:         $this->delete($cacheFile, $handle);
256:     }
257: 
258: 
259: 
260:     /**
261:      * Removes item from the cache.
262:      * @param  string key
263:      * @return void
264:      */
265:     public function remove($key)
266:     {
267:         unset($this->locks[$key]);
268:         $this->delete($this->getCacheFile($key));
269:     }
270: 
271: 
272: 
273:     /**
274:      * Removes items from the cache by conditions & garbage collector.
275:      * @param  array  conditions
276:      * @return void
277:      */
278:     public function clean(array $conds)
279:     {
280:         $all = !empty($conds[NCache::ALL]);
281:         $collector = empty($conds);
282: 
283:         // cleaning using file iterator
284:         if ($all || $collector) {
285:             $now = time();
286:             foreach (NFinder::find('_*')->from($this->dir)->childFirst() as $entry) {
287:                 $path = (string) $entry;
288:                 if ($entry->isDir()) { // collector: remove empty dirs
289:                     @rmdir($path); // @ - removing dirs is not necessary
290:                     continue;
291:                 }
292:                 if ($all) {
293:                     $this->delete($path);
294: 
295:                 } else { // collector
296:                     $meta = $this->readMetaAndLock($path, LOCK_SH);
297:                     if (!$meta) {
298:                         continue;
299:                     }
300: 
301:                     if ((!empty($meta[self::META_DELTA]) && filemtime($meta[self::FILE]) + $meta[self::META_DELTA] < $now)
302:                         || (!empty($meta[self::META_EXPIRE]) && $meta[self::META_EXPIRE] < $now)
303:                     ) {
304:                         $this->delete($path, $meta[self::HANDLE]);
305:                         continue;
306:                     }
307: 
308:                     flock($meta[self::HANDLE], LOCK_UN);
309:                     fclose($meta[self::HANDLE]);
310:                 }
311:             }
312: 
313:             if ($this->journal) {
314:                 $this->journal->clean($conds);
315:             }
316:             return;
317:         }
318: 
319:         // cleaning using journal
320:         if ($this->journal) {
321:             foreach ($this->journal->clean($conds) as $file) {
322:                 $this->delete($file);
323:             }
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 readMetaAndLock($file, $lock)
336:     {
337:         $handle = @fopen($file, 'r+b'); // @ - file may not exist
338:         if (!$handle) {
339:             return NULL;
340:         }
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:         $file = urlencode($key);
392:         if ($this->useDirs && $a = strrpos($file, '%00')) { // %00 = urlencode(NCache::NAMESPACE_SEPARATOR)
393:             $file = substr_replace($file, '/_', $a, 3);
394:         }
395:         return $this->dir . '/_' . $file;
396:     }
397: 
398: 
399: 
400:     /**
401:      * Deletes and closes file.
402:      * @param  string
403:      * @param  resource
404:      * @return void
405:      */
406:     private static function delete($file, $handle = NULL)
407:     {
408:         if (@unlink($file)) { // @ - file may not already exist
409:             if ($handle) {
410:                 flock($handle, LOCK_UN);
411:                 fclose($handle);
412:             }
413:             return;
414:         }
415: 
416:         if (!$handle) {
417:             $handle = @fopen($file, 'r+'); // @ - file may not exist
418:         }
419:         if ($handle) {
420:             flock($handle, LOCK_EX);
421:             ftruncate($handle, 0);
422:             flock($handle, LOCK_UN);
423:             fclose($handle);
424:             @unlink($file); // @ - file may not already exist
425:         }
426:     }
427: 
428: }
429: 
Nette Framework 2.0.0 (for PHP 5.2, prefixed) API API documentation generated by ApiGen 2.7.0