PageRenderTime 49ms CodeModel.GetById 19ms RepoModel.GetById 0ms app.codeStats 0ms

/www/libs/nette-dev/Caching/FileStorage.php

https://github.com/bazo/Mokuji
PHP | 454 lines | 280 code | 81 blank | 93 comment | 69 complexity | 1314a3c5b80c260e90c2c370c107c353 MD5 | raw file
Possible License(s): BSD-3-Clause, MIT
  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. // checks whether directory is writable
  57. $uniq = uniqid('_', TRUE);
  58. umask(0000);
  59. if (!@mkdir("$dir/$uniq", 0777)) { // intentionally @
  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) { // 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 (!empty($dp[Cache::EXPIRE])) {
  133. if (empty($dp[Cache::SLIDING])) {
  134. $meta[self::META_EXPIRE] = $dp[Cache::EXPIRE] + time(); // absolute time
  135. } else {
  136. $meta[self::META_DELTA] = (int) $dp[Cache::EXPIRE]; // sliding time
  137. }
  138. }
  139. if (!empty($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 (!empty($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, TRUE)) {
  154. return;
  155. }
  156. }
  157. $handle = @fopen($cacheFile, 'r+b'); // intentionally @
  158. if (!$handle) {
  159. $handle = fopen($cacheFile, 'wb'); // intentionally @
  160. if (!$handle) {
  161. return;
  162. }
  163. }
  164. if (!empty($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 ($data instanceof Callback || $data instanceof Closure) {
  184. $data = $data->__invoke();
  185. }
  186. if (!is_string($data)) {
  187. $data = serialize($data);
  188. $meta[self::META_SERIALIZED] = TRUE;
  189. }
  190. $head = serialize($meta) . '?>';
  191. $head = '<?php //netteCache[01]' . str_pad((string) strlen($head), 6, '0', STR_PAD_LEFT) . $head;
  192. $headLen = strlen($head);
  193. $dataLen = strlen($data);
  194. do {
  195. if (fwrite($handle, str_repeat("\x00", $headLen), $headLen) !== $headLen) {
  196. break;
  197. }
  198. if (fwrite($handle, $data, $dataLen) !== $dataLen) {
  199. break;
  200. }
  201. fseek($handle, 0);
  202. if (fwrite($handle, $head, $headLen) !== $headLen) {
  203. break;
  204. }
  205. fclose($handle);
  206. return TRUE;
  207. } while (FALSE);
  208. $this->delete($cacheFile, $handle);
  209. }
  210. /**
  211. * Removes item from the cache.
  212. * @param string key
  213. * @return void
  214. */
  215. public function remove($key)
  216. {
  217. $this->delete($this->getCacheFile($key));
  218. }
  219. /**
  220. * Removes items from the cache by conditions & garbage collector.
  221. * @param array conditions
  222. * @return void
  223. */
  224. public function clean(array $conds)
  225. {
  226. $all = !empty($conds[Cache::ALL]);
  227. $collector = empty($conds);
  228. // cleaning using file iterator
  229. if ($all || $collector) {
  230. $now = time();
  231. $base = $this->dir . DIRECTORY_SEPARATOR . 'c';
  232. $iterator = new RecursiveIteratorIterator(new RecursiveDirectoryIterator($this->dir), RecursiveIteratorIterator::CHILD_FIRST);
  233. foreach ($iterator as $entry) {
  234. $path = (string) $entry;
  235. if (strncmp($path, $base, strlen($base))) { // skip files out of cache
  236. continue;
  237. }
  238. if ($entry->isDir()) { // collector: remove empty dirs
  239. @rmdir($path); // intentionally @
  240. continue;
  241. }
  242. if ($all) {
  243. $this->delete($path);
  244. } else { // collector
  245. $meta = $this->readMeta($path, LOCK_SH);
  246. if (!$meta) continue;
  247. if (!empty($meta[self::META_EXPIRE]) && $meta[self::META_EXPIRE] < $now) {
  248. $this->delete($path, $meta[self::HANDLE]);
  249. continue;
  250. }
  251. fclose($meta[self::HANDLE]);
  252. }
  253. }
  254. if ($all && extension_loaded('sqlite')) {
  255. sqlite_exec("DELETE FROM cache", $this->getDb());
  256. }
  257. return;
  258. }
  259. // cleaning using journal
  260. if (!empty($conds[Cache::TAGS])) {
  261. $db = $this->getDb();
  262. foreach ((array) $conds[Cache::TAGS] as $tag) {
  263. $tmp[] = "'" . sqlite_escape_string($tag) . "'";
  264. }
  265. $query[] = "tag IN (" . implode(',', $tmp) . ")";
  266. }
  267. if (isset($conds[Cache::PRIORITY])) {
  268. $query[] = "priority <= " . (int) $conds[Cache::PRIORITY];
  269. }
  270. if (isset($query)) {
  271. $db = $this->getDb();
  272. $query = implode(' OR ', $query);
  273. $files = sqlite_single_query("SELECT file FROM cache WHERE $query", $db, FALSE);
  274. foreach ($files as $file) {
  275. $this->delete($file);
  276. }
  277. sqlite_exec("DELETE FROM cache WHERE $query", $db);
  278. }
  279. }
  280. /**
  281. * Reads cache data from disk.
  282. * @param string file path
  283. * @param int lock mode
  284. * @return array|NULL
  285. */
  286. protected function readMeta($file, $lock)
  287. {
  288. $handle = @fopen($file, 'r+b'); // intentionally @
  289. if (!$handle) return NULL;
  290. flock($handle, $lock);
  291. $head = stream_get_contents($handle, self::META_HEADER_LEN);
  292. if ($head && strlen($head) === self::META_HEADER_LEN) {
  293. $size = (int) substr($head, -6);
  294. $meta = stream_get_contents($handle, $size, self::META_HEADER_LEN);
  295. $meta = @unserialize($meta); // intentionally @
  296. if (is_array($meta)) {
  297. fseek($handle, $size + self::META_HEADER_LEN); // needed by PHP < 5.2.6
  298. $meta[self::FILE] = $file;
  299. $meta[self::HANDLE] = $handle;
  300. return $meta;
  301. }
  302. }
  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. fclose($meta[self::HANDLE]);
  315. if (empty($meta[self::META_SERIALIZED])) {
  316. return $data;
  317. } else {
  318. return @unserialize($data); // intentionally @
  319. }
  320. }
  321. /**
  322. * Returns file name.
  323. * @param string
  324. * @return string
  325. */
  326. protected function getCacheFile($key)
  327. {
  328. if ($this->useDirs) {
  329. $key = explode(Cache::NAMESPACE_SEPARATOR, $key, 2);
  330. return $this->dir . '/c' . (isset($key[1]) ? '-' . urlencode($key[0]) . '/_' . urlencode($key[1]) : '_' . urlencode($key[0]));
  331. } else {
  332. return $this->dir . '/c_' . urlencode($key);
  333. }
  334. }
  335. /**
  336. * Deletes and closes file.
  337. * @param string
  338. * @param resource
  339. * @return void
  340. */
  341. private static function delete($file, $handle = NULL)
  342. {
  343. if (@unlink($file)) { // intentionally @
  344. if ($handle) fclose($handle);
  345. return;
  346. }
  347. if (!$handle) {
  348. $handle = @fopen($file, 'r+'); // intentionally @
  349. }
  350. if ($handle) {
  351. flock($handle, LOCK_EX);
  352. ftruncate($handle, 0);
  353. fclose($handle);
  354. @unlink($file); // intentionally @; not atomic
  355. }
  356. }
  357. /**
  358. * Returns SQLite resource.
  359. * @return resource
  360. */
  361. protected function getDb()
  362. {
  363. if ($this->db === NULL) {
  364. if (!extension_loaded('sqlite')) {
  365. throw new InvalidStateException("SQLite extension is required for storing tags and priorities.");
  366. }
  367. $this->db = sqlite_open($this->dir . '/cachejournal.sdb');
  368. @sqlite_exec($this->db, 'CREATE TABLE cache (file VARCHAR NOT NULL, priority, tag VARCHAR);
  369. CREATE INDEX IDX_FILE ON cache (file); CREATE INDEX IDX_PRI ON cache (priority); CREATE INDEX IDX_TAG ON cache (tag);'); // intentionally @
  370. }
  371. return $this->db;
  372. }
  373. }