4cb0a4d8953200a8bd91fdf64d8206c917426345
[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 if( is_file( $dstPath ) ) {
218 $archiveDir = dirname( $archivePath );
219 if ( !is_dir( $archiveDir ) ) wfMkdirParents( $archiveDir );
220 wfSuppressWarnings();
221 $success = rename( $dstPath, $archivePath );
222 wfRestoreWarnings();
223
224 if( ! $success ) {
225 return new WikiErrorMsg( 'filerenameerror', wfEscapeWikiText( $dstPath ),
226 wfEscapeWikiText( $archivePath ) );
227 }
228 else wfDebug(__METHOD__.": moved file $dstPath to $archivePath\n");
229 $status = 'archived';
230 }
231 else {
232 $status = 'new';
233 }
234
235 $error = false;
236 wfSuppressWarnings();
237 if ( $flags & self::DELETE_SOURCE ) {
238 if ( !rename( $srcPath, $dstPath ) ) {
239 $error = new WikiErrorMsg( 'filerenameerror', wfEscapeWikiText( $srcPath ),
240 wfEscapeWikiText( $dstPath ) );
241 }
242 } else {
243 if ( !copy( $srcPath, $dstPath ) ) {
244 $error = new WikiErrorMsg( 'filerenameerror', wfEscapeWikiText( $srcPath ),
245 wfEscapeWikiText( $dstPath ) );
246 }
247 }
248 wfRestoreWarnings();
249
250 if( $error ) {
251 return $error;
252 } else {
253 wfDebug(__METHOD__.": wrote tempfile $srcPath to $dstPath\n");
254 }
255
256 chmod( $dstPath, 0644 );
257 return $status;
258 }
259
260 /**
261 * Get a relative path including trailing slash, e.g. f/fa/
262 * If the repo is not hashed, returns an empty string
263 */
264 function getHashPath( $name ) {
265 return FileRepo::getHashPathForLevel( $name, $this->hashLevels );
266 }
267
268 /**
269 * Call a callback function for every file in the repository.
270 * Uses the filesystem even in child classes.
271 */
272 function enumFilesInFS( $callback ) {
273 $numDirs = 1 << ( $this->hashLevels * 4 );
274 for ( $flatIndex = 0; $flatIndex < $numDirs; $flatIndex++ ) {
275 $hexString = sprintf( "%0{$this->hashLevels}x", $flatIndex );
276 $path = $this->directory;
277 for ( $hexPos = 0; $hexPos < $this->hashLevels; $hexPos++ ) {
278 $path .= '/' . substr( $hexString, 0, $hexPos + 1 );
279 }
280 if ( !file_exists( $path ) || !is_dir( $path ) ) {
281 continue;
282 }
283 $dir = opendir( $path );
284 while ( false !== ( $name = readdir( $dir ) ) ) {
285 call_user_func( $callback, $path . '/' . $name );
286 }
287 }
288 }
289
290 /**
291 * Call a callback function for every file in the repository
292 * May use either the database or the filesystem
293 */
294 function enumFiles( $callback ) {
295 $this->enumFilesInFS( $callback );
296 }
297
298 /**
299 * Get properties of a file with a given virtual URL
300 * The virtual URL must refer to this repo
301 */
302 function getFileProps( $virtualUrl ) {
303 $path = $this->resolveVirtualUrl( $virtualUrl );
304 return File::getPropsFromPath( $path );
305 }
306 }
307
308 ?>