PageRenderTime 39ms CodeModel.GetById 14ms RepoModel.GetById 0ms app.codeStats 0ms

/libs/Nette/Caching/FileStorage.php

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