dc404447f141c49061adf11e61a23ecf043acf42
[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
8 class FSRepo extends FileRepo {
9 var $directory, $url, $hashLevels;
10 var $fileFactory = array( 'UnregisteredLocalFile', 'newFromTitle' );
11 var $oldFileFactory = false;
12
13 function __construct( $info ) {
14 parent::__construct( $info );
15
16 // Required settings
17 $this->directory = $info['directory'];
18 $this->url = $info['url'];
19 $this->hashLevels = $info['hashLevels'];
20 }
21
22 /**
23 * Get the public root directory of the repository.
24 */
25 function getRootDirectory() {
26 return $this->directory;
27 }
28
29 /**
30 * Get the public root URL of the repository
31 */
32 function getRootUrl() {
33 return $this->url;
34 }
35
36 /**
37 * Returns true if the repository uses a multi-level directory structure
38 */
39 function isHashed() {
40 return (bool)$this->hashLevels;
41 }
42
43 /**
44 * Get the local directory corresponding to one of the three basic zones
45 */
46 function getZonePath( $zone ) {
47 switch ( $zone ) {
48 case 'public':
49 return $this->directory;
50 case 'temp':
51 return "{$this->directory}/temp";
52 case 'deleted':
53 return $GLOBALS['wgFileStore']['deleted']['directory'];
54 default:
55 return false;
56 }
57 }
58
59 /**
60 * Get the URL corresponding to one of the three basic zones
61 */
62 function getZoneUrl( $zone ) {
63 switch ( $zone ) {
64 case 'public':
65 return $this->url;
66 case 'temp':
67 return "{$this->url}/temp";
68 case 'deleted':
69 return $GLOBALS['wgFileStore']['deleted']['url'];
70 default:
71 return false;
72 }
73 }
74
75 /**
76 * Get a URL referring to this repository, with the private mwrepo protocol.
77 * The suffix, if supplied, is considered to be unencoded, and will be
78 * URL-encoded before being returned.
79 */
80 function getVirtualUrl( $suffix = false ) {
81 $path = 'mwrepo://' . $this->name;
82 if ( $suffix !== false ) {
83 $path .= '/' . rawurlencode( $suffix );
84 }
85 return $path;
86 }
87
88 /**
89 * Get the local path corresponding to a virtual URL
90 */
91 function resolveVirtualUrl( $url ) {
92 if ( substr( $url, 0, 9 ) != 'mwrepo://' ) {
93 throw new MWException( __METHOD__.': unknown protoocl' );
94 }
95
96 $bits = explode( '/', substr( $url, 9 ), 3 );
97 if ( count( $bits ) != 3 ) {
98 throw new MWException( __METHOD__.": invalid mwrepo URL: $url" );
99 }
100 list( $repo, $zone, $rel ) = $bits;
101 if ( $repo !== $this->name ) {
102 throw new MWException( __METHOD__.": fetching from a foreign repo is not supported" );
103 }
104 $base = $this->getZonePath( $zone );
105 if ( !$base ) {
106 throw new MWException( __METHOD__.": invalid zone: $zone" );
107 }
108 return $base . '/' . rawurldecode( $rel );
109 }
110
111 /**
112 * Store a file to a given destination.
113 */
114 function store( $srcPath, $dstZone, $dstRel, $flags = 0 ) {
115 if ( !is_writable( $this->directory ) ) {
116 return new WikiErrorMsg( 'upload_directory_read_only', wfEscapeWikiText( $this->directory ) );
117 }
118 $root = $this->getZonePath( $dstZone );
119 if ( !$root ) {
120 throw new MWException( "Invalid zone: $dstZone" );
121 }
122 $dstPath = "$root/$dstRel";
123
124 if ( !is_dir( dirname( $dstPath ) ) ) {
125 wfMkdirParents( dirname( $dstPath ) );
126 }
127
128 if ( self::isVirtualUrl( $srcPath ) ) {
129 $srcPath = $this->resolveVirtualUrl( $srcPath );
130 }
131
132 if ( $flags & self::DELETE_SOURCE ) {
133 if ( !rename( $srcPath, $dstPath ) ) {
134 return new WikiErrorMsg( 'filerenameerror', wfEscapeWikiText( $srcPath ),
135 wfEscapeWikiText( $dstPath ) );
136 }
137 } else {
138 if ( !copy( $srcPath, $dstPath ) ) {
139 return new WikiErrorMsg( 'filecopyerror', wfEscapeWikiText( $srcPath ),
140 wfEscapeWikiText( $dstPath ) );
141 }
142 }
143 chmod( $dstPath, 0644 );
144 return true;
145 }
146
147 /**
148 * Pick a random name in the temp zone and store a file to it.
149 * Returns the URL, or a WikiError on failure.
150 * @param string $originalName The base name of the file as specified
151 * by the user. The file extension will be maintained.
152 * @param string $srcPath The current location of the file.
153 */
154 function storeTemp( $originalName, $srcPath ) {
155 $date = gmdate( "YmdHis" );
156 $hashPath = $this->getHashPath( $originalName );
157 $dstRel = "$hashPath$date!$originalName";
158 $dstUrlRel = $hashPath . $date . '!' . rawurlencode( $originalName );
159
160 $result = $this->store( $srcPath, 'temp', $dstRel );
161 if ( WikiError::isError( $result ) ) {
162 return $result;
163 } else {
164 return $this->getVirtualUrl( 'temp' ) . '/' . $dstUrlRel;
165 }
166 }
167
168 /**
169 * Remove a temporary file or mark it for garbage collection
170 * @param string $virtualUrl The virtual URL returned by storeTemp
171 * @return boolean True on success, false on failure
172 */
173 function freeTemp( $virtualUrl ) {
174 $temp = "mwrepo://{$this->name}/temp";
175 if ( substr( $virtualUrl, 0, strlen( $temp ) ) != $temp ) {
176 wfDebug( __METHOD__.": Invalid virtual URL\n" );
177 return false;
178 }
179 $path = $this->resolveVirtualUrl( $virtualUrl );
180 wfSuppressWarnings();
181 $success = unlink( $path );
182 wfRestoreWarnings();
183 return $success;
184 }
185
186
187 /**
188 * Copy or move a file either from the local filesystem or from an mwrepo://
189 * virtual URL, into this repository at the specified destination location.
190 *
191 * @param string $srcPath The source path or URL
192 * @param string $dstRel The destination relative path
193 * @param string $archiveRel The relative path where the existing file is to
194 * be archived, if there is one. Relative to the public zone root.
195 * @param integer $flags Bitfield, may be FileRepo::DELETE_SOURCE to indicate
196 * that the source file should be deleted if possible
197 */
198 function publish( $srcPath, $dstRel, $archiveRel, $flags = 0 ) {
199 if ( !is_writable( $this->directory ) ) {
200 return new WikiErrorMsg( 'upload_directory_read_only', wfEscapeWikiText( $this->directory ) );
201 }
202 if ( substr( $srcPath, 0, 9 ) == 'mwrepo://' ) {
203 $srcPath = $this->resolveVirtualUrl( $srcPath );
204 }
205 if ( !$this->validateFilename( $dstRel ) ) {
206 throw new MWException( 'Validation error in $dstRel' );
207 }
208 if ( !$this->validateFilename( $archiveRel ) ) {
209 throw new MWException( 'Validation error in $archiveRel' );
210 }
211 $dstPath = "{$this->directory}/$dstRel";
212 $archivePath = "{$this->directory}/$archiveRel";
213
214 $dstDir = dirname( $dstPath );
215 if ( !is_dir( $dstDir ) ) wfMkdirParents( $dstDir );
216
217 // Check if the source is missing before we attempt to move the dest to archive
218 if ( !is_file( $srcPath ) ) {
219 return new WikiErrorMsg( 'filenotfound', wfEscapeWikiText( $srcPath ) );
220 }
221
222 if( is_file( $dstPath ) ) {
223 $archiveDir = dirname( $archivePath );
224 if ( !is_dir( $archiveDir ) ) wfMkdirParents( $archiveDir );
225 wfSuppressWarnings();
226 $success = rename( $dstPath, $archivePath );
227 wfRestoreWarnings();
228
229 if( ! $success ) {
230 return new WikiErrorMsg( 'filerenameerror', wfEscapeWikiText( $dstPath ),
231 wfEscapeWikiText( $archivePath ) );
232 }
233 else wfDebug(__METHOD__.": moved file $dstPath to $archivePath\n");
234 $status = 'archived';
235 }
236 else {
237 $status = 'new';
238 }
239
240 $error = false;
241 wfSuppressWarnings();
242 if ( $flags & self::DELETE_SOURCE ) {
243 if ( !rename( $srcPath, $dstPath ) ) {
244 $error = new WikiErrorMsg( 'filerenameerror', wfEscapeWikiText( $srcPath ),
245 wfEscapeWikiText( $dstPath ) );
246 }
247 } else {
248 if ( !copy( $srcPath, $dstPath ) ) {
249 $error = new WikiErrorMsg( 'filerenameerror', wfEscapeWikiText( $srcPath ),
250 wfEscapeWikiText( $dstPath ) );
251 }
252 }
253 wfRestoreWarnings();
254
255 if( $error ) {
256 return $error;
257 } else {
258 wfDebug(__METHOD__.": wrote tempfile $srcPath to $dstPath\n");
259 }
260
261 chmod( $dstPath, 0644 );
262 return $status;
263 }
264
265 /**
266 * Get a relative path including trailing slash, e.g. f/fa/
267 * If the repo is not hashed, returns an empty string
268 */
269 function getHashPath( $name ) {
270 return FileRepo::getHashPathForLevel( $name, $this->hashLevels );
271 }
272
273 /**
274 * Call a callback function for every file in the repository.
275 * Uses the filesystem even in child classes.
276 */
277 function enumFilesInFS( $callback ) {
278 $numDirs = 1 << ( $this->hashLevels * 4 );
279 for ( $flatIndex = 0; $flatIndex < $numDirs; $flatIndex++ ) {
280 $hexString = sprintf( "%0{$this->hashLevels}x", $flatIndex );
281 $path = $this->directory;
282 for ( $hexPos = 0; $hexPos < $this->hashLevels; $hexPos++ ) {
283 $path .= '/' . substr( $hexString, 0, $hexPos + 1 );
284 }
285 if ( !file_exists( $path ) || !is_dir( $path ) ) {
286 continue;
287 }
288 $dir = opendir( $path );
289 while ( false !== ( $name = readdir( $dir ) ) ) {
290 call_user_func( $callback, $path . '/' . $name );
291 }
292 }
293 }
294
295 /**
296 * Call a callback function for every file in the repository
297 * May use either the database or the filesystem
298 */
299 function enumFiles( $callback ) {
300 $this->enumFilesInFS( $callback );
301 }
302
303 /**
304 * Get properties of a file with a given virtual URL
305 * The virtual URL must refer to this repo
306 */
307 function getFileProps( $virtualUrl ) {
308 $path = $this->resolveVirtualUrl( $virtualUrl );
309 return File::getPropsFromPath( $path );
310 }
311 }
312
313