PageRenderTime 53ms CodeModel.GetById 21ms RepoModel.GetById 0ms app.codeStats 0ms

/Nette/Caching/FileStorage.php

https://github.com/DocX/nette
PHP | 411 lines | 234 code | 82 blank | 95 comment | 56 complexity | 28001cbe704c0d96a34ce7544f810e2d MD5 | raw file
Possible License(s): BSD-3-Clause
  1. <?php
  2. /**
  3. * Nette Framework
  4. *
  5. * Copyright (c) 2004, 2009 David Grudl (http://davidgrudl.com)
  6. *
  7. * This source file is subject to the "Nette license" that is bundled
  8. * with this package in the file license.txt.
  9. *
  10. * For more information please see http://nettephp.com
  11. *
  12. * @copyright Copyright (c) 2004, 2009 David Grudl
  13. * @license http://nettephp.com/license Nette license
  14. * @link http://nettephp.com
  15. * @category Nette
  16. * @package Nette\Caching
  17. */
  18. /*namespace Nette\Caching;*/
  19. require_once dirname(__FILE__) . '/../Object.php';
  20. require_once dirname(__FILE__) . '/../Caching/ICacheStorage.php';
  21. /**
  22. * Cache file storage.
  23. *
  24. * @author David Grudl
  25. * @copyright Copyright (c) 2004, 2009 David Grudl
  26. * @package Nette\Caching
  27. */
  28. class FileStorage extends /*Nette\*/Object implements ICacheStorage
  29. {
  30. /**
  31. * Atomic thread safe logic:
  32. *
  33. * 1) reading: open(r+b), lock(SH), read
  34. * - delete?: delete*, close
  35. * 2) deleting: open(r+b), delete*, close
  36. * 3) writing: open(r+b || wb), lock(EX), truncate*, write data, write meta, close
  37. *
  38. * delete* = lock(EX), try unlink, if fails (on NTFS) { truncate, close, unlink } else close (on ext3)
  39. */
  40. /**#@+ internal cache file structure */
  41. const META_HEADER_LEN = 28; // 22b signature + 6b meta-struct size + serialized meta-struct + data
  42. // meta structure: array of
  43. const META_TIME = 'time'; // timestamp
  44. const META_SERIALIZED = 'serialized'; // is content serialized?
  45. const META_PRIORITY = 'priority'; // priority
  46. const META_EXPIRE = 'expire'; // expiration timestamp
  47. const META_DELTA = 'delta'; // relative (sliding) expiration
  48. const META_ITEMS = 'di'; // array of dependent items (file => timestamp)
  49. const META_TAGS = 'tags'; // array of tags (tag => [foo])
  50. const META_CALLBACKS = 'callbacks'; // array of callbacks (function, args)
  51. /**#@-*/
  52. /**#@+ additional cache structure */
  53. const FILE = 'file';
  54. const HANDLE = 'handle';
  55. /**#@-*/
  56. /** @var float probability that the clean() routine is started */
  57. public static $gcProbability = 0.001;
  58. /** @var bool */
  59. public static $useDirectories;
  60. /** @var string */
  61. private $dir;
  62. /** @var bool */
  63. private $useDirs;
  64. public function __construct($dir)
  65. {
  66. if (self::$useDirectories === NULL) {
  67. self::$useDirectories = !ini_get('safe_mode');
  68. // checks whether directory is writable
  69. $uniq = uniqid();
  70. umask(0000);
  71. if (!@mkdir("$dir/$uniq", 0777)) { // intentionally @
  72. throw new /*\*/InvalidStateException("Unable to write to directory '$dir'. Make this directory writable.");
  73. }
  74. // tests subdirectory mode
  75. if (!self::$useDirectories && @file_put_contents("$dir/$uniq/_", '') !== FALSE) { // intentionally @
  76. self::$useDirectories = TRUE;
  77. unlink("$dir/$uniq/_");
  78. }
  79. rmdir("$dir/$uniq");
  80. }
  81. $this->dir = $dir;
  82. $this->useDirs = (bool) self::$useDirectories;
  83. if (mt_rand() / mt_getrandmax() < self::$gcProbability) {
  84. $this->clean(array());
  85. }
  86. }
  87. /**
  88. * Read from cache.
  89. * @param string key
  90. * @return mixed|NULL
  91. */
  92. public function read($key)
  93. {
  94. $meta = $this->readMeta($this->getCacheFile($key), LOCK_SH);
  95. if ($meta && $this->verify($meta)) {
  96. return $this->readData($meta); // calls fclose()
  97. } else {
  98. return NULL;
  99. }
  100. }
  101. /**
  102. * Verifies dependencies.
  103. * @param array
  104. * @return bool
  105. */
  106. private function verify($meta)
  107. {
  108. do {
  109. if (!empty($meta[self::META_DELTA])) {
  110. // meta[file] was added by readMeta()
  111. if (filemtime($meta[self::FILE]) + $meta[self::META_DELTA] < time()) break;
  112. touch($meta[self::FILE]);
  113. } elseif (!empty($meta[self::META_EXPIRE]) && $meta[self::META_EXPIRE] < time()) {
  114. break;
  115. }
  116. if (!empty($meta[self::META_CALLBACKS]) && !Cache::checkCallbacks($meta[self::META_CALLBACKS])) {
  117. break;
  118. }
  119. if (!empty($meta[self::META_ITEMS])) {
  120. foreach ($meta[self::META_ITEMS] as $depFile => $time) {
  121. $m = $this->readMeta($depFile, LOCK_SH);
  122. if ($m[self::META_TIME] !== $time) break 2;
  123. if ($m && !$this->verify($m)) break 2;
  124. }
  125. }
  126. return TRUE;
  127. } while (FALSE);
  128. $this->delete($meta[self::HANDLE], $meta[self::FILE]); // meta[handle] & meta[file] was added by readMeta()
  129. return FALSE;
  130. }
  131. /**
  132. * Writes item into the cache.
  133. * @param string key
  134. * @param mixed data
  135. * @param array dependencies
  136. * @return bool TRUE if no problem
  137. */
  138. public function write($key, $data, array $dp)
  139. {
  140. $meta = array(
  141. self::META_TIME => microtime(),
  142. );
  143. if (!is_string($data)) {
  144. $data = serialize($data);
  145. $meta[self::META_SERIALIZED] = TRUE;
  146. }
  147. if (isset($dp[Cache::PRIORITY])) {
  148. $meta[self::META_PRIORITY] = (int) $dp[Cache::PRIORITY];
  149. }
  150. if (!empty($dp[Cache::EXPIRE])) {
  151. if (empty($dp[Cache::SLIDING])) {
  152. $meta[self::META_EXPIRE] = $dp[Cache::EXPIRE] + time(); // absolute time
  153. } else {
  154. $meta[self::META_DELTA] = (int) $dp[Cache::EXPIRE]; // sliding time
  155. }
  156. }
  157. if (!empty($dp[Cache::TAGS])) {
  158. $meta[self::META_TAGS] = array_flip(array_values((array) $dp[Cache::TAGS]));
  159. }
  160. if (!empty($dp[Cache::ITEMS])) {
  161. foreach ((array) $dp[Cache::ITEMS] as $item) {
  162. $depFile = $this->getCacheFile($item);
  163. $m = $this->readMeta($depFile, LOCK_SH);
  164. $meta[self::META_ITEMS][$depFile] = $m[self::META_TIME];
  165. unset($m);
  166. }
  167. }
  168. if (!empty($dp[Cache::CALLBACKS])) {
  169. $meta[self::META_CALLBACKS] = $dp[Cache::CALLBACKS];
  170. }
  171. $cacheFile = $this->getCacheFile($key);
  172. if ($this->useDirs && !is_dir($dir = dirname($cacheFile))) {
  173. umask(0000);
  174. if (!mkdir($dir, 0777, TRUE)) {
  175. return FALSE;
  176. }
  177. }
  178. $handle = @fopen($cacheFile, 'r+b'); // intentionally @
  179. if (!$handle) {
  180. $handle = fopen($cacheFile, 'wb'); // intentionally @
  181. if (!$handle) {
  182. return FALSE;
  183. }
  184. }
  185. flock($handle, LOCK_EX);
  186. ftruncate($handle, 0);
  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. if (fwrite($handle, str_repeat("\x00", $headLen), $headLen) === $headLen) {
  192. if (fwrite($handle, $data, $dataLen) === $dataLen) {
  193. fseek($handle, 0);
  194. if (fwrite($handle, $head, $headLen) === $headLen) {
  195. fclose($handle);
  196. return TRUE;
  197. }
  198. }
  199. }
  200. $this->delete($handle, $cacheFile);
  201. return TRUE;
  202. }
  203. /**
  204. * Removes item from the cache.
  205. * @param string key
  206. * @return bool TRUE if no problem
  207. */
  208. public function remove($key)
  209. {
  210. $cacheFile = $this->getCacheFile($key);
  211. $meta = $this->readMeta($cacheFile, LOCK_EX);
  212. if ($meta) {
  213. $this->delete($meta[self::HANDLE], $cacheFile);
  214. }
  215. return TRUE;
  216. }
  217. /**
  218. * Removes items from the cache by conditions & garbage collector.
  219. * @param array conditions
  220. * @return bool TRUE if no problem
  221. */
  222. public function clean(array $conds)
  223. {
  224. $tags = isset($conds[Cache::TAGS]) ? array_flip((array) $conds[Cache::TAGS]) : array();
  225. $priority = isset($conds[Cache::PRIORITY]) ? $conds[Cache::PRIORITY] : -1;
  226. $all = !empty($conds[Cache::ALL]);
  227. $now = time();
  228. $base = $this->dir . DIRECTORY_SEPARATOR . 'c';
  229. $iterator = new /*\*/RecursiveIteratorIterator(new /*\*/RecursiveDirectoryIterator($this->dir), /*\*/RecursiveIteratorIterator::CHILD_FIRST);
  230. foreach ($iterator as $entry) {
  231. if (strncmp($entry, $base, strlen($base))) {
  232. continue;
  233. }
  234. if ($entry->isDir()) {
  235. @rmdir((string) $entry); // intentionally @
  236. continue;
  237. }
  238. do {
  239. $meta = $this->readMeta((string) $entry, LOCK_SH);
  240. if (!$meta || $all) continue 2;
  241. if (!empty($meta[self::META_EXPIRE]) && $meta[self::META_EXPIRE] < $now) {
  242. break;
  243. }
  244. if (!empty($meta[self::META_PRIORITY]) && $meta[self::META_PRIORITY] <= $priority) {
  245. break;
  246. }
  247. if (!empty($meta[self::META_TAGS]) && array_intersect_key($tags, $meta[self::META_TAGS])) {
  248. break;
  249. }
  250. fclose($meta[self::HANDLE]);
  251. continue 2;
  252. } while (FALSE);
  253. $this->delete($meta[self::HANDLE], (string) $entry);
  254. }
  255. return TRUE;
  256. }
  257. /**
  258. * Reads cache data from disk.
  259. * @param string file path
  260. * @param int lock mode
  261. * @return array|NULL
  262. */
  263. protected function readMeta($file, $lock)
  264. {
  265. $handle = @fopen($file, 'r+b'); // intentionally @
  266. if (!$handle) return NULL;
  267. flock($handle, $lock);
  268. $head = stream_get_contents($handle, self::META_HEADER_LEN);
  269. if ($head && strlen($head) === self::META_HEADER_LEN) {
  270. $size = (int) substr($head, -6);
  271. $meta = stream_get_contents($handle, $size, self::META_HEADER_LEN);
  272. $meta = @unserialize($meta); // intentionally @
  273. if (is_array($meta)) {
  274. fseek($handle, $size + self::META_HEADER_LEN); // needed by PHP < 5.2.6
  275. $meta[self::FILE] = $file;
  276. $meta[self::HANDLE] = $handle;
  277. return $meta;
  278. }
  279. }
  280. fclose($handle);
  281. return NULL;
  282. }
  283. /**
  284. * Reads cache data from disk and closes cache file handle.
  285. * @param array
  286. * @return mixed
  287. */
  288. protected function readData($meta)
  289. {
  290. $data = stream_get_contents($meta[self::HANDLE]);
  291. fclose($meta[self::HANDLE]);
  292. if (empty($meta[self::META_SERIALIZED])) {
  293. return $data;
  294. } else {
  295. return @unserialize($data); // intentionally @
  296. }
  297. }
  298. /**
  299. * Returns file name.
  300. * @param string
  301. * @return string
  302. */
  303. protected function getCacheFile($key)
  304. {
  305. if ($this->useDirs) {
  306. $key = explode(Cache::NAMESPACE_SEPARATOR, $key, 2);
  307. return $this->dir . '/c' . (isset($key[1]) ? '-' . urlencode($key[0]) . '/_' . urlencode($key[1]) : '_' . urlencode($key[0]));
  308. } else {
  309. return $this->dir . '/c_' . urlencode($key);
  310. }
  311. }
  312. /**
  313. * Deletes and closes file.
  314. * @param resource
  315. * @param string
  316. * @return void
  317. */
  318. private static function delete($handle, $file)
  319. {
  320. if (@unlink($file)) { // intentionally @
  321. fclose($handle);
  322. } else {
  323. flock($handle, LOCK_EX);
  324. ftruncate($handle, 0);
  325. fclose($handle);
  326. @unlink($file); // intentionally @; not atomic
  327. }
  328. }
  329. }