(bug 8701) Check database lock status when blocking/unblocking users
[lhc/web/wiklou.git] / includes / FileStore.php
1 <?php
2
3 class FileStore {
4 const DELETE_ORIGINAL = 1;
5
6 /**
7 * Fetch the FileStore object for a given storage group
8 */
9 static function get( $group ) {
10 global $wgFileStore;
11
12 if( isset( $wgFileStore[$group] ) ) {
13 $info = $wgFileStore[$group];
14 return new FileStore( $group,
15 $info['directory'],
16 $info['url'],
17 intval( $info['hash'] ) );
18 } else {
19 return null;
20 }
21 }
22
23 private function __construct( $group, $directory, $path, $hash ) {
24 $this->mGroup = $group;
25 $this->mDirectory = $directory;
26 $this->mPath = $path;
27 $this->mHashLevel = $hash;
28 }
29
30 /**
31 * Acquire a lock; use when performing write operations on a store.
32 * This is attached to your master database connection, so if you
33 * suffer an uncaught error the lock will be released when the
34 * connection is closed.
35 *
36 * @fixme Probably only works on MySQL. Abstract to the Database class?
37 */
38 static function lock() {
39 global $wgDBtype;
40 if ($wgDBtype != 'mysql')
41 return true;
42 $dbw = wfGetDB( DB_MASTER );
43 $lockname = $dbw->addQuotes( FileStore::lockName() );
44 $result = $dbw->query( "SELECT GET_LOCK($lockname, 5) AS lockstatus", __METHOD__ );
45 $row = $dbw->fetchObject( $result );
46 $dbw->freeResult( $result );
47
48 if( $row->lockstatus == 1 ) {
49 return true;
50 } else {
51 wfDebug( __METHOD__." failed to acquire lock\n" );
52 return false;
53 }
54 }
55
56 /**
57 * Release the global file store lock.
58 */
59 static function unlock() {
60 global $wgDBtype;
61 if ($wgDBtype != 'mysql')
62 return true;
63 $dbw = wfGetDB( DB_MASTER );
64 $lockname = $dbw->addQuotes( FileStore::lockName() );
65 $result = $dbw->query( "SELECT RELEASE_LOCK($lockname)", __METHOD__ );
66 $dbw->fetchObject( $result );
67 $dbw->freeResult( $result );
68 }
69
70 private static function lockName() {
71 return 'MediaWiki.' . wfWikiID() . '.FileStore';
72 }
73
74 /**
75 * Copy a file into the file store from elsewhere in the filesystem.
76 * Should be protected by FileStore::lock() to avoid race conditions.
77 *
78 * @param $key storage key string
79 * @param $flags
80 * DELETE_ORIGINAL - remove the source file on transaction commit.
81 *
82 * @throws FSException if copy can't be completed
83 * @return FSTransaction
84 */
85 function insert( $key, $sourcePath, $flags=0 ) {
86 $destPath = $this->filePath( $key );
87 return $this->copyFile( $sourcePath, $destPath, $flags );
88 }
89
90 /**
91 * Copy a file from the file store to elsewhere in the filesystem.
92 * Should be protected by FileStore::lock() to avoid race conditions.
93 *
94 * @param $key storage key string
95 * @param $flags
96 * DELETE_ORIGINAL - remove the source file on transaction commit.
97 *
98 * @throws FSException if copy can't be completed
99 * @return FSTransaction on success
100 */
101 function export( $key, $destPath, $flags=0 ) {
102 $sourcePath = $this->filePath( $key );
103 return $this->copyFile( $sourcePath, $destPath, $flags );
104 }
105
106 private function copyFile( $sourcePath, $destPath, $flags=0 ) {
107 if( !file_exists( $sourcePath ) ) {
108 // Abort! Abort!
109 throw new FSException( "missing source file '$sourcePath'\n" );
110 }
111
112 $transaction = new FSTransaction();
113
114 if( $flags & self::DELETE_ORIGINAL ) {
115 $transaction->addCommit( FSTransaction::DELETE_FILE, $sourcePath );
116 }
117
118 if( file_exists( $destPath ) ) {
119 // An identical file is already present; no need to copy.
120 } else {
121 if( !file_exists( dirname( $destPath ) ) ) {
122 wfSuppressWarnings();
123 $ok = mkdir( dirname( $destPath ), 0777, true );
124 wfRestoreWarnings();
125
126 if( !$ok ) {
127 throw new FSException(
128 "failed to create directory for '$destPath'\n" );
129 }
130 }
131
132 wfSuppressWarnings();
133 $ok = copy( $sourcePath, $destPath );
134 wfRestoreWarnings();
135
136 if( $ok ) {
137 wfDebug( __METHOD__." copied '$sourcePath' to '$destPath'\n" );
138 $transaction->addRollback( FSTransaction::DELETE_FILE, $destPath );
139 } else {
140 throw new FSException(
141 __METHOD__." failed to copy '$sourcePath' to '$destPath'\n" );
142 }
143 }
144
145 return $transaction;
146 }
147
148 /**
149 * Delete a file from the file store.
150 * Caller's responsibility to make sure it's not being used by another row.
151 *
152 * File is not actually removed until transaction commit.
153 * Should be protected by FileStore::lock() to avoid race conditions.
154 *
155 * @param $key storage key string
156 * @throws FSException if file can't be deleted
157 * @return FSTransaction
158 */
159 function delete( $key ) {
160 $destPath = $this->filePath( $key );
161 if( false === $destPath ) {
162 throw new FSExcepton( "file store does not contain file '$key'" );
163 } else {
164 return FileStore::deleteFile( $destPath );
165 }
166 }
167
168 /**
169 * Delete a non-managed file on a transactional basis.
170 *
171 * File is not actually removed until transaction commit.
172 * Should be protected by FileStore::lock() to avoid race conditions.
173 *
174 * @param $path file to remove
175 * @throws FSException if file can't be deleted
176 * @return FSTransaction
177 *
178 * @fixme Might be worth preliminary permissions check
179 */
180 static function deleteFile( $path ) {
181 if( file_exists( $path ) ) {
182 $transaction = new FSTransaction();
183 $transaction->addCommit( FSTransaction::DELETE_FILE, $path );
184 return $transaction;
185 } else {
186 throw new FSException( "cannot delete missing file '$path'" );
187 }
188 }
189
190 /**
191 * Stream a contained file directly to HTTP output.
192 * Will throw a 404 if file is missing; 400 if invalid key.
193 * @return true on success, false on failure
194 */
195 function stream( $key ) {
196 $path = $this->filePath( $key );
197 if( $path === false ) {
198 wfHttpError( 400, "Bad request", "Invalid or badly-formed filename." );
199 return false;
200 }
201
202 if( file_exists( $path ) ) {
203 // Set the filename for more convenient save behavior from browsers
204 // FIXME: Is this safe?
205 header( 'Content-Disposition: inline; filename="' . $key . '"' );
206
207 require_once 'StreamFile.php';
208 wfStreamFile( $path );
209 } else {
210 return wfHttpError( 404, "Not found",
211 "The requested resource does not exist." );
212 }
213 }
214
215 /**
216 * Confirm that the given file key is valid.
217 * Note that a valid key may refer to a file that does not exist.
218 *
219 * Key should consist of a 32-digit base-36 SHA-1 hash and
220 * an optional alphanumeric extension, all lowercase.
221 * The whole must not exceed 64 characters.
222 *
223 * @param $key
224 * @return boolean
225 */
226 static function validKey( $key ) {
227 return preg_match( '/^[0-9a-z]{32}(\.[0-9a-z]{1,31})?$/', $key );
228 }
229
230
231 /**
232 * Calculate file storage key from a file on disk.
233 * You must pass an extension to it, as some files may be calculated
234 * out of a temporary file etc.
235 *
236 * @param $path to file
237 * @param $extension
238 * @return string or false if could not open file or bad extension
239 */
240 static function calculateKey( $path, $extension ) {
241 wfSuppressWarnings();
242 $hash = sha1_file( $path );
243 wfRestoreWarnings();
244 if( $hash === false ) {
245 wfDebug( __METHOD__.": couldn't hash file '$path'\n" );
246 return false;
247 }
248
249 $base36 = wfBaseConvert( $hash, 16, 36, 32 );
250 if( $extension == '' ) {
251 $key = $base36;
252 } else {
253 $key = $base36 . '.' . $extension;
254 }
255
256 // Sanity check
257 if( self::validKey( $key ) ) {
258 return $key;
259 } else {
260 wfDebug( __METHOD__.": generated bad key '$key'\n" );
261 return false;
262 }
263 }
264
265 /**
266 * Return filesystem path to the given file.
267 * Note that the file may or may not exist.
268 * @return string or false if an invalid key
269 */
270 function filePath( $key ) {
271 if( self::validKey( $key ) ) {
272 return $this->mDirectory . DIRECTORY_SEPARATOR .
273 $this->hashPath( $key, DIRECTORY_SEPARATOR );
274 } else {
275 return false;
276 }
277 }
278
279 /**
280 * Return URL path to the given file, if the store is public.
281 * @return string or false if not public
282 */
283 function urlPath( $key ) {
284 if( $this->mUrl && self::validKey( $key ) ) {
285 return $this->mUrl . '/' . $this->hashPath( $key, '/' );
286 } else {
287 return false;
288 }
289 }
290
291 private function hashPath( $key, $separator ) {
292 $parts = array();
293 for( $i = 0; $i < $this->mHashLevel; $i++ ) {
294 $parts[] = $key{$i};
295 }
296 $parts[] = $key;
297 return implode( $separator, $parts );
298 }
299 }
300
301 /**
302 * Wrapper for file store transaction stuff.
303 *
304 * FileStore methods may return one of these for undoable operations;
305 * you can then call its rollback() or commit() methods to perform
306 * final cleanup if dependent database work fails or succeeds.
307 */
308 class FSTransaction {
309 const DELETE_FILE = 1;
310
311 /**
312 * Combine more items into a fancier transaction
313 */
314 function add( FSTransaction $transaction ) {
315 $this->mOnCommit = array_merge(
316 $this->mOnCommit, $transaction->mOnCommit );
317 $this->mOnRollback = array_merge(
318 $this->mOnRollback, $transaction->mOnRollback );
319 }
320
321 /**
322 * Perform final actions for success.
323 * @return true if actions applied ok, false if errors
324 */
325 function commit() {
326 return $this->apply( $this->mOnCommit );
327 }
328
329 /**
330 * Perform final actions for failure.
331 * @return true if actions applied ok, false if errors
332 */
333 function rollback() {
334 return $this->apply( $this->mOnRollback );
335 }
336
337 // --- Private and friend functions below...
338
339 function __construct() {
340 $this->mOnCommit = array();
341 $this->mOnRollback = array();
342 }
343
344 function addCommit( $action, $path ) {
345 $this->mOnCommit[] = array( $action, $path );
346 }
347
348 function addRollback( $action, $path ) {
349 $this->mOnRollback[] = array( $action, $path );
350 }
351
352 private function apply( $actions ) {
353 $result = true;
354 foreach( $actions as $item ) {
355 list( $action, $path ) = $item;
356 if( $action == self::DELETE_FILE ) {
357 wfSuppressWarnings();
358 $ok = unlink( $path );
359 wfRestoreWarnings();
360 if( $ok )
361 wfDebug( __METHOD__.": deleting file '$path'\n" );
362 else
363 wfDebug( __METHOD__.": failed to delete file '$path'\n" );
364 $result = $result && $ok;
365 }
366 }
367 return $result;
368 }
369 }
370
371 class FSException extends MWException { }
372
373 ?>