File.php 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485
  1. <?php
  2. /**
  3. * [Kohana Cache](api/Kohana_Cache) File driver. Provides a file based
  4. * driver for the Kohana Cache library. This is one of the slowest
  5. * caching methods.
  6. *
  7. * ### Configuration example
  8. *
  9. * Below is an example of a _file_ server configuration.
  10. *
  11. * return array(
  12. * 'file' => array( // File driver group
  13. * 'driver' => 'file', // using File driver
  14. * 'cache_dir' => APPPATH.'cache/.kohana_cache', // Cache location
  15. * ),
  16. * )
  17. *
  18. * In cases where only one cache group is required, if the group is named `default` there is
  19. * no need to pass the group name when instantiating a cache instance.
  20. *
  21. * #### General cache group configuration settings
  22. *
  23. * Below are the settings available to all types of cache driver.
  24. *
  25. * Name | Required | Description
  26. * -------------- | -------- | ---------------------------------------------------------------
  27. * driver | __YES__ | (_string_) The driver type to use
  28. * cache_dir | __NO__ | (_string_) The cache directory to use for this cache instance
  29. *
  30. * ### System requirements
  31. *
  32. * * Kohana 3.0.x
  33. * * PHP 5.2.4 or greater
  34. *
  35. * @package Kohana/Cache
  36. * @category Base
  37. * @author Kohana Team
  38. * @copyright (c) Kohana Team
  39. * @license https://koseven.ga/LICENSE.md
  40. */
  41. class Kohana_Cache_File extends Cache implements Cache_GarbageCollect {
  42. /**
  43. * Creates a hashed filename based on the string. This is used
  44. * to create shorter unique IDs for each cache filename.
  45. *
  46. * // Create the cache filename
  47. * $filename = Cache_File::filename($this->_sanitize_id($id));
  48. *
  49. * @param string $string string to hash into filename
  50. * @return string
  51. */
  52. protected static function filename($string)
  53. {
  54. return sha1($string).'.cache';
  55. }
  56. /**
  57. * @var string the caching directory
  58. */
  59. protected $_cache_dir;
  60. /**
  61. * Constructs the file cache driver. This method cannot be invoked externally. The file cache driver must
  62. * be instantiated using the `Cache::instance()` method.
  63. *
  64. * @param array $config config
  65. * @throws Cache_Exception
  66. */
  67. protected function __construct(array $config)
  68. {
  69. // Setup parent
  70. parent::__construct($config);
  71. try
  72. {
  73. $directory = Arr::get($this->_config, 'cache_dir', Kohana::$cache_dir);
  74. $this->_cache_dir = new SplFileInfo($directory);
  75. }
  76. // PHP < 5.3 exception handle
  77. catch (ErrorException $e)
  78. {
  79. $this->_cache_dir = $this->_make_directory($directory, 0777, TRUE);
  80. }
  81. // PHP >= 5.3 exception handle
  82. catch (UnexpectedValueException $e)
  83. {
  84. $this->_cache_dir = $this->_make_directory($directory, 0777, TRUE);
  85. }
  86. // If the defined directory is a file, get outta here
  87. if ($this->_cache_dir->isFile())
  88. {
  89. throw new Cache_Exception('Unable to create cache directory as a file already exists : :resource', array(':resource' => $this->_cache_dir->getRealPath()));
  90. }
  91. // Check the read status of the directory
  92. if ( ! $this->_cache_dir->isReadable())
  93. {
  94. throw new Cache_Exception('Unable to read from the cache directory :resource', array(':resource' => $this->_cache_dir->getRealPath()));
  95. }
  96. // Check the write status of the directory
  97. if ( ! $this->_cache_dir->isWritable())
  98. {
  99. throw new Cache_Exception('Unable to write to the cache directory :resource', array(':resource' => $this->_cache_dir->getRealPath()));
  100. }
  101. }
  102. /**
  103. * Retrieve a cached value entry by id.
  104. *
  105. * // Retrieve cache entry from file group
  106. * $data = Cache::instance('file')->get('foo');
  107. *
  108. * // Retrieve cache entry from file group and return 'bar' if miss
  109. * $data = Cache::instance('file')->get('foo', 'bar');
  110. *
  111. * @param string $id id of cache to entry
  112. * @param string $default default value to return if cache miss
  113. * @return mixed
  114. * @throws Cache_Exception
  115. */
  116. public function get($id, $default = NULL)
  117. {
  118. $filename = Cache_File::filename($this->_sanitize_id($id));
  119. $directory = $this->_resolve_directory($filename);
  120. // Wrap operations in try/catch to handle notices
  121. try
  122. {
  123. // Open file
  124. $file = new SplFileInfo($directory.$filename);
  125. // If file does not exist
  126. if ( ! $file->isFile())
  127. {
  128. // Return default value
  129. return $default;
  130. }
  131. else
  132. {
  133. // Test the expiry
  134. if ($this->_is_expired($file))
  135. {
  136. // Delete the file
  137. $this->_delete_file($file, FALSE, TRUE);
  138. return $default;
  139. }
  140. // open the file to read data
  141. $data = $file->openFile();
  142. // Run first fgets(). Cache data starts from the second line
  143. // as the first contains the lifetime timestamp
  144. $data->fgets();
  145. $cache = '';
  146. while ($data->eof() === FALSE)
  147. {
  148. $cache .= $data->fgets();
  149. }
  150. return unserialize($cache);
  151. }
  152. }
  153. catch (ErrorException $e)
  154. {
  155. // Handle ErrorException caused by failed unserialization
  156. if ($e->getCode() === E_NOTICE)
  157. {
  158. throw new Cache_Exception(__METHOD__.' failed to unserialize cached object with message : '.$e->getMessage());
  159. }
  160. // Otherwise throw the exception
  161. throw $e;
  162. }
  163. }
  164. /**
  165. * Set a value to cache with id and lifetime
  166. *
  167. * $data = 'bar';
  168. *
  169. * // Set 'bar' to 'foo' in file group, using default expiry
  170. * Cache::instance('file')->set('foo', $data);
  171. *
  172. * // Set 'bar' to 'foo' in file group for 30 seconds
  173. * Cache::instance('file')->set('foo', $data, 30);
  174. *
  175. * @param string $id id of cache entry
  176. * @param string $data data to set to cache
  177. * @param integer $lifetime lifetime in seconds
  178. * @return boolean
  179. */
  180. public function set($id, $data, $lifetime = NULL)
  181. {
  182. $filename = Cache_File::filename($this->_sanitize_id($id));
  183. $directory = $this->_resolve_directory($filename);
  184. // If lifetime is NULL
  185. if ($lifetime === NULL)
  186. {
  187. // Set to the default expiry
  188. $lifetime = Arr::get($this->_config, 'default_expire', Cache::DEFAULT_EXPIRE);
  189. }
  190. // Open directory
  191. $dir = new SplFileInfo($directory);
  192. // If the directory path is not a directory
  193. if ( ! $dir->isDir())
  194. {
  195. $this->_make_directory($directory, 0777, TRUE);
  196. }
  197. // Open file to inspect
  198. $resouce = new SplFileInfo($directory.$filename);
  199. $file = $resouce->openFile('w');
  200. try
  201. {
  202. $data = $lifetime."\n".serialize($data);
  203. $file->fwrite($data, strlen($data));
  204. return (bool) $file->fflush();
  205. }
  206. catch (ErrorException $e)
  207. {
  208. // If serialize through an error exception
  209. if ($e->getCode() === E_NOTICE)
  210. {
  211. // Throw a caching error
  212. throw new Cache_Exception(__METHOD__.' failed to serialize data for caching with message : '.$e->getMessage());
  213. }
  214. // Else rethrow the error exception
  215. throw $e;
  216. }
  217. }
  218. /**
  219. * Delete a cache entry based on id
  220. *
  221. * // Delete 'foo' entry from the file group
  222. * Cache::instance('file')->delete('foo');
  223. *
  224. * @param string $id id to remove from cache
  225. * @return boolean
  226. */
  227. public function delete($id)
  228. {
  229. $filename = Cache_File::filename($this->_sanitize_id($id));
  230. $directory = $this->_resolve_directory($filename);
  231. return $this->_delete_file(new SplFileInfo($directory.$filename), FALSE, TRUE);
  232. }
  233. /**
  234. * Delete all cache entries.
  235. *
  236. * Beware of using this method when
  237. * using shared memory cache systems, as it will wipe every
  238. * entry within the system for all clients.
  239. *
  240. * // Delete all cache entries in the file group
  241. * Cache::instance('file')->delete_all();
  242. *
  243. * @return boolean
  244. */
  245. public function delete_all()
  246. {
  247. return $this->_delete_file($this->_cache_dir, TRUE);
  248. }
  249. /**
  250. * Garbage collection method that cleans any expired
  251. * cache entries from the cache.
  252. *
  253. * @return void
  254. */
  255. public function garbage_collect()
  256. {
  257. $this->_delete_file($this->_cache_dir, TRUE, FALSE, TRUE);
  258. return;
  259. }
  260. /**
  261. * Deletes files recursively and returns FALSE on any errors
  262. *
  263. * // Delete a file or folder whilst retaining parent directory and ignore all errors
  264. * $this->_delete_file($folder, TRUE, TRUE);
  265. *
  266. * @param SplFileInfo $file file
  267. * @param boolean $retain_parent_directory retain the parent directory
  268. * @param boolean $ignore_errors ignore_errors to prevent all exceptions interrupting exec
  269. * @param boolean $only_expired only expired files
  270. * @return boolean
  271. * @throws Cache_Exception
  272. */
  273. protected function _delete_file(SplFileInfo $file, $retain_parent_directory = FALSE, $ignore_errors = FALSE, $only_expired = FALSE)
  274. {
  275. // Allow graceful error handling
  276. try
  277. {
  278. // If is file
  279. if ($file->isFile())
  280. {
  281. try
  282. {
  283. // Handle ignore files
  284. if (in_array($file->getFilename(), $this->config('ignore_on_delete')))
  285. {
  286. $delete = FALSE;
  287. }
  288. // If only expired is not set
  289. elseif ($only_expired === FALSE)
  290. {
  291. // We want to delete the file
  292. $delete = TRUE;
  293. }
  294. // Otherwise...
  295. else
  296. {
  297. // Assess the file expiry to flag it for deletion
  298. $delete = $this->_is_expired($file);
  299. }
  300. // If the delete flag is set delete file
  301. if ($delete === TRUE)
  302. return unlink($file->getRealPath());
  303. else
  304. return FALSE;
  305. }
  306. catch (ErrorException $e)
  307. {
  308. // Catch any delete file warnings
  309. if ($e->getCode() === E_WARNING)
  310. {
  311. throw new Cache_Exception(__METHOD__.' failed to delete file : :file', array(':file' => $file->getRealPath()));
  312. }
  313. }
  314. }
  315. // Else, is directory
  316. elseif ($file->isDir())
  317. {
  318. // Create new DirectoryIterator
  319. $files = new DirectoryIterator($file->getPathname());
  320. // Iterate over each entry
  321. while ($files->valid())
  322. {
  323. // Extract the entry name
  324. $name = $files->getFilename();
  325. // If the name is not a dot
  326. if ($name != '.' AND $name != '..')
  327. {
  328. // Create new file resource
  329. $fp = new SplFileInfo($files->getRealPath());
  330. // Delete the file
  331. $this->_delete_file($fp, $retain_parent_directory, $ignore_errors, $only_expired);
  332. }
  333. // Move the file pointer on
  334. $files->next();
  335. }
  336. // If set to retain parent directory, return now
  337. if ($retain_parent_directory)
  338. {
  339. return TRUE;
  340. }
  341. try
  342. {
  343. // Remove the files iterator
  344. // (fixes Windows PHP which has permission issues with open iterators)
  345. unset($files);
  346. // Try to remove the parent directory
  347. return rmdir($file->getRealPath());
  348. }
  349. catch (ErrorException $e)
  350. {
  351. // Catch any delete directory warnings
  352. if ($e->getCode() === E_WARNING)
  353. {
  354. throw new Cache_Exception(__METHOD__.' failed to delete directory : :directory', array(':directory' => $file->getRealPath()));
  355. }
  356. throw $e;
  357. }
  358. }
  359. else
  360. {
  361. // We get here if a file has already been deleted
  362. return FALSE;
  363. }
  364. }
  365. // Catch all exceptions
  366. catch (Exception $e)
  367. {
  368. // If ignore_errors is on
  369. if ($ignore_errors === TRUE)
  370. {
  371. // Return
  372. return FALSE;
  373. }
  374. // Throw exception
  375. throw $e;
  376. }
  377. }
  378. /**
  379. * Resolves the cache directory real path from the filename
  380. *
  381. * // Get the realpath of the cache folder
  382. * $realpath = $this->_resolve_directory($filename);
  383. *
  384. * @param string $filename filename to resolve
  385. * @return string
  386. */
  387. protected function _resolve_directory($filename)
  388. {
  389. return $this->_cache_dir->getRealPath().DIRECTORY_SEPARATOR.$filename[0].$filename[1].DIRECTORY_SEPARATOR;
  390. }
  391. /**
  392. * Makes the cache directory if it doesn't exist. Simply a wrapper for
  393. * `mkdir` to ensure DRY principles
  394. *
  395. * @link http://php.net/manual/en/function.mkdir.php
  396. * @param string $directory directory path
  397. * @param integer $mode chmod mode
  398. * @param boolean $recursive allows nested directories creation
  399. * @param resource $context a stream context
  400. * @return SplFileInfo
  401. * @throws Cache_Exception
  402. */
  403. protected function _make_directory($directory, $mode = 0777, $recursive = FALSE, $context = NULL)
  404. {
  405. // call mkdir according to the availability of a passed $context param
  406. $mkdir_result = $context ?
  407. mkdir($directory, $mode, $recursive, $context) :
  408. mkdir($directory, $mode, $recursive);
  409. // throw an exception if unsuccessful
  410. if ( ! $mkdir_result)
  411. {
  412. throw new Cache_Exception('Failed to create the defined cache directory : :directory', array(':directory' => $directory));
  413. }
  414. // chmod to solve potential umask issues
  415. chmod($directory, $mode);
  416. return new SplFileInfo($directory);
  417. }
  418. /**
  419. * Test if cache file is expired
  420. *
  421. * @param SplFileInfo $file the cache file
  422. * @return boolean TRUE if expired false otherwise
  423. */
  424. protected function _is_expired(SplFileInfo $file)
  425. {
  426. // Open the file and parse data
  427. $created = $file->getMTime();
  428. $data = $file->openFile("r");
  429. $lifetime = (int) $data->fgets();
  430. // If we're at the EOF at this point, corrupted!
  431. if ($data->eof())
  432. {
  433. throw new Cache_Exception(__METHOD__ . ' corrupted cache file!');
  434. }
  435. //close file
  436. $data = null;
  437. // test for expiry and return
  438. return (($lifetime !== 0) AND ( ($created + $lifetime) < time()));
  439. }
  440. }