1: <?php
  2: 
  3:   4:   5:   6: 
  7: 
  8: namespace Nette\Caching\Storages;
  9: 
 10: use Nette;
 11: use Nette\Caching\Cache;
 12: 
 13: 
 14:  15:  16: 
 17: class FileStorage extends Nette\Object implements Nette\Caching\IStorage
 18: {
 19:      20:  21:  22:  23:  24:  25:  26:  27:  28: 
 29: 
 30:     
 31:     const  = 28, 
 32:     
 33:         META_TIME = 'time', 
 34:         META_SERIALIZED = 'serialized', 
 35:         META_EXPIRE = 'expire', 
 36:         META_DELTA = 'delta', 
 37:         META_ITEMS = 'di', 
 38:         META_CALLBACKS = 'callbacks'; 
 39: 
 40:     
 41:     const FILE = 'file',
 42:         HANDLE = 'handle';
 43: 
 44: 
 45:     
 46:     public static $gcProbability = 0.001;
 47: 
 48:     
 49:     public static $useDirectories = TRUE;
 50: 
 51:     
 52:     private $dir;
 53: 
 54:     
 55:     private $useDirs;
 56: 
 57:     
 58:     private $journal;
 59: 
 60:     
 61:     private $locks;
 62: 
 63: 
 64:     public function __construct($dir, IJournal $journal = NULL)
 65:     {
 66:         if (!is_dir($dir)) {
 67:             throw new Nette\DirectoryNotFoundException("Directory '$dir' not found.");
 68:         }
 69: 
 70:         $this->dir = $dir;
 71:         $this->useDirs = (bool) static::$useDirectories;
 72:         $this->journal = $journal;
 73: 
 74:         if (mt_rand() / mt_getrandmax() < static::$gcProbability) {
 75:             $this->clean(array());
 76:         }
 77:     }
 78: 
 79: 
 80:      81:  82:  83:  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); 
 90: 
 91:         } else {
 92:             return NULL;
 93:         }
 94:     }
 95: 
 96: 
 97:      98:  99: 100: 101: 
