Namespaces

  • 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

  • DevNullStorage
  • FileJournal
  • FileStorage
  • MemcachedStorage
  • MemoryStorage
  • PhpFileStorage

Interfaces

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