Namespaces

  • Latte
    • Loaders
    • Macros
    • Runtime
  • Nette
    • Application
      • Responses
      • Routers
      • UI
    • Bridges
      • ApplicationDI
      • ApplicationLatte
      • ApplicationTracy
      • CacheDI
      • CacheLatte
      • DatabaseDI
      • DatabaseTracy
      • DITracy
      • FormsDI
      • FormsLatte
      • Framework
      • HttpDI
      • HttpTracy
      • MailDI
      • ReflectionDI
      • SecurityDI
      • SecurityTracy
    • Caching
      • Storages
    • ComponentModel
    • Database
      • Conventions
      • Drivers
      • Table
    • DI
      • Config
        • Adapters
      • Extensions
    • Forms
      • Controls
      • Rendering
    • Http
    • Iterators
    • Loaders
    • Localization
    • Mail
    • Neon
    • PhpGenerator
    • Reflection
    • Security
    • Tokenizer
    • Utils
  • Tracy
    • Bridges
      • Nette
  • none

Classes

  • DevNullStorage
  • FileStorage
  • MemcachedStorage
  • MemoryStorage
  • NewMemcachedStorage
  • SQLiteJournal
  • SQLiteStorage

Interfaces

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