3 * Data storage in the file system.
5 * This program is free software; you can redistribute it and/or modify
6 * it under the terms of the GNU General Public License as published by
7 * the Free Software Foundation; either version 2 of the License, or
8 * (at your option) any later version.
10 * This program is distributed in the hope that it will be useful,
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 * GNU General Public License for more details.
15 * You should have received a copy of the GNU General Public License along
16 * with this program; if not, write to the Free Software Foundation, Inc.,
17 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
18 * http://www.gnu.org/copyleft/gpl.html
25 * Base class for data storage in the file system.
29 abstract class FileCacheBase
{
31 protected $mType = 'object';
32 protected $mExt = 'cache';
38 /* @todo configurable? */
39 const MISS_FACTOR
= 15; // log 1 every MISS_FACTOR cache misses
40 const MISS_TTL_SEC
= 3600; // how many seconds ago is "recent"
42 protected function __construct() {
45 $this->mUseGzip
= (bool)$wgUseGzip;
49 * Get the base file cache directory
52 final protected function baseCacheDirectory() {
53 global $wgFileCacheDirectory;
54 return $wgFileCacheDirectory;
58 * Get the base cache directory (not specific to this file)
61 abstract protected function cacheDirectory();
64 * Get the path to the cache file
67 protected function cachePath() {
68 if ( $this->mFilePath
!== null ) {
69 return $this->mFilePath
;
72 $dir = $this->cacheDirectory();
73 # Build directories (methods include the trailing "/")
74 $subDirs = $this->typeSubdirectory() . $this->hashSubdirectory();
75 # Avoid extension confusion
76 $key = str_replace( '.', '%2E', urlencode( $this->mKey
) );
77 # Build the full file path
78 $this->mFilePath
= "{$dir}/{$subDirs}{$key}.{$this->mExt}";
79 if ( $this->useGzip() ) {
80 $this->mFilePath
.= '.gz';
83 return $this->mFilePath
;
87 * Check if the cache file exists
90 public function isCached() {
91 if ( $this->mCached
=== null ) {
92 $this->mCached
= file_exists( $this->cachePath() );
94 return $this->mCached
;
98 * Get the last-modified timestamp of the cache file
99 * @return string|bool TS_MW timestamp
101 public function cacheTimestamp() {
102 $timestamp = filemtime( $this->cachePath() );
103 return ( $timestamp !== false )
104 ?
wfTimestamp( TS_MW
, $timestamp )
109 * Check if up to date cache file exists
110 * @param string $timestamp MW_TS timestamp
114 public function isCacheGood( $timestamp = '' ) {
115 global $wgCacheEpoch;
117 if ( !$this->isCached() ) {
121 $cachetime = $this->cacheTimestamp();
122 $good = ( $timestamp <= $cachetime && $wgCacheEpoch <= $cachetime );
123 wfDebug( __METHOD__
. ": cachetime $cachetime, touched '{$timestamp}' epoch {$wgCacheEpoch}, good $good\n" );
129 * Check if the cache is gzipped
132 protected function useGzip() {
133 return $this->mUseGzip
;
137 * Get the uncompressed text from the cache
140 public function fetchText() {
141 if ( $this->useGzip() ) {
142 $fh = gzopen( $this->cachePath(), 'rb' );
143 return stream_get_contents( $fh );
145 return file_get_contents( $this->cachePath() );
150 * Save and compress text to the cache
151 * @return string compressed text
153 public function saveText( $text ) {
154 global $wgUseFileCache;
156 if ( !$wgUseFileCache ) {
160 if ( $this->useGzip() ) {
161 $text = gzencode( $text );
164 $this->checkCacheDirs(); // build parent dir
165 if ( !file_put_contents( $this->cachePath(), $text, LOCK_EX
) ) {
166 wfDebug( __METHOD__
. "() failed saving " . $this->cachePath() . "\n" );
167 $this->mCached
= null;
171 $this->mCached
= true;
176 * Clear the cache for this page
179 public function clearCache() {
180 wfSuppressWarnings();
181 unlink( $this->cachePath() );
183 $this->mCached
= false;
187 * Create parent directors of $this->cachePath()
190 protected function checkCacheDirs() {
191 wfMkdirParents( dirname( $this->cachePath() ), null, __METHOD__
);
195 * Get the cache type subdirectory (with trailing slash)
196 * An extending class could use that method to alter the type -> directory
197 * mapping. @see HTMLFileCache::typeSubdirectory() for an example.
201 protected function typeSubdirectory() {
202 return $this->mType
. '/';
206 * Return relative multi-level hash subdirectory (with trailing slash)
207 * or the empty string if not $wgFileCacheDepth
210 protected function hashSubdirectory() {
211 global $wgFileCacheDepth;
214 if ( $wgFileCacheDepth > 0 ) {
215 $hash = md5( $this->mKey
);
216 for ( $i = 1; $i <= $wgFileCacheDepth; $i++
) {
217 $subdir .= substr( $hash, 0, $i ) . '/';
225 * Roughly increments the cache misses in the last hour by unique visitors
226 * @param $request WebRequest
229 public function incrMissesRecent( WebRequest
$request ) {
231 if ( mt_rand( 0, self
::MISS_FACTOR
- 1 ) == 0 ) {
232 # Get a large IP range that should include the user even if that
233 # person's IP address changes
234 $ip = $request->getIP();
235 if ( !IP
::isValid( $ip ) ) {
238 $ip = IP
::isIPv6( $ip )
239 ? IP
::sanitizeRange( "$ip/32" )
240 : IP
::sanitizeRange( "$ip/16" );
242 # Bail out if a request already came from this range...
243 $key = wfMemcKey( get_class( $this ), 'attempt', $this->mType
, $this->mKey
, $ip );
244 if ( $wgMemc->get( $key ) ) {
245 return; // possibly the same user
247 $wgMemc->set( $key, 1, self
::MISS_TTL_SEC
);
249 # Increment the number of cache misses...
250 $key = $this->cacheMissKey();
251 if ( $wgMemc->get( $key ) === false ) {
252 $wgMemc->set( $key, 1, self
::MISS_TTL_SEC
);
254 $wgMemc->incr( $key );
260 * Roughly gets the cache misses in the last hour by unique visitors
263 public function getMissesRecent() {
265 return self
::MISS_FACTOR
* $wgMemc->get( $this->cacheMissKey() );
271 protected function cacheMissKey() {
272 return wfMemcKey( get_class( $this ), 'misses', $this->mType
, $this->mKey
);