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
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 ) {
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'] );
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];
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 ) ) {
54 if ( $this->oldFileFactory
) {
55 return call_user_func( $this->oldFileFactory
, $title, $this, $time );
60 return call_user_func( $this->fileFactory
, $title, $this );
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 );
77 if ( $img->exists() && ( !$time ||
$img->getTimestamp() <= $time ) ) {
80 # Now try an old version of the file
81 $img = $this->newFile( $title, $time );
82 if ( $img->exists() ) {
88 * Get the public root directory of the repository.
90 function getRootDirectory() {
91 return $this->directory
;
95 * Get the public root URL of the repository
97 function getRootUrl() {
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 ) {
128 return $this->directory
;
130 return "{$this->directory}/temp";
132 return $GLOBALS['wgFileStore']['deleted']['directory'];
139 * Get the URL corresponding to one of the three basic zones
141 function getZoneUrl( $zone ) {
146 return "{$this->url}/temp";
148 return $GLOBALS['wgFileStore']['deleted']['url'];
155 * Get a URL referring to this repository, with the private mwrepo protocol.
157 function getVirtualUrl( $suffix = false ) {
159 if ( $suffix !== false ) {
160 $path .= '/' . $suffix;
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 );
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 );
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 ) );
212 if ( !copy( $srcPath, $dstPath ) ) {
213 return new WikiErrorMsg( 'filecopyerror', wfEscapeWikiText( $srcPath ),
214 wfEscapeWikiText( $dstPath ) );
217 chmod( $dstPath, 0644 );
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 ) ) {
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" );
250 $path = $this->resolveVirtualUrl( $virtualUrl );
251 wfSuppressWarnings();
252 $success = unlink( $path );
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 );
284 return new WikiErrorMsg( 'filerenameerror', wfEscapeWikiText( $dstPath ),
285 wfEscapeWikiText( $archivePath ) );
287 else wfDebug(__METHOD__
.": moved file $dstPath to $archivePath\n");
288 $status = 'archived';
295 wfSuppressWarnings();
296 if ( $flags & self
::DELETE_SOURCE
) {
297 if ( !rename( $srcPath, $dstPath ) ) {
298 $error = new WikiErrorMsg( 'filerenameerror', wfEscapeWikiText( $srcPath ),
299 wfEscapeWikiText( $dstPath ) );
302 if ( !copy( $srcPath, $dstPath ) ) {
303 $error = new WikiErrorMsg( 'filerenameerror', wfEscapeWikiText( $srcPath ),
304 wfEscapeWikiText( $dstPath ) );
312 wfDebug(__METHOD__
.": wrote tempfile $srcPath to $dstPath\n");
315 chmod( $dstPath, 0644 );
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 );
327 for ( $i = 1; $i <= $this->hashLevels
; $i++
) {
328 $path .= substr( $hash, 0, $i ) . '/';
337 * Get the name of this repository, as specified by $info['name]' to the constructor
344 * Get the file description page base URL, or false if there isn't one.
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
) ) . ':';
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();
374 return $base . wfUrlencode( $name );
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 ) .
392 $descBase = $this->getDescBaseUrl();
394 return wfAppendQuery( $descBase . wfUrlencode( $name ), 'action=render' );
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 ) ) {
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 ) {
438 $name = $title->getUserCaseDBKey();
439 if ( $this->initialCapital
) {
440 $name = $wgContLang->ucfirst( $name );
443 $name = $title->getDBkey();