PageRenderTime 61ms CodeModel.GetById 32ms RepoModel.GetById 0ms app.codeStats 0ms

/libs/Nette/Caching/FileStorage.php

https://github.com/martinvenus/W20
PHP | 461 lines | 287 code | 83 blank | 91 comment | 67 complexity | e39d5e8165518d6df43556e9da2f0b5f MD5 | raw file
  1. <?php
  2. /**
  3. * This file is part of the Nette Framework (http://nette.org)
  4. *
  5. * Copyright (c) 2004, 2010 David Grudl (http://davidgrudl.com)
  6. *
  7. * For the full copyright and license information, please view
  8. * the file license.txt that was distributed with this source code.
  9. */
  10. namespace Nette\Caching;
  11. use Nette;
  12. /**
  13. * Cache file storage.
  14. *
  15. * @author David Grudl
  16. */
  17. class FileStorage extends Nette\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. /**#@+ @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. // checks whether directory is writable
  57. $uniq = uniqid('_', TRUE);
  58. umask(0000);
  59. if (!@mkdir("$dir/$uniq", 0777)) { // @ - is escalated to exception
  60. throw new \InvalidStateException("Unable to write to directory '$dir'. Make this directory writable.");
  61. }
  62. // tests subdirectory mode
  63. self::$useDirectories = !ini_get('safe_mode');
  64. if (!self::$useDirectories && @file_put_contents("$dir/$uniq/_", '') !== FALSE) { // @ - error is expected
  65. self::$useDirectories = TRUE;
  66. unlink("$dir/$uniq/_");
  67. }
  68. @rmdir("$dir/$uniq"); // @ - directory may not already exist
  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 (isset($dp[Cache::EXPIRATION])) {
  133. if (empty($dp[Cache::SLIDING])) {
  134. $meta[self::META_EXPIRE] = $dp[Cache::EXPIRATION] + time(); // absolute time
  135. } else {
  136. $meta[self::META_DELTA] = (int) $dp[Cache::EXPIRATION]; // sliding time
  137. }
  138. }
  139. if (isset($dp[Cache::ITEMS])) {
  140. foreach ((array) $dp[Cache::ITEMS] as $item) {
  141. $depFile = $this->getCacheFile($item);
  142. $m = $this->readMeta($depFile, LOCK_SH);
  143. $meta[self::META_ITEMS][$depFile] = $m[self::META_TIME];
  144. unset($m);
  145. }
  146. }
  147. if (isset($dp[Cache::CALLBACKS])) {
  148. $meta[self::META_CALLBACKS] = $dp[Cache::CALLBACKS];
  149. }
  150. $cacheFile = $this->getCacheFile($key);
  151. if ($this->useDirs && !is_dir($dir = dirname($cacheFile))) {
  152. umask(0000);
  153. if (!mkdir($dir, 0777)) {
  154. return;
  155. }
  156. }
  157. $handle = @fopen($cacheFile, 'r+b'); // @ - file may not exist
  158. if (!$handle) {
  159. $handle = fopen($cacheFile, 'wb');
  160. if (!$handle) {
  161. return;
  162. }
  163. }
  164. if (isset($dp[Cache::TAGS]) || isset($dp[Cache::PRIORITY])) {
  165. $db = $this->getDb();
  166. $dbFile = sqlite_escape_string($cacheFile);
  167. $query = '';
  168. if (!empty($dp[Cache::TAGS])) {
  169. foreach ((array) $dp[Cache::TAGS] as $tag) {
  170. $query .= "INSERT INTO cache (file, tag) VALUES ('$dbFile', '" . sqlite_escape_string($tag) . "');";
  171. }
  172. }
  173. if (isset($dp[Cache::PRIORITY])) {
  174. $query .= "INSERT INTO cache (file, priority) VALUES ('$dbFile', '" . (int) $dp[Cache::PRIORITY] . "');";
  175. }
  176. if (!sqlite_exec($db, "BEGIN; DELETE FROM cache WHERE file = '$dbFile'; $query COMMIT;")) {
  177. sqlite_exec($db, "ROLLBACK");
  178. return;
  179. }
  180. }
  181. flock($handle, LOCK_EX);
  182. ftruncate($handle, 0);
  183. if (!is_string($data)) {
  184. $data = serialize($data);
  185. $meta[self::META_SERIALIZED] = TRUE;
  186. }
  187. $head = serialize($meta) . '?>';
  188. $head = '<?php //netteCache[01]' . str_pad((string) strlen($head), 6, '0', STR_PAD_LEFT) . $head;
  189. $headLen = strlen($head);
  190. $dataLen = strlen($data);
  191. do {
  192. if (fwrite($handle, str_repeat("\x00", $headLen), $headLen) !== $headLen) {
  193. break;
  194. }
  195. if (fwrite($handle, $data, $dataLen) !== $dataLen) {
  196. break;
  197. }
  198. fseek($handle, 0);
  199. if (fwrite($handle, $head, $headLen) !== $headLen) {
  200. break;
  201. }
  202. flock($handle, LOCK_UN);
  203. fclose($handle);
  204. return TRUE;
  205. } while (FALSE);
  206. $this->delete($cacheFile, $handle);
  207. }
  208. /**
  209. * Removes item from the cache.
  210. * @param string key
  211. * @return void
  212. */
  213. public function remove($key)
  214. {
  215. $this->delete($this->getCacheFile($key));
  216. }
  217. /**
  218. * Removes items from the cache by conditions & garbage collector.
  219. * @param array conditions
  220. * @return void
  221. */
  222. public function clean(array $conds)
  223. {
  224. $all = !empty($conds[Cache::ALL]);
  225. $collector = empty($conds);
  226. // cleaning using file iterator
  227. if ($all || $collector) {
  228. $now = time();
  229. $base = $this->dir . DIRECTORY_SEPARATOR . 'c';
  230. $iterator = new \RecursiveIteratorIterator(new \RecursiveDirectoryIterator($this->dir), \RecursiveIteratorIterator::CHILD_FIRST);
  231. foreach ($iterator as $entry) {
  232. $path = (string) $entry;
  233. if (strncmp($path, $base, strlen($base))) { // skip files out of cache
  234. continue;
  235. }
  236. if ($entry->isDir()) { // collector: remove empty dirs
  237. @rmdir($path); // @ - removing dirs is not necessary
  238. continue;
  239. }
  240. if ($all) {
  241. $this->delete($path);
  242. } else { // collector
  243. $meta = $this->readMeta($path, LOCK_SH);
  244. if (!$meta) continue;
  245. if (!empty($meta[self::META_EXPIRE]) && $meta[self::META_EXPIRE] < $now) {
  246. $this->delete($path, $meta[self::HANDLE]);
  247. continue;
  248. }
  249. flock($meta[self::HANDLE], LOCK_UN);
  250. fclose($meta[self::HANDLE]);
  251. }
  252. }
  253. if ($all && extension_loaded('sqlite')) {
  254. sqlite_exec("DELETE FROM cache", $this->getDb());
  255. }
  256. return;
  257. }
  258. // cleaning using journal
  259. if (!empty($conds[Cache::TAGS])) {
  260. $db = $this->getDb();
  261. foreach ((array) $conds[Cache::TAGS] as $tag) {
  262. $tmp[] = "'" . sqlite_escape_string($tag) . "'";
  263. }
  264. $query[] = "tag IN (" . implode(',', $tmp) . ")";
  265. }
  266. if (isset($conds[Cache::PRIORITY])) {
  267. $query[] = "priority <= " . (int) $conds[Cache::PRIORITY];
  268. }
  269. if (isset($query)) {
  270. $db = $this->getDb();
  271. $query = implode(' OR ', $query);
  272. $files = sqlite_single_query("SELECT file FROM cache WHERE $query", $db, FALSE);
  273. foreach ($files as $file) {
  274. $this->delete($file);
  275. }
  276. sqlite_exec("DELETE FROM cache WHERE $query", $db);
  277. }
  278. }
  279. /**
  280. * Reads cache data from disk.
  281. * @param string file path
  282. * @param int lock mode
  283. * @return array|NULL
  284. */
  285. protected function readMeta($file, $lock)
  286. {
  287. $handle = @fopen($file, 'r+b'); // @ - file may not exist
  288. if (!$handle) return NULL;
  289. flock($handle, $lock);
  290. $head = stream_get_contents($handle, self::META_HEADER_LEN);
  291. if ($head && strlen($head) === self::META_HEADER_LEN) {
  292. $size = (int) substr($head, -6);
  293. $meta = stream_get_contents($handle, $size, self::META_HEADER_LEN);
  294. $meta = @unserialize($meta); // intentionally @
  295. if (is_array($meta)) {
  296. fseek($handle, $size + self::META_HEADER_LEN); // needed by PHP < 5.2.6
  297. $meta[self::FILE] = $file;
  298. $meta[self::HANDLE] = $handle;
  299. return $meta;
  300. }
  301. }
  302. flock($handle, LOCK_UN);
  303. fclose($handle);
  304. return NULL;
  305. }
  306. /**
  307. * Reads cache data from disk and closes cache file handle.
  308. * @param array
  309. * @return mixed
  310. */
  311. protected function readData($meta)
  312. {
  313. $data = stream_get_contents($meta[self::HANDLE]);
  314. flock($meta[self::HANDLE], LOCK_UN);
  315. fclose($meta[self::HANDLE]);
  316. if (empty($meta[self::META_SERIALIZED])) {
  317. return $data;
  318. } else {
  319. return @unserialize($data); // intentionally @
  320. }
  321. }
  322. /**
  323. * Returns file name.
  324. * @param string
  325. * @return string
  326. */
  327. protected function getCacheFile($key)
  328. {
  329. if ($this->useDirs) {
  330. $key = explode(Cache::NAMESPACE_SEPARATOR, $key, 2);
  331. return $this->dir . '/c' . (isset($key[1]) ? '-' . urlencode($key[0]) . '/_' . urlencode($key[1]) : '_' . urlencode($key[0]));
  332. } else {
  333. return $this->dir . '/c_' . urlencode($key);
  334. }
  335. }
  336. /**
  337. * Deletes and closes file.
  338. * @param string
  339. * @param resource
  340. * @return void
  341. */
  342. private static function delete($file, $handle = NULL)
  343. {
  344. if (@unlink($file)) { // @ - file may not already exist
  345. if ($handle) {
  346. flock($handle, LOCK_UN);
  347. fclose($handle);
  348. }
  349. return;
  350. }
  351. if (!$handle) {
  352. $handle = @fopen($file, 'r+'); // @ - file may not exist
  353. }
  354. if ($handle) {
  355. flock($handle, LOCK_EX);
  356. ftruncate($handle, 0);
  357. flock($handle, LOCK_UN);
  358. fclose($handle);
  359. @unlink($file); // @ - file may not already exist
  360. }
  361. }
  362. /**
  363. * Returns SQLite resource.
  364. * @return resource
  365. */
  366. protected function getDb()
  367. {
  368. if ($this->db === NULL) {
  369. if (!extension_loaded('sqlite')) {
  370. throw new \InvalidStateException("SQLite extension is required for storing tags and priorities.");
  371. }
  372. $this->db = sqlite_open($this->dir . '/cachejournal.sdb');
  373. @sqlite_exec($this->db, 'CREATE TABLE cache (file VARCHAR NOT NULL, priority, tag VARCHAR);
  374. CREATE INDEX IDX_FILE ON cache (file); CREATE INDEX IDX_PRI ON cache (priority); CREATE INDEX IDX_TAG ON cache (tag);'); // @ - table may already exist
  375. }
  376. return $this->db;
  377. }
  378. }