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