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

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

Interfaces

  • ICacheJournal
  • Overview
  • Package
  • Class
  • Tree
  • Deprecated
  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 FileStorage extends Object 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:     public function __construct($dir, ICacheJournal $journal = NULL)
 69:     {
 70:         $this->dir = realpath($dir);
 71:         if ($this->dir === FALSE) {
 72:             throw new DirectoryNotFoundException("Directory '$dir' not found.");
 73:         }
 74: 
 75:         $this->useDirs = (bool) self::$useDirectories;
 76:         $this->journal = $journal;
 77: 
 78:         if (mt_rand() / mt_getrandmax() < self::$gcProbability) {
 79:             $this->clean(array());
 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->readMetaAndLock($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:      * Verifies dependencies.
103:      * @param  array
104:      * @return bool
105:      */
106:     private function verify($meta)
107:     {
108:         do {
109:             if (!empty($meta[self::META_DELTA])) {
110:                 // meta[file] was added by readMetaAndLock()
111:                 if (filemtime($meta[self::FILE]) + $meta[self::META_DELTA] < time()) {
112:                     break;
113:                 }
114:                 touch($meta[self::FILE]);
115: 
116:             } elseif (!empty($meta[self::META_EXPIRE]) && $meta[self::META_EXPIRE] < time()) {
117:                 break;
118:             }
119: 
120:             if (!empty($meta[self::META_CALLBACKS]) && !Cache::checkCallbacks($meta[self::META_CALLBACKS])) {
121:                 break;
122:             }
123: 
124:             if (!empty($meta[self::META_ITEMS])) {
125:                 foreach ($meta[self::META_ITEMS] as $depFile => $time) {
126:                     $m = $this->readMetaAndLock($depFile, LOCK_SH);
127:                     if ($m[self::META_TIME] !== $time || ($m && !$this->verify($m))) {
128:                         break 2;
129:                     }
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 readMetaAndLock()
137:         return FALSE;
138:     }
139: 
140: 
141:     /**
142:      * Prevents item reading and writing. Lock is released by write() or remove().
143:      * @param  string key
144:      * @return void
145:      */
146:     public function lock($key)
147:     {
148:         $cacheFile = $this->getCacheFile($key);
149:         if ($this->useDirs && !is_dir($dir = dirname($cacheFile))) {
150:             @mkdir($dir); // @ - directory may already exist
151:         }
152:         $handle = @fopen($cacheFile, 'r+b'); // @ - file may not exist
153:         if (!$handle) {
154:             $handle = fopen($cacheFile, 'wb');
155:             if (!$handle) {
156:                 return;
157:             }
158:         }
159: 
160:         $this->locks[$key] = $handle;
161:         flock($handle, LOCK_EX);
162:     }
163: 
164: 
165:     /**
166:      * Writes item into the cache.
167:      * @param  string key
168:      * @param  mixed  data
169:      * @param  array  dependencies
170:      * @return void
171:      */
172:     public function write($key, $data, array $dp)
173:     {
174:         $meta = array(
175:             self::META_TIME => microtime(),
176:         );
177: 
178:         if (isset($dp[Cache::EXPIRATION])) {
179:             if (empty($dp[Cache::SLIDING])) {
180:                 $meta[self::META_EXPIRE] = $dp[Cache::EXPIRATION] + time(); // absolute time
181:             } else {
182:                 $meta[self::META_DELTA] = (int) $dp[Cache::EXPIRATION]; // sliding time
183:             }
184:         }
185: 
186:         if (isset($dp[Cache::ITEMS])) {
187:             foreach ((array) $dp[Cache::ITEMS] as $item) {
188:                 $depFile = $this->getCacheFile($item);
189:                 $m = $this->readMetaAndLock($depFile, LOCK_SH);
190:                 $meta[self::META_ITEMS][$depFile] = $m[self::META_TIME]; // may be NULL
191:                 unset($m);
192:             }
193:         }
194: 
195:         if (isset($dp[Cache::CALLBACKS])) {
196:             $meta[self::META_CALLBACKS] = $dp[Cache::CALLBACKS];
197:         }
198: 
199:         if (!isset($this->locks[$key])) {
200:             $this->lock($key);
201:             if (!isset($this->locks[$key])) {
202:                 return;
203:             }
204:         }
205:         $handle = $this->locks[$key];
206:         unset($this->locks[$key]);
207: 
208:         $cacheFile = $this->getCacheFile($key);
209: 
210:         if (isset($dp[Cache::TAGS]) || isset($dp[Cache::PRIORITY])) {
211:             if (!$this->journal) {
212:                 throw new InvalidStateException('CacheJournal has not been provided.');
213:             }
214:             $this->journal->write($cacheFile, $dp);
215:         }
216: 
217:         ftruncate($handle, 0);
218: 
219:         if (!is_string($data)) {
220:             $data = serialize($data);
221:             $meta[self::META_SERIALIZED] = TRUE;
222:         }
223: 
224:         $head = serialize($meta) . '?>';
225:         $head = '<?php //netteCache[01]' . str_pad((string) strlen($head), 6, '0', STR_PAD_LEFT) . $head;
226:         $headLen = strlen($head);
227:         $dataLen = strlen($data);
228: 
229:         do {
230:             if (fwrite($handle, str_repeat("\x00", $headLen), $headLen) !== $headLen) {
231:                 break;
232:             }
233: 
234:             if (fwrite($handle, $data, $dataLen) !== $dataLen) {
235:                 break;
236:             }
237: 
238:             fseek($handle, 0);
239:             if (fwrite($handle, $head, $headLen) !== $headLen) {
240:                 break;
241:             }
242: 
243:             flock($handle, LOCK_UN);
244:             fclose($handle);
245:             return;
246:         } while (FALSE);
247: 
248:         $this->delete($cacheFile, $handle);
249:     }
250: 
251: 
252:     /**
253:      * Removes item from the cache.
254:      * @param  string key
255:      * @return void
256:      */
257:     public function remove($key)
258:     {
259:         unset($this->locks[$key]);
260:         $this->delete($this->getCacheFile($key));
261:     }
262: 
263: 
264:     /**
265:      * Removes items from the cache by conditions & garbage collector.
266:      * @param  array  conditions
267:      * @return void
268:      */
269:     public function clean(array $conditions)
270:     {
271:         $all = !empty($conditions[Cache::ALL]);
272:         $collector = empty($conditions);
273: 
274:         // cleaning using file iterator
275:         if ($all || $collector) {
276:             $now = time();
277:             foreach (Finder::find('_*')->from($this->dir)->childFirst() as $entry) {
278:                 $path = (string) $entry;
279:                 if ($entry->isDir()) { // collector: remove empty dirs
280:                     @rmdir($path); // @ - removing dirs is not necessary
281:                     continue;
282:                 }
283:                 if ($all) {
284:                     $this->delete($path);
285: 
286:                 } else { // collector
287:                     $meta = $this->readMetaAndLock($path, LOCK_SH);
288:                     if (!$meta) {
289:                         continue;
290:                     }
291: 
292:                     if ((!empty($meta[self::META_DELTA]) && filemtime($meta[self::FILE]) + $meta[self::META_DELTA] < $now)
293:                         || (!empty($meta[self::META_EXPIRE]) && $meta[self::META_EXPIRE] < $now)
294:                     ) {
295:                         $this->delete($path, $meta[self::HANDLE]);
296:                         continue;
297:                     }
298: 
299:                     flock($meta[self::HANDLE], LOCK_UN);
300:                     fclose($meta[self::HANDLE]);
301:                 }
302:             }
303: 
304:             if ($this->journal) {
305:                 $this->journal->clean($conditions);
306:             }
307:             return;
308:         }
309: 
310:         // cleaning using journal
311:         if ($this->journal) {
312:             foreach ($this->journal->clean($conditions) as $file) {
313:                 $this->delete($file);
314:             }
315:         }
316:     }
317: 
318: 
319:     /**
320:      * Reads cache data from disk.
321:      * @param  string  file path
322:      * @param  int     lock mode
323:      * @return array|NULL
324:      */
325:     protected function readMetaAndLock($file, $lock)
326:     {
327:         $handle = @fopen($file, 'r+b'); // @ - file may not exist
328:         if (!$handle) {
329:             return NULL;
330:         }
331: 
332:         flock($handle, $lock);
333: 
334:         $head = stream_get_contents($handle, self::META_HEADER_LEN);
335:         if ($head && strlen($head) === self::META_HEADER_LEN) {
336:             $size = (int) substr($head, -6);
337:             $meta = stream_get_contents($handle, $size, self::META_HEADER_LEN);
338:             $meta = @unserialize($meta); // intentionally @
339:             if (is_array($meta)) {
340:                 fseek($handle, $size + self::META_HEADER_LEN); // needed by PHP < 5.2.6
341:                 $meta[self::FILE] = $file;
342:                 $meta[self::HANDLE] = $handle;
343:                 return $meta;
344:             }
345:         }
346: 
347:         flock($handle, LOCK_UN);
348:         fclose($handle);
349:         return NULL;
350:     }
351: 
352: 
353:     /**
354:      * Reads cache data from disk and closes cache file handle.
355:      * @param  array
356:      * @return mixed
357:      */
358:     protected function readData($meta)
359:     {
360:         $data = stream_get_contents($meta[self::HANDLE]);
361:         flock($meta[self::HANDLE], LOCK_UN);
362:         fclose($meta[self::HANDLE]);
363: 
364:         if (empty($meta[self::META_SERIALIZED])) {
365:             return $data;
366:         } else {
367:             return @unserialize($data); // intentionally @
368:         }
369:     }
370: 
371: 
372:     /**
373:      * Returns file name.
374:      * @param  string
375:      * @return string
376:      */
377:     protected function getCacheFile($key)
378:     {
379:         $file = urlencode($key);
380:         if ($this->useDirs && $a = strrpos($file, '%00')) { // %00 = urlencode(Cache::NAMESPACE_SEPARATOR)
381:             $file = substr_replace($file, '/_', $a, 3);
382:         }
383:         return $this->dir . '/_' . $file;
384:     }
385: 
386: 
387:     /**
388:      * Deletes and closes file.
389:      * @param  string
390:      * @param  resource
391:      * @return void
392:      */
393:     private static function delete($file, $handle = NULL)
394:     {
395:         if (@unlink($file)) { // @ - file may not already exist
396:             if ($handle) {
397:                 flock($handle, LOCK_UN);
398:                 fclose($handle);
399:             }
400:             return;
401:         }
402: 
403:         if (!$handle) {
404:             $handle = @fopen($file, 'r+'); // @ - file may not exist
405:         }
406:         if ($handle) {
407:             flock($handle, LOCK_EX);
408:             ftruncate($handle, 0);
409:             flock($handle, LOCK_UN);
410:             fclose($handle);
411:             @unlink($file); // @ - file may not already exist
412:         }
413:     }
414: 
415: }
416: 
Nette Framework 2.0.11 (for PHP 5.2, un-prefixed) API API documentation generated by ApiGen 2.8.0