File.php 13 KB

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