PageRenderTime 57ms CodeModel.GetById 18ms RepoModel.GetById 0ms app.codeStats 0ms

/libs/Nette/Caching/FileStorage.php

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