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