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