File.php 13 KB

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