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