102:     private function verify($meta)
103:     {
104:         do {
105:             if (!empty($meta[self::META_DELTA])) {
106:                 
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]); 
133:         return FALSE;
134:     }
135: 
136: 
137:     138: 139: 140: 141: 
142:     public function lock($key)
143:     {
144:         $cacheFile = $this->getCacheFile($key);
145:         if ($this->useDirs && !is_dir($dir = dirname($cacheFile))) {
146:             @mkdir($dir); 
147:         }
148:         $handle = fopen($cacheFile, 'c+b');
149:         if ($handle) {
150:             $this->locks[$key] = $handle;
151:             flock($handle, LOCK_EX);
152:         }
153:     }
154: 
155: 
156:     157: 158: 159: 160: 161: 162: 
163:     public function write($key, $data, array $dp)
164:     {
165:         $meta = array(
166:             self::META_TIME => microtime(),
167:         );
168: 
169:         if (isset($dp[Cache::EXPIRATION])) {
170:             if (empty($dp[Cache::SLIDING])) {
171:                 $meta[self::META_EXPIRE] = $dp[Cache::EXPIRATION] + time(); 
172:             } else {
173:                 $meta[self::META_DELTA] = (int) $dp[Cache::EXPIRATION]; 
174:             }
175:         }
176: 
177:         if (isset($dp[Cache::ITEMS])) {
178:             foreach ((array) $dp[Cache::ITEMS] as $item) {
179:                 $depFile = $this->getCacheFile($item);
180:                 $m = $this->readMetaAndLock($depFile, LOCK_SH);
181:                 $meta[self::META_ITEMS][$depFile] = $m[self::META_TIME]; 
182:                 unset($m);
183:             }
184:         }
185: 
186:         if (isset($dp[Cache::CALLBACKS])) {
187:             $meta[self::META_CALLBACKS] = $dp[Cache::CALLBACKS];
188:         }
189: 
190:         if (!isset($this->locks[$key])) {
191:             $this->lock($key);
192:             if (!isset($this->locks[$key])) {
193:                 return;
194:             }
195:         }
196:         $handle = $this->locks[$key];
197:         unset($this->locks[$key]);
198: 
199:         $cacheFile = $this->getCacheFile($key);
200: 
201:         if (isset($dp[Cache::TAGS]) || isset($dp[Cache::PRIORITY])) {
202:             if (!$this->journal) {
203:                 throw new Nette\InvalidStateException('CacheJournal has not been provided.');
204:             }
205:             $this->journal->write($cacheFile, $dp);
206:         }
207: 
208:         ftruncate($handle, 0);
209: 
210:         if (!is_string($data)) {
211:             $data = serialize($data);
212:             $meta[self::META_SERIALIZED] = TRUE;
213:         }
214: 
215:         $head = serialize($meta) . '?>';
216:         $head = '<?php //netteCache[01]' . str_pad((string) strlen($head), 6, '0', STR_PAD_LEFT) . $head;
217:         $headLen = strlen($head);
218:         $dataLen = strlen($data);
219: 
220:         do {
221:             if (fwrite($handle, str_repeat("\x00", $headLen), $headLen) !== $headLen) {
222:                 break;
223:             }
224: 
225:             if (fwrite($handle, $data, $dataLen) !== $dataLen) {
226:                 break;
227:             }
228: 
229:             fseek($handle, 0);
230:             if (fwrite($handle, $head, $headLen) !== $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: 245: 246: 247: 
248:     public function remove($key)
249:     {
250:         unset($this->locks[$key]);
251:         $this->delete($this->getCacheFile($key));
252:     }
253: 
254: 
255:     256: 257: 258: 259: 
260:     public function clean(array $conditions)
261:     {
262:         $all = !empty($conditions[Cache::ALL]);
263:         $collector = empty($conditions);
264: 
265:         
266:         if ($all || $collector) {
267:             $now = time();
268:             foreach (Nette\Utils\Finder::find('_*')->from($this->dir)->childFirst() as $entry) {
269:                 $path = (string) $entry;
270:                 if ($entry->isDir()) { 
271:                     @rmdir($path); 
272:                     continue;
273:                 }
274:                 if ($all) {
275:                     $this->delete($path);
276: 
277:                 } else { 
278:                     $meta = $this->readMetaAndLock($path, LOCK_SH);
279:                     if (!$meta) {
280:                         continue;
281:                     }
282: 
283:                     if ((!empty($meta[self::META_DELTA]) && filemtime($meta[self::FILE]) + $meta[self::META_DELTA] < $now)
284:                         || (!empty($meta[self::META_EXPIRE]) && $meta[self::META_EXPIRE] < $now)
285:                     ) {
286:                         $this->delete($path, $meta[self::HANDLE]);
287:                         continue;
288:                     }
289: 
290:                     flock($meta[self::HANDLE], LOCK_UN);
291:                     fclose($meta[self::HANDLE]);
292:                 }
293:             }
294: 
295:             if ($this->journal) {
296:                 $this->journal->clean($conditions);
297:             }
298:             return;
299:         }
300: 
301:         
302:         if ($this->journal) {
303:             foreach ($this->journal->clean($conditions) as $file) {
304:                 $this->delete($file);
305:             }
306:         }
307:     }
308: 
309: 
310:     311: 312: 313: 314: 315: 
316:     protected function readMetaAndLock($file, $lock)
317:     {
318:         $handle = @fopen($file, 'r+b'); 
319:         if (!$handle) {
320:             return NULL;
321:         }
322: 
323:         flock($handle, $lock);
324: 
325:         $head = stream_get_contents($handle, self::META_HEADER_LEN);
326:         if ($head && strlen($head) === self::META_HEADER_LEN) {
327:             $size = (int) substr($head, -6);
328:             $meta = stream_get_contents($handle, $size, self::META_HEADER_LEN);
329:             $meta = @unserialize($meta); 
330:             if (is_array($meta)) {
331:                 $meta[self::FILE] = $file;
332:                 $meta[self::HANDLE] = $handle;
333:                 return $meta;
334:             }
335:         }
336: 
337:         flock($handle, LOCK_UN);
338:         fclose($handle);
339:         return NULL;
340:     }
341: 
342: 
343:     344: 345: 346: 347: 
348:     protected function readData($meta)
349:     {
350:         $data = stream_get_contents($meta[self::HANDLE]);
351:         flock($meta[self::HANDLE], LOCK_UN);
352:         fclose($meta[self::HANDLE]);
353: 
354:         if (empty($meta[self::META_SERIALIZED])) {
355:             return $data;
356:         } else {
357:             return @unserialize($data); 
358:         }
359:     }
360: 
361: 
362:     363: 364: 365: 366: 
367:     protected function getCacheFile($key)
368:     {
369:         $file = urlencode($key);
370:         if ($this->useDirs && $a = strrpos($file, '%00')) { 
371:             $file = substr_replace($file, '/_', $a, 3);
372:         }
373:         return $this->dir . '/_' . $file;
374:     }
375: 
376: 
377:     378: 379: 380: 381: 382: 
383:     private static function delete($file, $handle = NULL)
384:     {
385:         if (@unlink($file)) { 
386:             if ($handle) {
387:                 flock($handle, LOCK_UN);
388:                 fclose($handle);
389:             }
390:             return;
391:         }
392: 
393:         if (!$handle) {
394:             $handle = @fopen($file, 'r+'); 
395:         }
396:         if ($handle) {
397:             flock($handle, LOCK_EX);
398:             ftruncate($handle, 0);
399:             flock($handle, LOCK_UN);
400:             fclose($handle);
401:             @unlink($file); 
402:         }
403:     }
404: 
405: }
406: