Fixed a few filerepo bugs, added some documentation
[lhc/web/wiklou.git] / includes / filerepo / FSRepo.php
1 <?php
2
3 /**
4 * A repository for files accessible via the local filesystem. Does not support
5 * database access or registration.
6 *
7 * TODO: split off abstract base FileRepo
8 */
9
10 class FSRepo {
11 const DELETE_SOURCE = 1;
12
13 var $directory, $url, $hashLevels, $thumbScriptUrl, $transformVia404;
14 var $descBaseUrl, $scriptDirUrl, $articleUrl, $fetchDescription;
15 var $fileFactory = array( 'UnregisteredLocalFile', 'newFromTitle' );
16 var $oldFileFactory = false;
17
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'] );
25
26 // Optional settings
27 foreach ( array( 'descBaseUrl', 'scriptDirUrl', 'articleUrl', 'fetchDescription',
28 'thumbScriptUrl' ) as $var )
29 {
30 if ( isset( $info[$var] ) ) {
31 $this->$var = $info[$var];
32 }
33 }
34 }
35
36 /**
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.
44 */
45 function newFile( $title, $time = false ) {
46 if ( !($title instanceof Title) ) {
47 $title = Title::makeTitleSafe( NS_IMAGE, $title );
48 if ( !is_object( $title ) ) {
49 return null;
50 }
51 }
52 if ( $time ) {
53 if ( $this->oldFileFactory ) {
54 return call_user_func( $this->oldFileFactory, $title, $this, $time );
55 } else {
56 return false;
57 }
58 } else {
59 return call_user_func( $this->fileFactory, $title, $this );
60 }
61 }
62
63 /**
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.
67 *
68 * @param mixed $time 14-character timestamp, or false for the current version
69 */
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 );
73 if ( !$img ) {
74 return false;
75 }
76 if ( $img->exists() && ( !$time || $img->getTimestamp() <= $time ) ) {
77 return $img;
78 }
79 # Now try an old version of the file
80 $img = $this->newFile( $title, $time );
81 if ( $img->exists() ) {
82 return $img;
83 }
84 }
85
86 /**
87 * Get the public root directory of the repository.
88 */
89 function getRootDirectory() {
90 return $this->directory;
91 }
92
93 /**
94 * Get the public root URL of the repository
95 */
96 function getRootUrl() {
97 return $this->url;
98 }
99
100 /**
101 * Returns true if the repository uses a multi-level directory structure
102 */
103 function isHashed() {
104 return (bool)$this->hashLevels;
105 }
106
107 /**
108 * Get the URL of thumb.php
109 */
110 function getThumbScriptUrl() {
111 return $this->thumbScriptUrl;
112 }
113
114 /**
115 * Returns true if the repository can transform files via a 404 handler
116 */
117 function canTransformVia404() {
118 return $this->transformVia404;
119 }
120
121 /**
122 * Get the local directory corresponding to one of the three basic zones
123 */
124 function getZonePath( $zone ) {
125 switch ( $zone ) {
126 case 'public':
127 return $this->directory;
128 case 'temp':
129 return "{$this->directory}/temp";
130 case 'deleted':
131 return $GLOBALS['wgFileStore']['deleted']['directory'];
132 default:
133 return false;
134 }
135 }
136
137 /**
138 * Get the URL corresponding to one of the three basic zones
139 */
140 function getZoneUrl( $zone ) {
141 switch ( $zone ) {
142 case 'public':
143 return $this->url;
144 case 'temp':
145 return "{$this->url}/temp";
146 case 'deleted':
147 return $GLOBALS['wgFileStore']['deleted']['url'];
148 default:
149 return false;
150 }
151 }
152
153 /**
154 * Get a URL referring to this repository, with the private mwrepo protocol.
155 */
156 function getVirtualUrl( $suffix = false ) {
157 $path = 'mwrepo://';
158 if ( $suffix !== false ) {
159 $path .= '/' . $suffix;
160 }
161 return $path;
162 }
163
164 /**
165 * Get the local path corresponding to a virtual URL
166 */
167 function resolveVirtualUrl( $url ) {
168 if ( substr( $url, 0, 9 ) != 'mwrepo://' ) {
169 throw new MWException( __METHOD__.': unknown protoocl' );
170 }
171
172 $bits = explode( '/', substr( $url, 9 ), 3 );
173 if ( count( $bits ) != 3 ) {
174 throw new MWException( __METHOD__.": invalid mwrepo URL: $url" );
175 }
176 list( $host, $zone, $rel ) = $bits;
177 if ( $host !== '' ) {
178 throw new MWException( __METHOD__.": fetching from a foreign repo is not supported" );
179 }
180 $base = $this->getZonePath( $zone );
181 if ( !$base ) {
182 throw new MWException( __METHOD__.": invalid zone: $zone" );
183 }
184 return $base . '/' . urldecode( $rel );
185 }
186
187 /**
188 * Store a file to a given destination.
189 */
190 function store( $srcPath, $dstZone, $dstRel, $flags = 0 ) {
191 $root = $this->getZonePath( $dstZone );
192 if ( !$root ) {
193 throw new MWException( "Invalid zone: $dstZone" );
194 }
195 $dstPath = "$root/$dstRel";
196
197 if ( !is_dir( dirname( $dstPath ) ) ) {
198 wfMkdirParents( dirname( $dstPath ) );
199 }
200
201 if ( substr( $srcPath, 0, 9 ) == 'mwrepo://' ) {
202 $srcPath = $this->resolveVirtualUrl( $srcPath );
203 }
204
205 if ( $flags & self::DELETE_SOURCE ) {
206 if ( !rename( $srcPath, $dstPath ) ) {
207 return new WikiErrorMsg( 'filerenameerror', wfEscapeWikiText( $srcPath ),
208 wfEscapeWikiText( $dstPath ) );
209 }
210 } else {
211 if ( !copy( $srcPath, $dstPath ) ) {
212 return new WikiErrorMsg( 'filecopyerror', wfEscapeWikiText( $srcPath ),
213 wfEscapeWikiText( $dstPath ) );
214 }
215 }
216 chmod( $dstPath, 0644 );
217 return true;
218 }
219
220 /**
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.
226 */
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 ) ) {
232 return $result;
233 } else {
234 return $this->getVirtualUrl( "temp/$dstRel" );
235 }
236 }
237
238 /**
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.
241 *
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
248 */
249 function publish( $srcPath, $dstPath, $archivePath, $flags = 0 ) {
250 if ( substr( $srcPath, 0, 9 ) == 'mwrepo://' ) {
251 $srcPath = $this->resolveVirtualUrl( $srcPath );
252 }
253 $dstDir = dirname( $dstPath );
254 if ( !is_dir( $dstDir ) ) wfMkdirParents( $dstDir );
255
256 if( is_file( $dstPath ) ) {
257 $archiveDir = dirname( $archivePath );
258 if ( !is_dir( $archiveDir ) ) wfMkdirParents( $archiveDir );
259 wfSuppressWarnings();
260 $success = rename( $dstPath, $archivePath );
261 wfRestoreWarnings();
262
263 if( ! $success ) {
264 return new WikiErrorMsg( 'filerenameerror', wfEscapeWikiText( $dstPath ),
265 wfEscapeWikiText( $archivePath ) );
266 }
267 else wfDebug(__METHOD__.": moved file $dstPath to $archivePath\n");
268 $status = 'archived';
269 }
270 else {
271 $status = 'new';
272 }
273
274 $error = false;
275 wfSuppressWarnings();
276 if ( $flags & self::DELETE_SOURCE ) {
277 if ( !rename( $srcPath, $dstPath ) ) {
278 $error = new WikiErrorMsg( 'filerenameerror', wfEscapeWikiText( $srcPath ),
279 wfEscapeWikiText( $dstPath ) );
280 }
281 } else {
282 if ( !copy( $srcPath, $dstPath ) ) {
283 $error = new WikiErrorMsg( 'filerenameerror', wfEscapeWikiText( $srcPath ),
284 wfEscapeWikiText( $dstPath ) );
285 }
286 }
287 wfRestoreWarnings();
288
289 if( $error ) {
290 return $error;
291 } else {
292 wfDebug(__METHOD__.": wrote tempfile $srcPath to $dstPath\n");
293 }
294
295 chmod( $dstPath, 0644 );
296 return $status;
297 }
298
299 /**
300 * Get a relative path including trailing slash, e.g. f/fa/
301 * If the repo is not hashed, returns an empty string
302 */
303 function getHashPath( $name ) {
304 if ( $this->isHashed() ) {
305 $hash = md5( $name );
306 $path = '';
307 for ( $i = 1; $i <= $this->hashLevels; $i++ ) {
308 $path .= substr( $hash, 0, $i ) . '/';
309 }
310 return $path;
311 } else {
312 return '';
313 }
314 }
315
316 /**
317 * Get the name of this repository, as specified by $info['name]' to the constructor
318 */
319 function getName() {
320 return $this->name;
321 }
322
323 /**
324 * Get the file description page base URL, or false if there isn't one.
325 * @private
326 */
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 ) ) . ':';
335 } else {
336 $this->descBaseUrl = false;
337 }
338 }
339 return $this->descBaseUrl;
340 }
341
342 /**
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.
347 *
348 * In particular, it uses the article paths as specified to the repository
349 * constructor, whereas local repositories use the local Title functions.
350 */
351 function getDescriptionUrl( $name ) {
352 $base = $this->getDescBaseUrl();
353 if ( $base ) {
354 return $base . wfUrlencode( $name );
355 } else {
356 return false;
357 }
358 }
359
360 /**
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().
365 */
366 function getDescriptionRenderUrl( $name ) {
367 if ( isset( $this->scriptDirUrl ) ) {
368 return $this->scriptDirUrl . '/index.php?title=' .
369 wfUrlencode( Namespace::getCanonicalName( NS_IMAGE ) . ':' . $name ) .
370 '&action=render';
371 } else {
372 $descBase = $this->getDescBaseUrl();
373 if ( $descBase ) {
374 return wfAppendQuery( $descBase . wfUrlencode( $name ), 'action=render' );
375 } else {
376 return false;
377 }
378 }
379 }
380
381 /**
382 * Call a callback function for every file in the repository.
383 * Uses the filesystem even in child classes.
384 */
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 );
392 }
393 if ( !file_exists( $path ) || !is_dir( $path ) ) {
394 continue;
395 }
396 $dir = opendir( $path );
397 while ( false !== ( $name = readdir( $dir ) ) ) {
398 call_user_func( $callback, $path . '/' . $name );
399 }
400 }
401 }
402
403 /**
404 * Call a callaback function for every file in the repository
405 * May use either the database or the filesystem
406 */
407 function enumFiles( $callback ) {
408 $this->enumFilesInFS( $callback );
409 }
410 }
411
412 ?>