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;
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 foreach ( array( 'descBaseUrl', 'scriptDirUrl', 'articleUrl', 'fetchDescription',
28 'thumbScriptUrl' ) as $var )
30 if ( isset( $info[$var] ) ) {
31 $this->$var = $info[$var];
37 * Create a new File object from the local repository
38 * @param mixed $title Title object or string
39 * @param mixed $time Time at which the image is supposed to have existed.
40 * If this is specified, the returned object will be an
41 * instance of the repository's old file class instead of
42 * a current file. Repositories not supporting version
43 * control should return false if this parameter is set.
45 function newFile( $title, $time = false ) {
46 if ( !($title instanceof Title
) ) {
47 $title = Title
::makeTitleSafe( NS_IMAGE
, $title );
48 if ( !is_object( $title ) ) {
53 if ( $this->oldFileFactory
) {
54 return call_user_func( $this->oldFileFactory
, $title, $this, $time );
59 return call_user_func( $this->fileFactory
, $title, $this );
64 * Find an instance of the named file that existed at the specified time
65 * Returns false if the file did not exist. Repositories not supporting
66 * version control should return false if the time is specified.
68 * @param mixed $time 14-character timestamp, or false for the current version
70 function findFile( $title, $time = false ) {
71 # First try the current version of the file to see if it precedes the timestamp
72 $img = $this->newFile( $title );
76 if ( $img->exists() && ( !$time ||
$img->getTimestamp() <= $time ) ) {
79 # Now try an old version of the file
80 $img = $this->newFile( $title, $time );
81 if ( $img->exists() ) {
87 * Get the public root directory of the repository.
89 function getRootDirectory() {
90 return $this->directory
;
94 * Get the public root URL of the repository
96 function getRootUrl() {
101 * Returns true if the repository uses a multi-level directory structure
103 function isHashed() {
104 return (bool)$this->hashLevels
;
108 * Get the URL of thumb.php
110 function getThumbScriptUrl() {
111 return $this->thumbScriptUrl
;
115 * Returns true if the repository can transform files via a 404 handler
117 function canTransformVia404() {
118 return $this->transformVia404
;
122 * Get the local directory corresponding to one of the three basic zones
124 function getZonePath( $zone ) {
127 return $this->directory
;
129 return "{$this->directory}/temp";
131 return $GLOBALS['wgFileStore']['deleted']['directory'];
138 * Get the URL corresponding to one of the three basic zones
140 function getZoneUrl( $zone ) {
145 return "{$this->url}/temp";
147 return $GLOBALS['wgFileStore']['deleted']['url'];
154 * Get a URL referring to this repository, with the private mwrepo protocol.
156 function getVirtualUrl( $suffix = false ) {
158 if ( $suffix !== false ) {
159 $path .= '/' . $suffix;
165 * Get the local path corresponding to a virtual URL
167 function resolveVirtualUrl( $url ) {
168 if ( substr( $url, 0, 9 ) != 'mwrepo://' ) {
169 throw new MWException( __METHOD__
.': unknown protoocl' );
172 $bits = explode( '/', substr( $url, 9 ), 3 );
173 if ( count( $bits ) != 3 ) {
174 throw new MWException( __METHOD__
.": invalid mwrepo URL: $url" );
176 list( $host, $zone, $rel ) = $bits;
177 if ( $host !== '' ) {
178 throw new MWException( __METHOD__
.": fetching from a foreign repo is not supported" );
180 $base = $this->getZonePath( $zone );
182 throw new MWException( __METHOD__
.": invalid zone: $zone" );
184 return $base . '/' . urldecode( $rel );
188 * Store a file to a given destination.
190 function store( $srcPath, $dstZone, $dstRel, $flags = 0 ) {
191 $root = $this->getZonePath( $dstZone );
193 throw new MWException( "Invalid zone: $dstZone" );
195 $dstPath = "$root/$dstRel";
197 if ( !is_dir( dirname( $dstPath ) ) ) {
198 wfMkdirParents( dirname( $dstPath ) );
201 if ( substr( $srcPath, 0, 9 ) == 'mwrepo://' ) {
202 $srcPath = $this->resolveVirtualUrl( $srcPath );
205 if ( $flags & self
::DELETE_SOURCE
) {
206 if ( !rename( $srcPath, $dstPath ) ) {
207 return new WikiErrorMsg( 'filerenameerror', wfEscapeWikiText( $srcPath ),
208 wfEscapeWikiText( $dstPath ) );
211 if ( !copy( $srcPath, $dstPath ) ) {
212 return new WikiErrorMsg( 'filecopyerror', wfEscapeWikiText( $srcPath ),
213 wfEscapeWikiText( $dstPath ) );
216 chmod( $dstPath, 0644 );
221 * Pick a random name in the temp zone and store a file to it.
222 * Returns the URL, or a WikiError on failure.
223 * @param string $originalName The base name of the file as specified
224 * by the user. The file extension will be maintained.
225 * @param string $srcPath The current location of the file.
227 function storeTemp( $originalName, $srcPath ) {
228 $dstRel = $this->getHashPath( $originalName ) .
229 gmdate( "YmdHis" ) . '!' . $originalName;
230 $result = $this->store( $srcPath, 'temp', $dstRel );
231 if ( WikiError
::isError( $result ) ) {
234 return $this->getVirtualUrl( "temp/$dstRel" );
239 * Copy or move a file either from the local filesystem or from an mwrepo://
240 * virtual URL, into this repository at the specified destination location.
242 * @param string $srcPath The source path or URL
243 * @param string $dstPath The destination relative path
244 * @param string $archivePath The relative path where the existing file is to
245 * be archived, if there is one.
246 * @param integer $flags Bitfield, may be FSRepo::DELETE_SOURCE to indicate
247 * that the source file should be deleted if possible
249 function publish( $srcPath, $dstPath, $archivePath, $flags = 0 ) {
250 if ( substr( $srcPath, 0, 9 ) == 'mwrepo://' ) {
251 $srcPath = $this->resolveVirtualUrl( $srcPath );
253 $dstDir = dirname( $dstPath );
254 if ( !is_dir( $dstDir ) ) wfMkdirParents( $dstDir );
256 if( is_file( $dstPath ) ) {
257 $archiveDir = dirname( $archivePath );
258 if ( !is_dir( $archiveDir ) ) wfMkdirParents( $archiveDir );
259 wfSuppressWarnings();
260 $success = rename( $dstPath, $archivePath );
264 return new WikiErrorMsg( 'filerenameerror', wfEscapeWikiText( $dstPath ),
265 wfEscapeWikiText( $archivePath ) );
267 else wfDebug(__METHOD__
.": moved file $dstPath to $archivePath\n");
268 $status = 'archived';
275 wfSuppressWarnings();
276 if ( $flags & self
::DELETE_SOURCE
) {
277 if ( !rename( $srcPath, $dstPath ) ) {
278 $error = new WikiErrorMsg( 'filerenameerror', wfEscapeWikiText( $srcPath ),
279 wfEscapeWikiText( $dstPath ) );
282 if ( !copy( $srcPath, $dstPath ) ) {
283 $error = new WikiErrorMsg( 'filerenameerror', wfEscapeWikiText( $srcPath ),
284 wfEscapeWikiText( $dstPath ) );
292 wfDebug(__METHOD__
.": wrote tempfile $srcPath to $dstPath\n");
295 chmod( $dstPath, 0644 );
300 * Get a relative path including trailing slash, e.g. f/fa/
301 * If the repo is not hashed, returns an empty string
303 function getHashPath( $name ) {
304 if ( $this->isHashed() ) {
305 $hash = md5( $name );
307 for ( $i = 1; $i <= $this->hashLevels
; $i++
) {
308 $path .= substr( $hash, 0, $i ) . '/';
317 * Get the name of this repository, as specified by $info['name]' to the constructor
324 * Get the file description page base URL, or false if there isn't one.
327 function getDescBaseUrl() {
328 if ( is_null( $this->descBaseUrl
) ) {
329 if ( !is_null( $this->articleUrl
) ) {
330 $this->descBaseUrl
= str_replace( '$1',
331 urlencode( Namespace::getCanonicalName( NS_IMAGE
) ) . ':', $this->articleUrl
);
332 } elseif ( !is_null( $this->scriptDirUrl
) ) {
333 $this->descBaseUrl
= $this->scriptDirUrl
. '/index.php?title=' .
334 urlencode( Namespace::getCanonicalName( NS_IMAGE
) ) . ':';
336 $this->descBaseUrl
= false;
339 return $this->descBaseUrl
;
343 * Get the URL of an image description page. May return false if it is
344 * unknown or not applicable. In general this should only be called by the
345 * File class, since it may return invalid results for certain kinds of
346 * repositories. Use File::getDescriptionUrl() in user code.
348 * In particular, it uses the article paths as specified to the repository
349 * constructor, whereas local repositories use the local Title functions.
351 function getDescriptionUrl( $name ) {
352 $base = $this->getDescBaseUrl();
354 return $base . wfUrlencode( $name );
361 * Get the URL of the content-only fragment of the description page. For
362 * MediaWiki this means action=render. This should only be called by the
363 * repository's file class, since it may return invalid results. User code
364 * should use File::getDescriptionText().
366 function getDescriptionRenderUrl( $name ) {
367 if ( isset( $this->scriptDirUrl
) ) {
368 return $this->scriptDirUrl
. '/index.php?title=' .
369 wfUrlencode( Namespace::getCanonicalName( NS_IMAGE
) . ':' . $name ) .
372 $descBase = $this->getDescBaseUrl();
374 return wfAppendQuery( $descBase . wfUrlencode( $name ), 'action=render' );
382 * Call a callback function for every file in the repository.
383 * Uses the filesystem even in child classes.
385 function enumFilesInFS( $callback ) {
386 $numDirs = 1 << ( $this->hashLevels
* 4 );
387 for ( $flatIndex = 0; $flatIndex < $numDirs; $flatIndex++
) {
388 $hexString = sprintf( "%0{$this->hashLevels}x", $flatIndex );
389 $path = $this->directory
;
390 for ( $hexPos = 0; $hexPos < $this->hashLevels
; $hexPos++
) {
391 $path .= '/' . substr( $hexString, 0, $hexPos +
1 );
393 if ( !file_exists( $path ) ||
!is_dir( $path ) ) {
396 $dir = opendir( $path );
397 while ( false !== ( $name = readdir( $dir ) ) ) {
398 call_user_func( $callback, $path . '/' . $name );
404 * Call a callaback function for every file in the repository
405 * May use either the database or the filesystem
407 function enumFiles( $callback ) {
408 $this->enumFilesInFS( $callback );