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