PageRenderTime 22ms CodeModel.GetById 15ms RepoModel.GetById 0ms app.codeStats 0ms

/libs/Nette/Caching/Storages/FileStorage.php

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