More cleanup from r22859
[mediawiki.git] / includes / filerepo / FSRepo.php
blobc33df9464f591fb4733f30f8c7535a7ceadfb975
1 <?php
3 /**
4 * A repository for files accessible via the local filesystem. Does not support
5 * database access or registration.
7 * TODO: split off abstract base FileRepo
8 */
10 class FSRepo {
11 const DELETE_SOURCE = 1;
13 var $directory, $url, $hashLevels, $thumbScriptUrl, $transformVia404;
14 var $descBaseUrl, $scriptDirUrl, $articleUrl, $fetchDescription, $initialCapital;
15 var $fileFactory = array( 'UnregisteredLocalFile', 'newFromTitle' );
16 var $oldFileFactory = false;
18 function __construct( $info ) {
19 // Required settings
20 $this->name = $info['name'];
21 $this->directory = $info['directory'];
22 $this->url = $info['url'];
23 $this->hashLevels = $info['hashLevels'];
24 $this->transformVia404 = !empty( $info['transformVia404'] );
26 // Optional settings
27 $this->initialCapital = true; // by default
28 foreach ( array( 'descBaseUrl', 'scriptDirUrl', 'articleUrl', 'fetchDescription',
29 'thumbScriptUrl', 'initialCapital' ) as $var )
31 if ( isset( $info[$var] ) ) {
32 $this->$var = $info[$var];
37 /**
38 * Create a new File object from the local repository
39 * @param mixed $title Title object or string
40 * @param mixed $time Time at which the image is supposed to have existed.
41 * If this is specified, the returned object will be an
42 * instance of the repository's old file class instead of
43 * a current file. Repositories not supporting version
44 * control should return false if this parameter is set.
46 function newFile( $title, $time = false ) {
47 if ( !($title instanceof Title) ) {
48 $title = Title::makeTitleSafe( NS_IMAGE, $title );
49 if ( !is_object( $title ) ) {
50 return null;
53 if ( $time ) {
54 if ( $this->oldFileFactory ) {
55 return call_user_func( $this->oldFileFactory, $title, $this, $time );
56 } else {
57 return false;
59 } else {
60 return call_user_func( $this->fileFactory, $title, $this );
64 /**
65 * Find an instance of the named file that existed at the specified time
66 * Returns false if the file did not exist. Repositories not supporting
67 * version control should return false if the time is specified.
69 * @param mixed $time 14-character timestamp, or false for the current version
71 function findFile( $title, $time = false ) {
72 # First try the current version of the file to see if it precedes the timestamp
73 $img = $this->newFile( $title );
74 if ( !$img ) {
75 return false;
77 if ( $img->exists() && ( !$time || $img->getTimestamp() <= $time ) ) {
78 return $img;
80 # Now try an old version of the file
81 $img = $this->newFile( $title, $time );
82 if ( $img->exists() ) {
83 return $img;
87 /**
88 * Get the public root directory of the repository.
90 function getRootDirectory() {
91 return $this->directory;
94 /**
95 * Get the public root URL of the repository
97 function getRootUrl() {
98 return $this->url;
102 * Returns true if the repository uses a multi-level directory structure
104 function isHashed() {
105 return (bool)$this->hashLevels;
109 * Get the URL of thumb.php
111 function getThumbScriptUrl() {
112 return $this->thumbScriptUrl;
116 * Returns true if the repository can transform files via a 404 handler
118 function canTransformVia404() {
119 return $this->transformVia404;
123 * Get the local directory corresponding to one of the three basic zones
125 function getZonePath( $zone ) {
126 switch ( $zone ) {
127 case 'public':
128 return $this->directory;
129 case 'temp':
130 return "{$this->directory}/temp";
131 case 'deleted':
132 return $GLOBALS['wgFileStore']['deleted']['directory'];
133 default:
134 return false;
139 * Get the URL corresponding to one of the three basic zones
141 function getZoneUrl( $zone ) {
142 switch ( $zone ) {
143 case 'public':
144 return $this->url;
145 case 'temp':
146 return "{$this->url}/temp";
147 case 'deleted':
148 return $GLOBALS['wgFileStore']['deleted']['url'];
149 default:
150 return false;
155 * Get a URL referring to this repository, with the private mwrepo protocol.
157 function getVirtualUrl( $suffix = false ) {
158 $path = 'mwrepo://';
159 if ( $suffix !== false ) {
160 $path .= '/' . $suffix;
162 return $path;
166 * Get the local path corresponding to a virtual URL
168 function resolveVirtualUrl( $url ) {
169 if ( substr( $url, 0, 9 ) != 'mwrepo://' ) {
170 throw new MWException( __METHOD__.': unknown protoocl' );
173 $bits = explode( '/', substr( $url, 9 ), 3 );
174 if ( count( $bits ) != 3 ) {
175 throw new MWException( __METHOD__.": invalid mwrepo URL: $url" );
177 list( $host, $zone, $rel ) = $bits;
178 if ( $host !== '' ) {
179 throw new MWException( __METHOD__.": fetching from a foreign repo is not supported" );
181 $base = $this->getZonePath( $zone );
182 if ( !$base ) {
183 throw new MWException( __METHOD__.": invalid zone: $zone" );
185 return $base . '/' . urldecode( $rel );
189 * Store a file to a given destination.
191 function store( $srcPath, $dstZone, $dstRel, $flags = 0 ) {
192 $root = $this->getZonePath( $dstZone );
193 if ( !$root ) {
194 throw new MWException( "Invalid zone: $dstZone" );
196 $dstPath = "$root/$dstRel";
198 if ( !is_dir( dirname( $dstPath ) ) ) {
199 wfMkdirParents( dirname( $dstPath ) );
202 if ( substr( $srcPath, 0, 9 ) == 'mwrepo://' ) {
203 $srcPath = $this->resolveVirtualUrl( $srcPath );
206 if ( $flags & self::DELETE_SOURCE ) {
207 if ( !rename( $srcPath, $dstPath ) ) {
208 return new WikiErrorMsg( 'filerenameerror', wfEscapeWikiText( $srcPath ),
209 wfEscapeWikiText( $dstPath ) );
211 } else {
212 if ( !copy( $srcPath, $dstPath ) ) {
213 return new WikiErrorMsg( 'filecopyerror', wfEscapeWikiText( $srcPath ),
214 wfEscapeWikiText( $dstPath ) );
217 chmod( $dstPath, 0644 );
218 return true;
222 * Pick a random name in the temp zone and store a file to it.
223 * Returns the URL, or a WikiError on failure.
224 * @param string $originalName The base name of the file as specified
225 * by the user. The file extension will be maintained.
226 * @param string $srcPath The current location of the file.
228 function storeTemp( $originalName, $srcPath ) {
229 $dstRel = $this->getHashPath( $originalName ) .
230 gmdate( "YmdHis" ) . '!' . $originalName;
231 $result = $this->store( $srcPath, 'temp', $dstRel );
232 if ( WikiError::isError( $result ) ) {
233 return $result;
234 } else {
235 return $this->getVirtualUrl( "temp/$dstRel" );
240 * Remove a temporary file or mark it for garbage collection
241 * @param string $virtualUrl The virtual URL returned by storeTemp
242 * @return boolean True on success, false on failure
244 function freeTemp( $virtualUrl ) {
245 $temp = 'mwrepo:///temp';
246 if ( substr( $virtualUrl, 0, strlen( $temp ) ) != $temp ) {
247 wfDebug( __METHOD__.": Invalid virtual URL\n" );
248 return false;
250 $path = $this->resolveVirtualUrl( $virtualUrl );
251 wfSuppressWarnings();
252 $success = unlink( $path );
253 wfRestoreWarnings();
254 return $success;
259 * Copy or move a file either from the local filesystem or from an mwrepo://
260 * virtual URL, into this repository at the specified destination location.
262 * @param string $srcPath The source path or URL
263 * @param string $dstPath The destination relative path
264 * @param string $archivePath The relative path where the existing file is to
265 * be archived, if there is one.
266 * @param integer $flags Bitfield, may be FSRepo::DELETE_SOURCE to indicate
267 * that the source file should be deleted if possible
269 function publish( $srcPath, $dstPath, $archivePath, $flags = 0 ) {
270 if ( substr( $srcPath, 0, 9 ) == 'mwrepo://' ) {
271 $srcPath = $this->resolveVirtualUrl( $srcPath );
273 $dstDir = dirname( $dstPath );
274 if ( !is_dir( $dstDir ) ) wfMkdirParents( $dstDir );
276 if( is_file( $dstPath ) ) {
277 $archiveDir = dirname( $archivePath );
278 if ( !is_dir( $archiveDir ) ) wfMkdirParents( $archiveDir );
279 wfSuppressWarnings();
280 $success = rename( $dstPath, $archivePath );
281 wfRestoreWarnings();
283 if( ! $success ) {
284 return new WikiErrorMsg( 'filerenameerror', wfEscapeWikiText( $dstPath ),
285 wfEscapeWikiText( $archivePath ) );
287 else wfDebug(__METHOD__.": moved file $dstPath to $archivePath\n");
288 $status = 'archived';
290 else {
291 $status = 'new';
294 $error = false;
295 wfSuppressWarnings();
296 if ( $flags & self::DELETE_SOURCE ) {
297 if ( !rename( $srcPath, $dstPath ) ) {
298 $error = new WikiErrorMsg( 'filerenameerror', wfEscapeWikiText( $srcPath ),
299 wfEscapeWikiText( $dstPath ) );
301 } else {
302 if ( !copy( $srcPath, $dstPath ) ) {
303 $error = new WikiErrorMsg( 'filerenameerror', wfEscapeWikiText( $srcPath ),
304 wfEscapeWikiText( $dstPath ) );
307 wfRestoreWarnings();
309 if( $error ) {
310 return $error;
311 } else {
312 wfDebug(__METHOD__.": wrote tempfile $srcPath to $dstPath\n");
315 chmod( $dstPath, 0644 );
316 return $status;
320 * Get a relative path including trailing slash, e.g. f/fa/
321 * If the repo is not hashed, returns a slash
323 function getHashPath( $name ) {
324 if ( $this->isHashed() ) {
325 $hash = md5( $name );
326 $path = '';
327 for ( $i = 1; $i <= $this->hashLevels; $i++ ) {
328 $path .= substr( $hash, 0, $i ) . '/';
330 return $path;
331 } else {
332 return '/';
337 * Get the name of this repository, as specified by $info['name]' to the constructor
339 function getName() {
340 return $this->name;
344 * Get the file description page base URL, or false if there isn't one.
345 * @private
347 function getDescBaseUrl() {
348 if ( is_null( $this->descBaseUrl ) ) {
349 if ( !is_null( $this->articleUrl ) ) {
350 $this->descBaseUrl = str_replace( '$1',
351 urlencode( Namespace::getCanonicalName( NS_IMAGE ) ) . ':', $this->articleUrl );
352 } elseif ( !is_null( $this->scriptDirUrl ) ) {
353 $this->descBaseUrl = $this->scriptDirUrl . '/index.php?title=' .
354 urlencode( Namespace::getCanonicalName( NS_IMAGE ) ) . ':';
355 } else {
356 $this->descBaseUrl = false;
359 return $this->descBaseUrl;
363 * Get the URL of an image description page. May return false if it is
364 * unknown or not applicable. In general this should only be called by the
365 * File class, since it may return invalid results for certain kinds of
366 * repositories. Use File::getDescriptionUrl() in user code.
368 * In particular, it uses the article paths as specified to the repository
369 * constructor, whereas local repositories use the local Title functions.
371 function getDescriptionUrl( $name ) {
372 $base = $this->getDescBaseUrl();
373 if ( $base ) {
374 return $base . wfUrlencode( $name );
375 } else {
376 return false;
381 * Get the URL of the content-only fragment of the description page. For
382 * MediaWiki this means action=render. This should only be called by the
383 * repository's file class, since it may return invalid results. User code
384 * should use File::getDescriptionText().
386 function getDescriptionRenderUrl( $name ) {
387 if ( isset( $this->scriptDirUrl ) ) {
388 return $this->scriptDirUrl . '/index.php?title=' .
389 wfUrlencode( Namespace::getCanonicalName( NS_IMAGE ) . ':' . $name ) .
390 '&action=render';
391 } else {
392 $descBase = $this->getDescBaseUrl();
393 if ( $descBase ) {
394 return wfAppendQuery( $descBase . wfUrlencode( $name ), 'action=render' );
395 } else {
396 return false;
402 * Call a callback function for every file in the repository.
403 * Uses the filesystem even in child classes.
405 function enumFilesInFS( $callback ) {
406 $numDirs = 1 << ( $this->hashLevels * 4 );
407 for ( $flatIndex = 0; $flatIndex < $numDirs; $flatIndex++ ) {
408 $hexString = sprintf( "%0{$this->hashLevels}x", $flatIndex );
409 $path = $this->directory;
410 for ( $hexPos = 0; $hexPos < $this->hashLevels; $hexPos++ ) {
411 $path .= '/' . substr( $hexString, 0, $hexPos + 1 );
413 if ( !file_exists( $path ) || !is_dir( $path ) ) {
414 continue;
416 $dir = opendir( $path );
417 while ( false !== ( $name = readdir( $dir ) ) ) {
418 call_user_func( $callback, $path . '/' . $name );
424 * Call a callaback function for every file in the repository
425 * May use either the database or the filesystem
427 function enumFiles( $callback ) {
428 $this->enumFilesInFS( $callback );
432 * Get the name of an image from its title object
434 function getNameFromTitle( $title ) {
435 global $wgCapitalLinks;
436 if ( $this->initialCapital != $wgCapitalLinks ) {
437 global $wgContLang;
438 $name = $title->getUserCaseDBKey();
439 if ( $this->initialCapital ) {
440 $name = $wgContLang->ucfirst( $name );
442 } else {
443 $name = $title->getDBkey();
445 return $name;