From 50aa44f81f4cf9f952c3c56ffa466172e2c90c8c Mon Sep 17 00:00:00 2001 From: rurban Date: Mon, 19 Aug 2002 06:41:28 +0000 Subject: [PATCH] *** empty log message *** git-svn-id: svn://svn.code.sf.net/p/phpwiki/code/trunk@2258 96ab9672-09ca-45d6-a79d-3d69d39ca109 --- lib/WikiPluginCached.php | 10 +- lib/pear/Cache.php | 356 +++++++++++++++++++++ lib/pear/Cache/Container.php | 455 +++++++++++++++++++++++++++ lib/pear/Cache/Container/file.php | 342 ++++++++++++++++++++ lib/pear/Cache/Container/imgfile.php | 382 ++++++++++++++++++++++ lib/pear/Cache/Error.php | 51 +++ 6 files changed, 1591 insertions(+), 5 deletions(-) create mode 100644 lib/pear/Cache.php create mode 100644 lib/pear/Cache/Container.php create mode 100644 lib/pear/Cache/Container/file.php create mode 100644 lib/pear/Cache/Container/imgfile.php create mode 100644 lib/pear/Cache/Error.php diff --git a/lib/WikiPluginCached.php b/lib/WikiPluginCached.php index bfda65cca..af2045fdf 100644 --- a/lib/WikiPluginCached.php +++ b/lib/WikiPluginCached.php @@ -1,4 +1,4 @@ - | +// | Sebastian Bergmann | +// +----------------------------------------------------------------------+ +// +// $Id: Cache.php,v 1.1 2002-08-19 06:41:28 rurban Exp $ + +require_once('lib/pear/PEAR.php'); +require_once('lib/pear/Cache/Error.php'); + +/** +* Cache is a base class for cache implementations. +* +* The pear cache module is a generic data cache which can be used to +* cache script runs. The idea behind the cache is quite simple. If you have +* the same input parameters for whatever tasks/algorithm you use you'll +* usually get the same output. So why not caching templates, functions calls, +* graphic generation etc. Caching certain actions e.g. XSLT tranformations +* saves you lots of time. +* +* The design of the cache reminds of PHPLibs session implementation. A +* (PHPLib: session) controller uses storage container (PHPLib: ct_*.inc) to save +* certain data (PHPLib: session data). In contrast to the session stuff it's up to +* you to generate an ID for the data to cache. If you're using the output cache +* you might use the script name as a seed for cache::generateID(), if your using the +* function cache you'd use an array with all function parameters. +* +* Usage example of the generic data cache: +* +* require_once('Cache.php'); +* +* $cache = new Cache('file', array('cache_dir' => 'cache/') ); +* $id = $cache->generateID('testentry'); +* +* if ($data = $cache->get($id)) { +* print "Cache hit.
Data: $data"; +* +* } else { +* $data = 'data of any kind'; +* $cache->save($id, $data); +* print 'Cache miss.
'; +* } +* +* WARNING: No File/DB-Table-Row locking is implemented yet, +* it's possible, that you get corrupted data-entries under +* bad circumstances (especially with the file container) +* +* @author Ulf Wendel +* @version $Id: Cache.php,v 1.1 2002-08-19 06:41:28 rurban Exp $ +* @package Cache +* @access public +*/ +class Cache extends PEAR { + + /** + * Enables / disables caching. + * + * TODO: Add explanation what this is good for. + * + * @var boolean + * @access private + */ + var $caching = true; + + /** + * Garbage collection: probability in seconds + * + * If set to a value above 0 a garbage collection will + * flush all cache entries older than the specified number + * of seconds. + * + * @var integer + * @see $gc_probability, $gc_maxlifetime + * @access public + */ + var $gc_time = 1; + + /** + * Garbage collection: probability in percent + * + * TODO: Add an explanation. + * + * @var integer 0 => never + * @see $gc_time, $gc_maxlifetime + * @access public + */ + var $gc_probability = 1; + + /** + * Garbage collection: delete all entries not use for n seconds. + * + * Default is one day, 60 * 60 * 24 = 86400 seconds. + * + * @var integer + * @see $gc_probability, $gc_time + */ + var $gc_maxlifetime = 86400; + + /** + * Storage container object. + * + * @var object Cache_Container + */ + var $container; + + // + // public methods + // + + /** + * + * @param string Name of container class + * @param array Array with container class options + */ + function Cache($container, $container_options = '') + { + $this->PEAR(); + $container = strtolower($container); + $container_class = 'Cache_Container_' . $container; + $container_classfile = 'lib/pear/Cache/Container/' . $container . '.php'; + + include_once $container_classfile; + $this->container = new $container_class($container_options); + } + + //deconstructor + function _Cache() + { + $this->garbageCollection(); + } + + /** + * Returns the current caching state. + * + * @return boolean The current caching state. + * @access public + */ + function getCaching() + { + return ($this->caching); + } + + /** + * Enables or disables caching. + * + * @param boolean The new caching state. + * @access public + */ + function setCaching($state) + { + $this->caching = $state; + } + + /** + * Returns the requested dataset it if exists and is not expired + * + * @param string dataset ID + * @param string cache group + * @return mixed cached data or NULL on failure + * @access public + */ + function get($id, $group = 'default') { + if (!$this->caching) + return ''; + + if ($this->isCached($id, $group) && !$this->isExpired($id, $group)) + return $this->load($id, $group); + + return NULL; + } // end func get + + /** + * Stores the given data in the cache. + * + * @param string dataset ID used as cache identifier + * @param mixed data to cache + * @param integer lifetime of the cached data in seconds - 0 for endless + * @param string cache group + * @return boolean + * @access public + */ + function save($id, $data, $expires = 0, $group = 'default') { + if (!$this->caching) + return true; + + return $this->extSave($id, $data, '',$expires, $group); + } // end func save + + /** + * Stores a dataset without additional userdefined data. + * + * @param string dataset ID + * @param mixed data to store + * @param string additional userdefined data + * @param mixed userdefined expire date + * @param string cache group + * @return boolean + * @throws Cache_Error + * @access public + * @see getUserdata() + */ + function extSave($id, $cachedata, $userdata, $expires = 0, $group = 'default') { + if (!$this->caching) + return true; + + return $this->container->save($id, $cachedata, $expires, $group, $userdata); + } // end func extSave + + /** + * Loads the given ID from the cache. + * + * @param string dataset ID + * @param string cache group + * @return mixed cached data or NULL on failure + * @access public + */ + function load($id, $group = 'default') { + if (!$this->caching) + return ''; + + return $this->container->load($id, $group); + } // end func load + + /** + * Returns the userdata field of a cached data set. + * + * @param string dataset ID + * @param string cache group + * @return string userdata + * @access public + * @see extSave() + */ + function getUserdata($id, $group = 'default') { + if (!$this->caching) + return ''; + + return $this->container->getUserdata($id, $group); + } // end func getUserdata + + /** + * Removes the specified dataset from the cache. + * + * @param string dataset ID + * @param string cache group + * @return boolean + * @access public + */ + function remove($id, $group = 'default') { + if (!$this->caching) + return true; + + return $this->container->remove($id, $group); + } // end func remove + + /** + * Flushes the cache - removes all data from it + * + * @param string cache group, if empty all groups will be flashed + * @return integer number of removed datasets + */ + function flush($group = 'default') { + if (!$this->caching) + return true; + + return $this->container->flush($group); + } // end func flush + + /** + * Checks if a dataset exists. + * + * Note: this does not say that the cached data is not expired! + * + * @param string dataset ID + * @param string cache group + * @return boolean + * @access public + */ + function isCached($id, $group = 'default') { + if (!$this->caching) + return false; + + return $this->container->isCached($id, $group); + } // end func isCached + + /** + * Checks if a dataset is expired + * + * @param string dataset ID + * @param string cache group + * @param integer maximum age for the cached data in seconds - 0 for endless + * If the cached data is older but the given lifetime it will + * be removed from the cache. You don't have to provide this + * argument if you call isExpired(). Every dataset knows + * it's expire date and will be removed automatically. Use + * this only if you know what you're doing... + * @return boolean + * @access public + */ + function isExpired($id, $group = 'default', $max_age = 0) { + if (!$this->caching) + return true; + + return $this->container->isExpired($id, $group, $max_age); + } // end func isExpired + + /** + * Generates a "unique" ID for the given value + * + * This is a quick but dirty hack to get a "unique" ID for a any kind of variable. + * ID clashes might occur from time to time although they are extreme unlikely! + * + * @param mixed variable to generate a ID for + * @return string "unique" ID + * @access public + */ + function generateID($variable) { + // WARNING: ID clashes are possible although unlikely + return md5(serialize($variable)); + } + + /** + * Calls the garbage collector of the storage object with a certain probability + * + * @param boolean Force a garbage collection run? + * @see $gc_probability, $gc_time + */ + function garbageCollection($force = false) { + static $last_run = 0; + + if (!$this->caching) + return; + + srand((double) microtime() * 1000000); + + // time and probability based + if (($force) || ($last_run && $last_run < time() + $this->gc_time) || (rand(1, 100) < $this->gc_probability)) { + $this->container->garbageCollection($this->gc_maxlifetime); + $last_run = time(); + } + } // end func garbageCollection + +} // end class cache +?> diff --git a/lib/pear/Cache/Container.php b/lib/pear/Cache/Container.php new file mode 100644 index 000000000..e2f4281c2 --- /dev/null +++ b/lib/pear/Cache/Container.php @@ -0,0 +1,455 @@ + | +// | Sebastian Bergmann | +// | Christian Stocker | +// +----------------------------------------------------------------------+ +// +// $Id: Container.php,v 1.1 2002-08-19 06:41:28 rurban Exp $ + +require_once 'lib/pear/Cache/Error.php'; + +/** +* Common base class of all cache storage container. +* +* To speed up things we do a preload you should know about, otherwise it might +* play you a trick. The Cache controller classes (Cache/Cache, Cache/Output, ...) +* usually do something like is (isCached($id) && !isExpired($id)) return $container->load($id). +* if you implement isCached(), isExpired() and load() straight ahead, each of this +* functions will result in a storage medium (db, file,...) access. This generates too much load. +* Now, a simple speculative preload should saves time in most cases. Whenever +* one of the mentioned methods is invoked we preload the cached dataset into class variables. +* That means that we have only one storage medium access for the sequence +* (isCached($id) && !isExpired($id)) return $container->load($id). +* The bad thing is that the preloaded data might be outdated meanwhile, which is +* unlikely but for you power users, be warned. If you do not want the preload +* you should switch it off by setting the class variable $preload to false. Anyway, this is +* not recommended! +* +* @author Ulf Wendel +* @version $Id: Container.php,v 1.1 2002-08-19 06:41:28 rurban Exp $ +* @package Cache +* @access public +* @abstract +*/ +class Cache_Container { + + /** + * Flag indicating wheter to preload datasets. + * + * See the class description for more details. + * + * @var boolean + */ + var $preload = true; + + /** + * ID of a preloaded dataset + * + * @var string + */ + var $id = ''; + + /** + * Cache group of a preloaded dataset + * + * @var string + */ + var $group = ''; + + /** + * Expiration timestamp of a preloaded dataset. + * + * @var integer 0 means never, endless + */ + var $expires = 0; + + /** + * Value of a preloaded dataset. + * + * @var string + */ + var $cachedata = ''; + + /** + * Preloaded userdata field. + * + * @var string + */ + var $userdata = ''; + + /** + * Flag indicating that the dataset requested for preloading is unknown. + * + * @var boolean + */ + var $unknown = true; + + /** + * Encoding mode for cache data: base64 or addslashes() (slash). + * + * @var string base64 or slash + */ + var $encoding_mode = 'base64'; + + /** + * Highwater mark - maximum space required by all cache entries. + * + * Whenever the garbage collection runs it checks the amount of space + * required by all cache entries. If it's more than n (highwater) bytes + * the garbage collection deletes as many entries as necessary to reach the + * lowwater mark. + * + * @var int + * @see lowwater + */ + var $highwater = 2048000; + + + /** + * Lowwater mark + * + * @var int + * @see highwater + */ + var $lowwater = 1536000; + + + /** + * Options that can be set in every derived class using it's constructor. + * + * @var array + */ + var $allowed_options = array('encoding_mode', 'highwater', 'lowwater'); + + + /** + * Loads a dataset from the cache. + * + * @param string dataset ID + * @param string cache group + * @return mixed dataset value or NULL on failure + * @access public + */ + function load($id, $group) { + if ($this->preload) { + if ($this->id != $id || $this->group != $group) + $this->preload($id, $group); + + return $this->cachedata; + } else { + list( , $data, ) = $this->fetch($id, $group); + return $data; + } + } // end func load + + /** + * Returns the userdata field of a cached data set. + * + * @param string dataset ID + * @param string cache group + * @return string userdata + * @access public + */ + function getUserdata($id, $group) { + if ($this->preload) { + if ($this->id != $id || $this->group != $group) + $this->preload($id, $group); + + return $this->userdata; + } else { + list( , , $userdata) = $this->fetch($id, $group); + return $userdata; + } + } // end func getUserdata + + /** + * Checks if a dataset is expired. + * + * @param string dataset ID + * @param string cache group + * @param integer maximum age timestamp + * @return boolean + * @access public + */ + function isExpired($id, $group, $max_age) { + if ($this->preload) { + if ($this->id != $id || $this->group != $group) + $this->preload($id, $group); + + if ($this->unknown) + return false; + } else { + // check if at all it is cached + if (!$this->isCached($id, $group)) + return false; + + // I'm lazy... + list($this->expires, , ) = $this->fetch($id, $group); + } + + // endless + if (0 == $this->expires) + return false; + + // you feel fine, Ulf? + if ($expired = ($this->expires <= time() || ($max_age && ($this->expires <= $max_age))) ) { + + $this->remove($id, $group); + $this->flushPreload(); + } + return $expired; + } // end func isExpired + + /** + * Checks if a dataset is cached. + * + * @param string dataset ID + * @param string cache group + * @return boolean + */ + function isCached($id, $group) { + if ($this->preload) { + if ($this->id != $id || $this->group != $group) + $this->preload($id, $group); + + return !($this->unknown); + } else { + return $this->idExists($id, $group); + } + } // end func isCached + + // + // abstract methods + // + + /** + * Fetches a dataset from the storage medium. + * + * @param string dataset ID + * @param string cache group + * @return array format: [expire date, cached data, user data] + * @throws Cache_Error + * @abstract + */ + function fetch($id, $group) { + return array(NULL, NULL, NULL); + } // end func fetch + + /** + * Stores a dataset. + * + * @param string dataset ID + * @param mixed data to store + * @param mixed userdefined expire date + * @param string cache group + * @param string additional userdefined data + * @return boolean + * @throws Cache_Error + * @access public + * @abstract + */ + function save($id, $data, $expire, $group, $userdata) { + // QUESTION: Should we update the preload buffer instead? + // Don't think so as the sequence save()/load() is unlikely. + $this->flushPreload($id, $group); + + return NULL; + } // end func save + + /** + * Removes a dataset. + * + * @param string dataset ID + * @param string cache group + * @return boolean + * @access public + * @abstract + */ + function remove($id, $group) { + $this->flushPreload($id, $group); + return NULL; + } // end func remove + + /** + * Flushes the cache - removes all caches datasets from the cache. + * + * @param string If a cache group is given only the group will be flushed + * @return integer Number of removed datasets, -1 on failure + * @access public + * @abstract + */ + function flush($group) { + $this->flushPreload(); + return NULL; + } // end func flush + + /** + * Checks if a dataset exists. + * + * @param string dataset ID + * @param string cache group + * @return boolean + * @access public + * @abstract + */ + function idExists($id, $group) { + return NULL; + } // end func idExists + + /** + * Starts the garbage collection. + * + * @access public + * @abstract + */ + function garbageCollection() { + $this->flushPreload(); + } // end func garbageCollection + + /** + * Does a speculative preload of a dataset + * + * @param string dataset ID + * @param string cache group + * @return boolean + */ + function preload($id, $group) { + // whatever happens, remember the preloaded ID + $this->id = $id; + $this->group = $group; + + list($this->expires, $this->cachedata, $this->userdata) = $this->fetch($id, $group); + + if (NULL === $this->expires) { + // Uuups, unknown ID + $this->flushPreload(); + + return false; + } + + $this->unknown = false; + + return true; + } // end func preload + + /** + * Flushes the internal preload buffer. + * + * save(), remove() and flush() must call this method + * to preevent differences between the preloaded values and + * the real cache contents. + * + * @param string dataset ID, if left out the preloaded values will be flushed. + * If given the preloaded values will only be flushed if they are + * equal to the given id and group + * @param string cache group + * @see preload() + */ + function flushPreload($id = '', $group = 'default') { + if (!$id || ($this->id == $id && $this->group == $group)) { + // clear the internal preload values + $this->id = ''; + $this->group = ''; + $this->cachedata = ''; + $this->userdata = ''; + $this->expires = -1; + $this->unknown = true; + } + } // end func flushPreload + + /** + * Imports the requested datafields as object variables if allowed + * + * @param array List of fields to be imported as object variables + * @param array List of allowed datafields + */ + function setOptions($requested, $allowed) { + foreach ($allowed as $k => $field) + if (isset($requested[$field])) + $this->$field = $requested[$field]; + + } // end func setOptions + + /** + * Encodes the data for the storage container. + * + * @var mixed data to encode + */ + function encode($data) { + if ('base64' == $this->encoding_mode) + return base64_encode(serialize($data)); + else + return serialize($data); + } // end func encode + + + /** + * Decodes the data from the storage container. + * + * @var mixed + */ + function decode($data) { + if ('base64' == $this->encoding_mode) + return unserialize(base64_decode($data)); + else + return unserialize($data); + } // end func decode + + + /** + * Translates human readable/relative times in unixtime + * + * @param mixed can be in the following formats: + * human readable : yyyymmddhhmm[ss]] eg: 20010308095100 + * relative in seconds (1) : +xx eg: +10 + * relative in seconds (2) : x < 946681200 eg: 10 + * absolute unixtime : x < 2147483648 eg: 2147483648 + * see comments in code for details + * @return integer unix timestamp + */ + function getExpiresAbsolute($expires) + { + if (!$expires) + return 0; + //for api-compatibility, one has not to provide a "+", + // if integer is < 946681200 (= Jan 01 2000 00:00:00) + if ('+' == $expires[0] || $expires < 946681200) + { + return(time() + $expires); + } + //if integer is < 100000000000 (= in 3140 years), + // it must be an absolut unixtime + // (since the "human readable" definition asks for a higher number) + elseif ($expires < 100000000000) + { + return $expires; + } + // else it's "human readable"; + else + { + $year = substr($expires, 0, 4); + $month = substr($expires, 4, 2); + $day = substr($expires, 6, 2); + $hour = substr($expires, 8, 2); + $minute = substr($expires, 10, 2); + $second = substr($expires, 12, 2); + return mktime($hour, $minute, $second, $month, $day, $year); + } + + } // end func getExpireAbsolute + +} // end class Container +?> diff --git a/lib/pear/Cache/Container/file.php b/lib/pear/Cache/Container/file.php new file mode 100644 index 000000000..b99067e76 --- /dev/null +++ b/lib/pear/Cache/Container/file.php @@ -0,0 +1,342 @@ + | +// | Sebastian Bergmann | +// +----------------------------------------------------------------------+ +// +// $Id: file.php,v 1.1 2002-08-19 06:41:28 rurban Exp $ + +require_once('lib/pear/Cache/Container.php'); + +/** +* Stores cache contents in a file. +* +* @author Ulf Wendel +* @version $Id: file.php,v 1.1 2002-08-19 06:41:28 rurban Exp $ +*/ +class Cache_Container_file extends Cache_Container { + + /** + * Directory where to put the cache files. + * + * @var string Make sure to add a trailing slash + */ + var $cache_dir = ''; + + /** + * Filename prefix for cache files. + * + * You can use the filename prefix to implement a "domain" based cache or just + * to give the files a more descriptive name. The word "domain" is borroed from + * a user authentification system. One user id (cached dataset with the ID x) + * may exists in different domains (different filename prefix). You might want + * to use this to have different cache values for a production, development and + * quality assurance system. If you want the production cache not to be influenced + * by the quality assurance activities, use different filename prefixes for them. + * + * I personally don't think that you'll never need this, but 640kb happend to be + * not enough, so... you know what I mean. If you find a useful application of the + * feature please update this inline doc. + * + * @var string + */ + var $filename_prefix = ''; + + + /** + * List of cache entries, used within a gc run + * + * @var array + */ + var $entries; + + /** + * Total number of bytes required by all cache entries, used within a gc run. + * + * @var int + */ + var $total_size = 0; + + /** + * Creates the cache directory if neccessary + * + * @param array Config options: ["cache_dir" => ..., "filename_prefix" => ...] + */ + function Cache_Container_file($options = '') { + if (is_array($options)) + $this->setOptions($options, array_merge($this->allowed_options, array('cache_dir', 'filename_prefix'))); + + clearstatcache(); + if ($this->cache_dir) + { + // make relative paths absolute for use in deconstructor. + // it looks like the deconstructor has problems with relative paths + if (OS_UNIX && '/' != $this->cache_dir{0} ) + $this->cache_dir = realpath( getcwd() . '/' . $this->cache_dir) . '/'; + + // check if a trailing slash is in cache_dir + if (!substr($this->cache_dir,-1) ) + $this->cache_dir .= '/'; + + if (!file_exists($this->cache_dir) || !is_dir($this->cache_dir)) + mkdir($this->cache_dir, 0755); + } + $this->entries = array(); + $this->group_dirs = array(); + + } // end func contructor + + function fetch($id, $group) { + $file = $this->getFilename($id, $group); + if (!file_exists($file)) + return array(NULL, NULL, NULL); + + // retrive the content + if (!($fh = @fopen($file, 'rb'))) + return new Cache_Error("Can't access cache file '$file'. Check access rights and path.", __FILE__, __LINE__); + + // file format: + // 1st line: expiration date + // 2nd line: user data + // 3rd+ lines: cache data + $expire = trim(fgets($fh, 12)); + $userdata = trim(fgets($fh, 257)); + $cachedata = $this->decode(fread($fh, filesize($file))); + fclose($fh); + + // last usage date used by the gc - maxlifetime + // touch without second param produced stupid entries... + touch($file,time()); + clearstatcache(); + + return array($expire, $cachedata, $userdata); + } // end func fetch + + /** + * Stores a dataset. + * + * WARNING: If you supply userdata it must not contain any linebreaks, + * otherwise it will break the filestructure. + */ + function save($id, $cachedata, $expires, $group, $userdata) { + $this->flushPreload($id, $group); + + $file = $this->getFilename($id, $group); + if (!($fh = @fopen($file, 'wb'))) + return new Cache_Error("Can't access '$file' to store cache data. Check access rights and path.", __FILE__, __LINE__); + + // file format: + // 1st line: expiration date + // 2nd line: user data + // 3rd+ lines: cache data + $expires = $this->getExpiresAbsolute($expires); + fwrite($fh, $expires . "\n"); + fwrite($fh, $userdata . "\n"); + fwrite($fh, $this->encode($cachedata)); + + fclose($fh); + + // I'm not sure if we need this + // i don't think we need this (chregu) + // touch($file); + + return true; + } // end func save + + function remove($id, $group) { + $this->flushPreload($id, $group); + + $file = $this->getFilename($id, $group); + if (file_exists($file)) { + + $ok = unlink($file); + clearstatcache(); + + return $ok; + } + + return false; + } // end func remove + + function flush($group) { + $this->flushPreload(); + $dir = ($group) ? $this->cache_dir . $group . '/' : $this->cache_dir; + + $num_removed = $this->deleteDir($dir); + unset($this->group_dirs[$group]); + clearstatcache(); + + return $num_removed; + } // end func flush + + function idExists($id, $group) { + + return file_exists($this->getFilename($id, $group)); + } // end func idExists + + /** + * Deletes all expired files. + * + * Garbage collection for files is a rather "expensive", "long time" + * operation. All files in the cache directory have to be examined which + * means that they must be opened for reading, the expiration date has to be + * read from them and if neccessary they have to be unlinked (removed). + * If you have a user comment for a good default gc probability please add it to + * to the inline docs. + * + * @param integer Maximum lifetime in seconds of an no longer used/touched entry + * @throws Cache_Error + */ + function garbageCollection($maxlifetime) { + + $this->flushPreload(); + clearstatcache(); + + $ok = $this->doGarbageCollection($maxlifetime, $this->cache_dir); + + // check the space used by the cache entries + if ($this->total_size > $this->highwater) { + + krsort($this->entries); + reset($this->entries); + + while ($this->total_size > $this->lowwater && list($lastmod, $entry) = each($this->entries)) { + if (@unlink($entry['file'])) + $this->total_size -= $entry['size']; + else + new CacheError("Can't delete {$entry["file"]}. Check the permissions."); + } + + } + + $this->entries = array(); + $this->total_size = 0; + + return $ok; + } // end func garbageCollection + + /** + * Does the recursive gc procedure, protected. + * + * @param integer Maximum lifetime in seconds of an no longer used/touched entry + * @param string directory to examine - don't sets this parameter, it's used for a + * recursive function call! + * @throws Cache_Error + */ + function doGarbageCollection($maxlifetime, $dir) { + + if (!($dh = opendir($dir))) + return new Cache_Error("Can't access cache directory '$dir'. Check permissions and path.", __FILE__, __LINE__); + + while ($file = readdir($dh)) { + if ('.' == $file || '..' == $file) + continue; + + $file = $dir . $file; + if (is_dir($file)) { + $this->doGarbageCollection($maxlifetime,$file . '/'); + continue; + } + + // skip trouble makers but inform the user + if (!($fh = @fopen($file, 'rb'))) { + new Cache_Error("Can't access cache file '$file', skipping it. Check permissions and path.", __FILE__, __LINE__); + continue; + } + + $expire = fgets($fh, 12); + fclose($fh); + $lastused = filemtime($file); + + $this->entries[$lastused] = array('file' => $file, 'size' => filesize($file)); + $this->total_size += filesize($file); + + // remove if expired + if (( ($expire && $expire <= time()) || ($lastused <= (time() - $maxlifetime)) ) && !unlink($file)) + new Cache_Error("Can't unlink cache file '$file', skipping. Check permissions and path.", __FILE__, __LINE__); + } + + closedir($dh); + + // flush the disk state cache + clearstatcache(); + + } // end func doGarbageCollection + + /** + * Returns the filename for the specified id. + * + * @param string dataset ID + * @param string cache group + * @return string full filename with the path + * @access public + */ + function getFilename($id, $group) { + + if (isset($this->group_dirs[$group])) + return $this->group_dirs[$group] . $this->filename_prefix . $id; + + $dir = $this->cache_dir . $group . '/'; + if (!file_exists($dir)) { + mkdir($dir, 0755); + clearstatcache(); + } + + $this->group_dirs[$group] = $dir; + + return $dir . $this->filename_prefix . $id; + } // end func getFilename + + /** + * Deletes a directory and all files in it. + * + * @param string directory + * @return integer number of removed files + * @throws Cache_Error + */ + function deleteDir($dir) { + if (!($dh = opendir($dir))) + return new Cache_Error("Can't remove directory '$dir'. Check permissions and path.", __FILE__, __LINE__); + + $num_removed = 0; + + while ($file = readdir($dh)) { + if ('.' == $file || '..' == $file) + continue; + + $file = $dir . $file; + if (is_dir($file)) { + $file .= '/'; + $num = $this->deleteDir($file . '/'); + if (is_int($num)) + $num_removed += $num; + } else { + if (unlink($file)) + $num_removed++; + } + } + // according to php-manual the following is needed for windows installations. + closedir($dh); + unset( $dh); + if ($dir != $this->cache_dir) { //delete the sub-dir entries itself also, but not the cache-dir. + rmDir($dir); + $num_removed++; + } + + return $num_removed; + } // end func deleteDir + +} // end class file +?> diff --git a/lib/pear/Cache/Container/imgfile.php b/lib/pear/Cache/Container/imgfile.php new file mode 100644 index 000000000..1f7a71523 --- /dev/null +++ b/lib/pear/Cache/Container/imgfile.php @@ -0,0 +1,382 @@ + | +// | Sebastian Bergmann | +// +----------------------------------------------------------------------+ +// +// $Id: imgfile.php,v 1.1 2002-08-19 06:41:28 rurban Exp $ + +require_once('lib/pear/Cache/Container.php'); + +/** +* Stores cache contents in a file. +* +* @author Ulf Wendel +* @version $Id: imgfile.php,v 1.1 2002-08-19 06:41:28 rurban Exp $ +*/ +class Cache_Container_file extends Cache_Container { + + /** + * Directory where to put the cache files. + * + * @var string Make sure to add a trailing slash + */ + var $cache_dir = ''; + + /** + * Filename prefix for cache files. + * + * You can use the filename prefix to implement a "domain" based cache or just + * to give the files a more descriptive name. The word "domain" is borroed from + * a user authentification system. One user id (cached dataset with the ID x) + * may exists in different domains (different filename prefix). You might want + * to use this to have different cache values for a production, development and + * quality assurance system. If you want the production cache not to be influenced + * by the quality assurance activities, use different filename prefixes for them. + * + * I personally don't think that you'll never need this, but 640kb happend to be + * not enough, so... you know what I mean. If you find a useful application of the + * feature please update this inline doc. + * + * @var string + */ + var $filename_prefix = ''; + + + /** + * List of cache entries, used within a gc run + * + * @var array + */ + var $entries; + + /** + * Total number of bytes required by all cache entries, used within a gc run. + * + * @var int + */ + var $total_size = 0; + + /** + * Creates the cache directory if neccessary + * + * @param array Config options: ["cache_dir" => ..., "filename_prefix" => ...] + */ + function Cache_Container_file($options = '') { + if (is_array($options)) + $this->setOptions($options, array_merge($this->allowed_options, array('cache_dir', 'filename_prefix'))); + + clearstatcache(); + if ($this->cache_dir) + { + // make relative paths absolute for use in deconstructor. + // it looks like the deconstructor has problems with relative paths + if (OS_UNIX && '/' != $this->cache_dir{0} ) + $this->cache_dir = realpath( getcwd() . '/' . $this->cache_dir) . '/'; + + // check if a trailing slash is in cache_dir + if (!substr($this->cache_dir,-1) ) + $this->cache_dir .= '/'; + + if (!file_exists($this->cache_dir) || !is_dir($this->cache_dir)) + mkdir($this->cache_dir, 0755); + } + $this->entries = array(); + $this->group_dirs = array(); + + } // end func contructor + + function fetch($id, $group) { + $file = $this->getFilename($id, $group); + if (!file_exists($file)) + return array(NULL, NULL, NULL); + + // retrive the content + if (!($fh = @fopen($file, 'rb'))) + return new Cache_Error("Can't access cache file '$file'. Check access rights and path.", __FILE__, __LINE__); + + // file format: + // 1st line: expiration date + // 2nd line: user data + // 3rd+ lines: cache data + $expire = trim(fgets($fh, 12)); + $userdata = trim(fgets($fh, 257)); + $cachedata = $this->decode(fread($fh, filesize($file))); + fclose($fh); + +//JOHANNES START + if (is_array($cachedata)) + if (file_exists($file.'.img')) { + $fh = @fopen($file.'.img',"rb"); + $cachedata['image'] = fread($fh,filesize($file.'.img')); + fclose($fh); + } +//JOHANNES END + + // last usage date used by the gc - maxlifetime + // touch without second param produced stupid entries... + touch($file,time()); + clearstatcache(); + + return array($expire, $cachedata, $userdata); + } // end func fetch + + /** + * Stores a dataset. + * + * WARNING: If you supply userdata it must not contain any linebreaks, + * otherwise it will break the filestructure. + */ + function save($id, $cachedata, $expires, $group, $userdata) { + $this->flushPreload($id, $group); + + $file = $this->getFilename($id, $group); + if (!($fh = @fopen($file, 'wb'))) + return new Cache_Error("Can't access '$file' to store cache data. Check access rights and path.", __FILE__, __LINE__); + +//JOHANNES + if (is_array($cachedata)&&isset($cachedata['image'])) { + $image = $cachedata['image']; + unset($cachedata['image']); + } +//JOHANNES + + // file format: + // 1st line: expiration date + // 2nd line: user data + // 3rd+ lines: cache data + $expires = $this->getExpiresAbsolute($expires); + fwrite($fh, $expires . "\n"); + fwrite($fh, $userdata . "\n"); + fwrite($fh, $this->encode($cachedata)); + + fclose($fh); + + // I'm not sure if we need this + // i don't think we need this (chregu) + // touch($file); + +//JOHANNES START + if ($image) { + $file = $this->getFilename($id, $group).'.img'; + if (!($fh = @fopen($file, 'wb'))) + return new Cache_Error("Can't access '$file' to store cache data. Check access rights and path.", __FILE__, __LINE__); + fwrite($fh, $image); + fclose($fh); + } +//JOHANNES END + + return true; + } // end func save + + function remove($id, $group) { + $this->flushPreload($id, $group); + + $file = $this->getFilename($id, $group); + if (file_exists($file)) { + + $ok = unlink($file); + clearstatcache(); + + return $ok; + } + + return false; + } // end func remove + + function flush($group) { + $this->flushPreload(); + $dir = ($group) ? $this->cache_dir . $group . '/' : $this->cache_dir; + + $num_removed = $this->deleteDir($dir); + unset($this->group_dirs[$group]); + clearstatcache(); + + return $num_removed; + } // end func flush + + function idExists($id, $group) { + + return file_exists($this->getFilename($id, $group)); + } // end func idExists + + /** + * Deletes all expired files. + * + * Garbage collection for files is a rather "expensive", "long time" + * operation. All files in the cache directory have to be examined which + * means that they must be opened for reading, the expiration date has to be + * read from them and if neccessary they have to be unlinked (removed). + * If you have a user comment for a good default gc probability please add it to + * to the inline docs. + * + * @param integer Maximum lifetime in seconds of an no longer used/touched entry + * @throws Cache_Error + */ + function garbageCollection($maxlifetime) { + + $this->flushPreload(); + clearstatcache(); + + $ok = $this->doGarbageCollection($maxlifetime, $this->cache_dir); + + // check the space used by the cache entries + if ($this->total_size > $this->highwater) { + + krsort($this->entries); + reset($this->entries); + + while ($this->total_size > $this->lowwater && list($lastmod, $entry) = each($this->entries)) { + if (@unlink($entry['file'])) + $this->total_size -= $entry['size']; + else + new CacheError("Can't delete {$entry["file"]}. Check the permissions."); + } + + } + + $this->entries = array(); + $this->total_size = 0; + + return $ok; + } // end func garbageCollection + + /** + * Does the recursive gc procedure, protected. + * + * @param integer Maximum lifetime in seconds of an no longer used/touched entry + * @param string directory to examine - don't sets this parameter, it's used for a + * recursive function call! + * @throws Cache_Error + */ + function doGarbageCollection($maxlifetime, $dir) { + + if (!($dh = opendir($dir))) + return new Cache_Error("Can't access cache directory '$dir'. Check permissions and path.", __FILE__, __LINE__); + + while ($file = readdir($dh)) { + if ('.' == $file || '..' == $file) + continue; +//JOHANNES START + if ('.img' == substr($file,-4)) + continue; +//JOHANNES END + + $file = $dir . $file; + if (is_dir($file)) { + $this->doGarbageCollection($maxlifetime,$file . '/'); + continue; + } + + // skip trouble makers but inform the user + if (!($fh = @fopen($file, 'rb'))) { + new Cache_Error("Can't access cache file '$file', skipping it. Check permissions and path.", __FILE__, __LINE__); + continue; + } + + $expire = fgets($fh, 12); + fclose($fh); + $lastused = filemtime($file); + +//JOHANNES START + $x = 0; + if (file_exists($file.'.img')) + $x = filesize($file.'.img'); + $this->entries[$lastused] = array('file' => $file, 'size' => filesize($file)+$x); + $this->total_size += filesize($file)+$x; + + // remove if expired + if ( ($expire && $expire <= time()) || ($lastused <= (time() - $maxlifetime)) ) { + $ok = unlink($file); + if ( file_exists($file.'.img') ) + $ok = $ok && unlink($file.'.img'); + if (!$ok) + new Cache_Error("Can't unlink cache file '$file', skipping. Check permissions and path.", __FILE__, __LINE__); + } +//JOHANNES END + } + + closedir($dh); + + // flush the disk state cache + clearstatcache(); + + } // end func doGarbageCollection + + /** + * Returns the filename for the specified id. + * + * @param string dataset ID + * @param string cache group + * @return string full filename with the path + * @access public + */ + function getFilename($id, $group) { + + if (isset($this->group_dirs[$group])) + return $this->group_dirs[$group] . $this->filename_prefix . $id; + + $dir = $this->cache_dir . $group . '/'; + if (!file_exists($dir)) { + mkdir($dir, 0755); + clearstatcache(); + } + + $this->group_dirs[$group] = $dir; + + return $dir . $this->filename_prefix . $id; + } // end func getFilename + + /** + * Deletes a directory and all files in it. + * + * @param string directory + * @return integer number of removed files + * @throws Cache_Error + */ + function deleteDir($dir) { + if (!($dh = opendir($dir))) + return new Cache_Error("Can't remove directory '$dir'. Check permissions and path.", __FILE__, __LINE__); + + $num_removed = 0; + + while ($file = readdir($dh)) { + if ('.' == $file || '..' == $file) + continue; + + $file = $dir . $file; + if (is_dir($file)) { + $file .= '/'; + $num = $this->deleteDir($file . '/'); + if (is_int($num)) + $num_removed += $num; + } else { + if (unlink($file)) + $num_removed++; + } + } + // according to php-manual the following is needed for windows installations. + closedir($dh); + unset( $dh); + if ($dir != $this->cache_dir) { //delete the sub-dir entries itself also, but not the cache-dir. + rmDir($dir); + $num_removed++; + } + + return $num_removed; + } // end func deleteDir + +} // end class file +?> diff --git a/lib/pear/Cache/Error.php b/lib/pear/Cache/Error.php new file mode 100644 index 000000000..c4d1365b7 --- /dev/null +++ b/lib/pear/Cache/Error.php @@ -0,0 +1,51 @@ + | +// +----------------------------------------------------------------------+ +// +// $Id: Error.php,v 1.1 2002-08-19 06:41:28 rurban Exp $ + +require_once 'lib/pear/PEAR.php'; + +/** +* Cache Error class +* +* @package Cache +*/ +class Cache_Error extends PEAR_Error { + + + /** + * Prefix of all error messages. + * + * @var string + */ + var $error_message_prefix = 'Cache-Error: '; + + /** + * Creates an cache error object. + * + * @param string error message + * @param string file where the error occured + * @param string linenumber where the error occured + */ + function Cache_Error($msg, $file = __FILE__, $line = __LINE__) { + + $this->PEAR_Error(sprintf("%s [%s on line %d].", $msg, $file, $line)); + + } // end func Cache_Error + +} // end class Cache_Error +?> -- 2.45.0