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

Classes

Interfaces

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