Source for file FileStorage.php

Documentation is available at FileStorage.php

  1. 1: <?php
  2. 2:  
  3. 3: /**
  4. 4:  * Nette Framework
  5. 5:  *
  6. 6:  * @copyright  Copyright (c) 2004, 2010 David Grudl
  7. 7:  * @license    http://nettephp.com/license  Nette license
  8. 8:  * @link       http://nettephp.com
  9. 9:  * @category   Nette
  10. 10:  * @package    Nette\Caching
  11. 11:  */
  12. 12:  
  13. 13:  
  14. 14:  
  15. 15: /**
  16. 16:  * Cache file storage.
  17. 17:  *
  18. 18:  * @copyright  Copyright (c) 2004, 2010 David Grudl
  19. 19:  * @package    Nette\Caching
  20. 20:  */
  21. 21: class FileStorage extends Object implements ICacheStorage
  22. 22: {
  23. 23:     /**
  24. 24:      * Atomic thread safe logic:
  25. 25:      *
  26. 26:      * 1) reading: open(r+b), lock(SH), read
  27. 27:      *     - delete?: delete*, close
  28. 28:      * 2) deleting: delete*
  29. 29:      * 3) writing: open(r+b || wb), lock(EX), truncate*, write data, write meta, close
  30. 30:      *
  31. 31:      * delete* = try unlink, if fails (on NTFS) { lock(EX), truncate, close, unlink } else close (on ext3)
  32. 32:      */
  33. 33:  
  34. 34:     /**#@+ @ignore internal cache file structure */
  35. 35:     const META_HEADER_LEN 28// 22b signature + 6b meta-struct size + serialized meta-struct + data
  36. 36:     // meta structure: array of
  37. 37:     const META_TIME 'time'// timestamp
  38. 38:     const META_SERIALIZED 'serialized'// is content serialized?
  39. 39:     const META_EXPIRE 'expire'// expiration timestamp
  40. 40:     const META_DELTA 'delta'// relative (sliding) expiration
  41. 41:     const META_ITEMS 'di'// array of dependent items (file => timestamp)
  42. 42:     const META_CALLBACKS 'callbacks'// array of callbacks (function, args)
  43. 43:     /**#@-*/
  44. 44:  
  45. 45:     /**#@+ additional cache structure */
  46. 46:     const FILE = 'file';
  47. 47:     const HANDLE = 'handle';
  48. 48:     /**#@-*/
  49. 49:  
  50. 50:  
  51. 51:     /** @var float  probability that the clean() routine is started */
  52. 52:     public static $gcProbability 0.001;
  53. 53:  
  54. 54:     /** @var bool */
  55. 55:     public static $useDirectories;
  56. 56:  
  57. 57:     /** @var string */
  58. 58:     private $dir;
  59. 59:  
  60. 60:     /** @var bool */
  61. 61:     private $useDirs;
  62. 62:  
  63. 63:     /** @var resource */
  64. 64:     private $db;
  65. 65:  
  66. 66:  
  67. 67:  
  68. 68:     public function __construct($dir)
  69. 69:     {
  70. 70:         if (self::$useDirectories === NULL{
  71. 71:             // checks whether directory is writable
  72. 72:             $uniq uniqid('_'TRUE);
  73. 73:             umask(0000);
  74. 74:             if (!@mkdir("$dir/$uniq"0777)) // intentionally @
  75. 75:                 throw new InvalidStateException("Unable to write to directory '$dir'. Make this directory writable.");
  76. 76:             }
  77. 77:  
  78. 78:             // tests subdirectory mode
  79. 79:             self::$useDirectories !ini_get('safe_mode');
  80. 80:             if (!self::$useDirectories && @file_put_contents("$dir/$uniq/_"''!== FALSE// intentionally @
  81. 81:                 self::$useDirectories TRUE;
  82. 82:                 unlink("$dir/$uniq/_");
  83. 83:             }
  84. 84:             rmdir("$dir/$uniq");
  85. 85:         }
  86. 86:  
  87. 87:         $this->dir $dir;
  88. 88:         $this->useDirs = (bool) self::$useDirectories;
  89. 89:  
  90. 90:         if (mt_rand(mt_getrandmax(self::$gcProbability{
  91. 91:             $this->clean(array());
  92. 92:         }
  93. 93:     }
  94. 94:  
  95. 95:  
  96. 96:  
  97. 97:     /**
  98. 98:      * Read from cache.
  99. 99:      * @param  string key
  100. 100:      * @return mixed|NULL
  101. 101:      */
  102. 102:     public function read($key)
  103. 103:     {
  104. 104:         $meta $this->readMeta($this->getCacheFile($key)LOCK_SH);
  105. 105:         if ($meta && $this->verify($meta)) {
  106. 106:             return $this->readData($meta)// calls fclose()
  107. 107:  
  108. 108:         else {
  109. 109:             return NULL;
  110. 110:         }
  111. 111:     }
  112. 112:  
  113. 113:  
  114. 114:  
  115. 115:     /**
  116. 116:      * Verifies dependencies.
  117. 117:      * @param  array 
  118. 118:      * @return bool 
  119. 119:      */
  120. 120:     private function verify($meta)
  121. 121:     {
  122. 122:         do {
  123. 123:             if (!empty($meta[self::META_DELTA])) {
  124. 124:                 // meta[file] was added by readMeta()
  125. 125:                 if (filemtime($meta[self::FILE]$meta[self::META_DELTAtime()) break;
  126. 126:                 touch($meta[self::FILE]);
  127. 127:  
  128. 128:             elseif (!empty($meta[self::META_EXPIRE]&& $meta[self::META_EXPIREtime()) {
  129. 129:                 break;
  130. 130:             }
  131. 131:  
  132. 132:             if (!empty($meta[self::META_CALLBACKS]&& !Cache::checkCallbacks($meta[self::META_CALLBACKS])) {
  133. 133:                 break;
  134. 134:             }
  135. 135:  
  136. 136:             if (!empty($meta[self::META_ITEMS])) {
  137. 137:                 foreach ($meta[self::META_ITEMSas $depFile => $time{
  138. 138:                     $m $this->readMeta($depFileLOCK_SH);
  139. 139:                     if ($m[self::META_TIME!== $timebreak 2;
  140. 140:                     if ($m && !$this->verify($m)) break 2;
  141. 141:                 }
  142. 142:             }
  143. 143:  
  144. 144:             return TRUE;
  145. 145:         while (FALSE);
  146. 146:  
  147. 147:         $this->delete($meta[self::FILE]$meta[self::HANDLE])// meta[handle] & meta[file] was added by readMeta()
  148. 148:         return FALSE;
  149. 149:     }
  150. 150:  
  151. 151:  
  152. 152:  
  153. 153:     /**
  154. 154:      * Writes item into the cache.
  155. 155:      * @param  string key
  156. 156:      * @param  mixed  data
  157. 157:      * @param  array  dependencies
  158. 158:      * @return void 
  159. 159:      */
  160. 160:     public function write($key$dataarray $dp)
  161. 161:     {
  162. 162:         $meta array(
  163. 163:             self::META_TIME => microtime(),
  164. 164:         );
  165. 165:  
  166. 166:         if (!empty($dp[Cache::EXPIRE])) {
  167. 167:             if (empty($dp[Cache::SLIDING])) {
  168. 168:                 $meta[self::META_EXPIRE$dp[Cache::EXPIREtime()// absolute time
  169. 169:             else {
  170. 170:                 $meta[self::META_DELTA= (int) $dp[Cache::EXPIRE]// sliding time
  171. 171:             }
  172. 172:         }
  173. 173:  
  174. 174:         if (!empty($dp[Cache::ITEMS])) {
  175. 175:             foreach ((array) $dp[Cache::ITEMSas $item{
  176. 176:                 $depFile $this->getCacheFile($item);
  177. 177:                 $m $this->readMeta($depFileLOCK_SH);
  178. 178:                 $meta[self::META_ITEMS][$depFile$m[self::META_TIME];
  179. 179:                 unset($m);
  180. 180:             }
  181. 181:         }
  182. 182:  
  183. 183:         if (!empty($dp[Cache::CALLBACKS])) {
  184. 184:             $meta[self::META_CALLBACKS$dp[Cache::CALLBACKS];
  185. 185:         }
  186. 186:  
  187. 187:         $cacheFile $this->getCacheFile($key);
  188. 188:         if ($this->useDirs && !is_dir($dir dirname($cacheFile))) {
  189. 189:             umask(0000);
  190. 190:             if (!mkdir($dir0777TRUE)) {
  191. 191:                 return;
  192. 192:             }
  193. 193:         }
  194. 194:         $handle @fopen($cacheFile'r+b')// intentionally @
  195. 195:         if (!$handle{
  196. 196:             $handle fopen($cacheFile'wb')// intentionally @
  197. 197:             if (!$handle{
  198. 198:                 return;
  199. 199:             }
  200. 200:         }
  201. 201:  
  202. 202:         if (!empty($dp[Cache::TAGS]|| isset($dp[Cache::PRIORITY])) {
  203. 203:             $db $this->getDb();
  204. 204:             $dbFile sqlite_escape_string($cacheFile);
  205. 205:             $query '';
  206. 206:             if (!empty($dp[Cache::TAGS])) {
  207. 207:                 foreach ((array) $dp[Cache::TAGSas $tag{
  208. 208:                     $query .= "INSERT INTO cache (file, tag) VALUES ('$dbFile', 'sqlite_escape_string($tag"');";
  209. 209:                 }
  210. 210:             }
  211. 211:             if (isset($dp[Cache::PRIORITY])) {
  212. 212:                 $query .= "INSERT INTO cache (file, priority) VALUES ('$dbFile', '. (int) $dp[Cache::PRIORITY"');";
  213. 213:             }
  214. 214:             if (!sqlite_exec($db"BEGIN; DELETE FROM cache WHERE file = '$dbFile'; $query COMMIT;")) {
  215. 215:                 sqlite_exec($db"ROLLBACK");
  216. 216:                 return;
  217. 217:             }
  218. 218:         }
  219. 219:  
  220. 220:         flock($handleLOCK_EX);
  221. 221:         ftruncate($handle0);
  222. 222:  
  223. 223:         if ($data instanceof Callback || $data instanceof Closure{
  224. 224:             $data $data->__invoke();
  225. 225:         }
  226. 226:         if (!is_string($data)) {
  227. 227:             $data serialize($data);
  228. 228:             $meta[self::META_SERIALIZEDTRUE;
  229. 229:         }
  230. 230:  
  231. 231:         $head serialize($meta'?>';
  232. 232:         $head '<?php //netteCache[01]' str_pad((string) strlen($head)6'0'STR_PAD_LEFT$head;
  233. 233:         $headLen strlen($head);
  234. 234:         $dataLen strlen($data);
  235. 235:  
  236. 236:         do {
  237. 237:             if (fwrite($handlestr_repeat("\x00"$headLen)$headLen!== $headLen{
  238. 238:                 break;
  239. 239:             }
  240. 240:  
  241. 241:             if (fwrite($handle$data$dataLen!== $dataLen{
  242. 242:                 break;
  243. 243:             }
  244. 244:  
  245. 245:             fseek($handle0);
  246. 246:             if (fwrite($handle$head$headLen!== $headLen{
  247. 247:                 break;
  248. 248:             }
  249. 249:  
  250. 250:             fclose($handle);
  251. 251:             return TRUE;
  252. 252:         while (FALSE);
  253. 253:  
  254. 254:         $this->delete($cacheFile$handle);
  255. 255:     }
  256. 256:  
  257. 257:  
  258. 258:  
  259. 259:     /**
  260. 260:      * Removes item from the cache.
  261. 261:      * @param  string key
  262. 262:      * @return void 
  263. 263:      */
  264. 264:     public function remove($key)
  265. 265:     {
  266. 266:         $this->delete($this->getCacheFile($key));
  267. 267:     }
  268. 268:  
  269. 269:  
  270. 270:  
  271. 271:     /**
  272. 272:      * Removes items from the cache by conditions & garbage collector.
  273. 273:      * @param  array  conditions
  274. 274:      * @return void 
  275. 275:      */
  276. 276:     public function clean(array $conds)
  277. 277:     {
  278. 278:         $all !empty($conds[Cache::ALL]);
  279. 279:         $collector empty($conds);
  280. 280:  
  281. 281:         // cleaning using file iterator
  282. 282:         if ($all || $collector{
  283. 283:             $now time();
  284. 284:             $base $this->dir . DIRECTORY_SEPARATOR 'c';
  285. 285:             $iterator new RecursiveIteratorIterator(new RecursiveDirectoryIterator($this->dir)RecursiveIteratorIterator::CHILD_FIRST);
  286. 286:             foreach ($iterator as $entry{
  287. 287:                 $path = (string) $entry;
  288. 288:                 if (strncmp($path$basestrlen($base))) // skip files out of cache
  289. 289:                     continue;
  290. 290:                 }
  291. 291:                 if ($entry->isDir()) // collector: remove empty dirs
  292. 292:                     @rmdir($path)// intentionally @
  293. 293:                     continue;
  294. 294:                 }
  295. 295:                 if ($all{
  296. 296:                     $this->delete($path);
  297. 297:  
  298. 298:                 else // collector
  299. 299:                     $meta $this->readMeta($pathLOCK_SH);
  300. 300:                     if (!$metacontinue;
  301. 301:  
  302. 302:                     if (!empty($meta[self::META_EXPIRE]&& $meta[self::META_EXPIRE$now{
  303. 303:                         $this->delete($path$meta[self::HANDLE]);
  304. 304:                         continue;
  305. 305:                     }
  306. 306:  
  307. 307:                     fclose($meta[self::HANDLE]);
  308. 308:                 }
  309. 309:             }
  310. 310:  
  311. 311:             if ($all && extension_loaded('sqlite')) {
  312. 312:                 sqlite_exec("DELETE FROM cache"$this->getDb());
  313. 313:             }
  314. 314:             return;
  315. 315:         }
  316. 316:  
  317. 317:         // cleaning using journal
  318. 318:         if (!empty($conds[Cache::TAGS])) {
  319. 319:             $db $this->getDb();
  320. 320:             foreach ((array) $conds[Cache::TAGSas $tag{
  321. 321:                 $tmp["'" sqlite_escape_string($tag"'";
  322. 322:             }
  323. 323:             $query["tag IN (" implode(','$tmp")";
  324. 324:         }
  325. 325:  
  326. 326:         if (isset($conds[Cache::PRIORITY])) {
  327. 327:             $query["priority <= " . (int) $conds[Cache::PRIORITY];
  328. 328:         }
  329. 329:  
  330. 330:         if (isset($query)) {
  331. 331:             $db $this->getDb();
  332. 332:             $query implode(' OR '$query);
  333. 333:             $files sqlite_single_query("SELECT file FROM cache WHERE $query"$dbFALSE);
  334. 334:             foreach ($files as $file{
  335. 335:                 $this->delete($file);
  336. 336:             }
  337. 337:             sqlite_exec("DELETE FROM cache WHERE $query"$db);
  338. 338:         }
  339. 339:     }
  340. 340:  
  341. 341:  
  342. 342:  
  343. 343:     /**
  344. 344:      * Reads cache data from disk.
  345. 345:      * @param  string  file path
  346. 346:      * @param  int     lock mode
  347. 347:      * @return array|NULL
  348. 348:      */
  349. 349:     protected function readMeta($file$lock)
  350. 350:     {
  351. 351:         $handle @fopen($file'r+b')// intentionally @
  352. 352:         if (!$handlereturn NULL;
  353. 353:  
  354. 354:         flock($handle$lock);
  355. 355:  
  356. 356:         $head stream_get_contents($handleself::META_HEADER_LEN);
  357. 357:         if ($head && strlen($head=== self::META_HEADER_LEN{
  358. 358:             $size = (int) substr($head-6);
  359. 359:             $meta stream_get_contents($handle$sizeself::META_HEADER_LEN);
  360. 360:             $meta @unserialize($meta)// intentionally @
  361. 361:             if (is_array($meta)) {
  362. 362:                 fseek($handle$size self::META_HEADER_LEN)// needed by PHP < 5.2.6
  363. 363:                 $meta[self::FILE$file;
  364. 364:                 $meta[self::HANDLE$handle;
  365. 365:                 return $meta;
  366. 366:             }
  367. 367:         }
  368. 368:  
  369. 369:         fclose($handle);
  370. 370:         return NULL;
  371. 371:     }
  372. 372:  
  373. 373:  
  374. 374:  
  375. 375:     /**
  376. 376:      * Reads cache data from disk and closes cache file handle.
  377. 377:      * @param  array 
  378. 378:      * @return mixed 
  379. 379:      */
  380. 380:     protected function readData($meta)
  381. 381:     {
  382. 382:         $data stream_get_contents($meta[self::HANDLE]);
  383. 383:         fclose($meta[self::HANDLE]);
  384. 384:  
  385. 385:         if (empty($meta[self::META_SERIALIZED])) {
  386. 386:             return $data;
  387. 387:         else {
  388. 388:             return @unserialize($data)// intentionally @
  389. 389:         }
  390. 390:     }
  391. 391:  
  392. 392:  
  393. 393:  
  394. 394:     /**
  395. 395:      * Returns file name.
  396. 396:      * @param  string 
  397. 397:      * @return string 
  398. 398:      */
  399. 399:     protected function getCacheFile($key)
  400. 400:     {
  401. 401:         if ($this->useDirs{
  402. 402:             $key explode(Cache::NAMESPACE_SEPARATOR$key2);
  403. 403:             return $this->dir . '/c' (isset($key[1]'-' urlencode($key[0]'/_' urlencode($key[1]'_' urlencode($key[0]));
  404. 404:         else {
  405. 405:             return $this->dir . '/c_' urlencode($key);
  406. 406:         }
  407. 407:     }
  408. 408:  
  409. 409:  
  410. 410:  
  411. 411:     /**
  412. 412:      * Deletes and closes file.
  413. 413:      * @param  string 
  414. 414:      * @param  resource 
  415. 415:      * @return void 
  416. 416:      */
  417. 417:     private static function delete($file$handle NULL)
  418. 418:     {
  419. 419:         if (@unlink($file)) // intentionally @
  420. 420:             if ($handlefclose($handle);
  421. 421:             return;
  422. 422:         }
  423. 423:  
  424. 424:         if (!$handle{
  425. 425:             $handle @fopen($file'r+')// intentionally @
  426. 426:         }
  427. 427:         if ($handle{
  428. 428:             flock($handleLOCK_EX);
  429. 429:             ftruncate($handle0);
  430. 430:             fclose($handle);
  431. 431:             @unlink($file)// intentionally @; not atomic
  432. 432:         }
  433. 433:     }
  434. 434:  
  435. 435:  
  436. 436:  
  437. 437:     /**
  438. 438:      * Returns SQLite resource.
  439. 439:      * @return resource 
  440. 440:      */
  441. 441:     protected function getDb()
  442. 442:     {
  443. 443:         if ($this->db === NULL{
  444. 444:             if (!extension_loaded('sqlite')) {
  445. 445:                 throw new InvalidStateException("SQLite extension is required for storing tags and priorities.");
  446. 446:             }
  447. 447:             $this->db sqlite_open($this->dir . '/cachejournal.sdb');
  448. 448:             @sqlite_exec($this->db'CREATE TABLE cache (file VARCHAR NOT NULL, priority, tag VARCHAR);
  449. 449:             CREATE INDEX IDX_FILE ON cache (file); CREATE INDEX IDX_PRI ON cache (priority); CREATE INDEX IDX_TAG ON cache (tag);')// intentionally @
  450. 450:         }
  451. 451:         return $this->db;
  452. 452:     }
  453. 453: