diff options
Diffstat (limited to 'includes/FileStore.php')
-rw-r--r-- | includes/FileStore.php | 377 |
1 files changed, 377 insertions, 0 deletions
diff --git a/includes/FileStore.php b/includes/FileStore.php new file mode 100644 index 00000000..85aaedfe --- /dev/null +++ b/includes/FileStore.php @@ -0,0 +1,377 @@ +<?php + +class FileStore { + const DELETE_ORIGINAL = 1; + + /** + * Fetch the FileStore object for a given storage group + */ + static function get( $group ) { + global $wgFileStore; + + if( isset( $wgFileStore[$group] ) ) { + $info = $wgFileStore[$group]; + return new FileStore( $group, + $info['directory'], + $info['url'], + intval( $info['hash'] ) ); + } else { + return null; + } + } + + private function __construct( $group, $directory, $path, $hash ) { + $this->mGroup = $group; + $this->mDirectory = $directory; + $this->mPath = $path; + $this->mHashLevel = $hash; + } + + /** + * Acquire a lock; use when performing write operations on a store. + * This is attached to your master database connection, so if you + * suffer an uncaught error the lock will be released when the + * connection is closed. + * + * @fixme Probably only works on MySQL. Abstract to the Database class? + */ + static function lock() { + $fname = __CLASS__ . '::' . __FUNCTION__; + + $dbw = wfGetDB( DB_MASTER ); + $lockname = $dbw->addQuotes( FileStore::lockName() ); + $result = $dbw->query( "SELECT GET_LOCK($lockname, 5) AS lockstatus", $fname ); + $row = $dbw->fetchObject( $result ); + $dbw->freeResult( $result ); + + if( $row->lockstatus == 1 ) { + return true; + } else { + wfDebug( "$fname failed to acquire lock\n" ); + return false; + } + } + + /** + * Release the global file store lock. + */ + static function unlock() { + $fname = __CLASS__ . '::' . __FUNCTION__; + + $dbw = wfGetDB( DB_MASTER ); + $lockname = $dbw->addQuotes( FileStore::lockName() ); + $result = $dbw->query( "SELECT RELEASE_LOCK($lockname)", $fname ); + $row = $dbw->fetchObject( $result ); + $dbw->freeResult( $result ); + } + + private static function lockName() { + global $wgDBname, $wgDBprefix; + return "MediaWiki.{$wgDBname}.{$wgDBprefix}FileStore"; + } + + /** + * Copy a file into the file store from elsewhere in the filesystem. + * Should be protected by FileStore::lock() to avoid race conditions. + * + * @param $key storage key string + * @param $flags + * DELETE_ORIGINAL - remove the source file on transaction commit. + * + * @throws FSException if copy can't be completed + * @return FSTransaction + */ + function insert( $key, $sourcePath, $flags=0 ) { + $destPath = $this->filePath( $key ); + return $this->copyFile( $sourcePath, $destPath, $flags ); + } + + /** + * Copy a file from the file store to elsewhere in the filesystem. + * Should be protected by FileStore::lock() to avoid race conditions. + * + * @param $key storage key string + * @param $flags + * DELETE_ORIGINAL - remove the source file on transaction commit. + * + * @throws FSException if copy can't be completed + * @return FSTransaction on success + */ + function export( $key, $destPath, $flags=0 ) { + $sourcePath = $this->filePath( $key ); + return $this->copyFile( $sourcePath, $destPath, $flags ); + } + + private function copyFile( $sourcePath, $destPath, $flags=0 ) { + $fname = __CLASS__ . '::' . __FUNCTION__; + + if( !file_exists( $sourcePath ) ) { + // Abort! Abort! + throw new FSException( "missing source file '$sourcePath'\n" ); + } + + $transaction = new FSTransaction(); + + if( $flags & self::DELETE_ORIGINAL ) { + $transaction->addCommit( FSTransaction::DELETE_FILE, $sourcePath ); + } + + if( file_exists( $destPath ) ) { + // An identical file is already present; no need to copy. + } else { + if( !file_exists( dirname( $destPath ) ) ) { + wfSuppressWarnings(); + $ok = mkdir( dirname( $destPath ), 0777, true ); + wfRestoreWarnings(); + + if( !$ok ) { + throw new FSException( + "failed to create directory for '$destPath'\n" ); + } + } + + wfSuppressWarnings(); + $ok = copy( $sourcePath, $destPath ); + wfRestoreWarnings(); + + if( $ok ) { + wfDebug( "$fname copied '$sourcePath' to '$destPath'\n" ); + $transaction->addRollback( FSTransaction::DELETE_FILE, $destPath ); + } else { + throw new FSException( + "$fname failed to copy '$sourcePath' to '$destPath'\n" ); + } + } + + return $transaction; + } + + /** + * Delete a file from the file store. + * Caller's responsibility to make sure it's not being used by another row. + * + * File is not actually removed until transaction commit. + * Should be protected by FileStore::lock() to avoid race conditions. + * + * @param $key storage key string + * @throws FSException if file can't be deleted + * @return FSTransaction + */ + function delete( $key ) { + $destPath = $this->filePath( $key ); + if( false === $destPath ) { + throw new FSExcepton( "file store does not contain file '$key'" ); + } else { + return FileStore::deleteFile( $destPath ); + } + } + + /** + * Delete a non-managed file on a transactional basis. + * + * File is not actually removed until transaction commit. + * Should be protected by FileStore::lock() to avoid race conditions. + * + * @param $path file to remove + * @throws FSException if file can't be deleted + * @return FSTransaction + * + * @fixme Might be worth preliminary permissions check + */ + static function deleteFile( $path ) { + if( file_exists( $path ) ) { + $transaction = new FSTransaction(); + $transaction->addCommit( FSTransaction::DELETE_FILE, $path ); + return $transaction; + } else { + throw new FSException( "cannot delete missing file '$path'" ); + } + } + + /** + * Stream a contained file directly to HTTP output. + * Will throw a 404 if file is missing; 400 if invalid key. + * @return true on success, false on failure + */ + function stream( $key ) { + $path = $this->filePath( $key ); + if( $path === false ) { + wfHttpError( 400, "Bad request", "Invalid or badly-formed filename." ); + return false; + } + + if( file_exists( $path ) ) { + // Set the filename for more convenient save behavior from browsers + // FIXME: Is this safe? + header( 'Content-Disposition: inline; filename="' . $key . '"' ); + + require_once 'StreamFile.php'; + wfStreamFile( $path ); + } else { + return wfHttpError( 404, "Not found", + "The requested resource does not exist." ); + } + } + + /** + * Confirm that the given file key is valid. + * Note that a valid key may refer to a file that does not exist. + * + * Key should consist of a 32-digit base-36 SHA-1 hash and + * an optional alphanumeric extension, all lowercase. + * The whole must not exceed 64 characters. + * + * @param $key + * @return boolean + */ + static function validKey( $key ) { + return preg_match( '/^[0-9a-z]{32}(\.[0-9a-z]{1,31})?$/', $key ); + } + + + /** + * Calculate file storage key from a file on disk. + * You must pass an extension to it, as some files may be calculated + * out of a temporary file etc. + * + * @param $path to file + * @param $extension + * @return string or false if could not open file or bad extension + */ + static function calculateKey( $path, $extension ) { + $fname = __CLASS__ . '::' . __FUNCTION__; + + wfSuppressWarnings(); + $hash = sha1_file( $path ); + wfRestoreWarnings(); + if( $hash === false ) { + wfDebug( "$fname: couldn't hash file '$path'\n" ); + return false; + } + + $base36 = wfBaseConvert( $hash, 16, 36, 32 ); + if( $extension == '' ) { + $key = $base36; + } else { + $key = $base36 . '.' . $extension; + } + + // Sanity check + if( self::validKey( $key ) ) { + return $key; + } else { + wfDebug( "$fname: generated bad key '$key'\n" ); + return false; + } + } + + /** + * Return filesystem path to the given file. + * Note that the file may or may not exist. + * @return string or false if an invalid key + */ + function filePath( $key ) { + if( self::validKey( $key ) ) { + return $this->mDirectory . DIRECTORY_SEPARATOR . + $this->hashPath( $key, DIRECTORY_SEPARATOR ); + } else { + return false; + } + } + + /** + * Return URL path to the given file, if the store is public. + * @return string or false if not public + */ + function urlPath( $key ) { + if( $this->mUrl && self::validKey( $key ) ) { + return $this->mUrl . '/' . $this->hashPath( $key, '/' ); + } else { + return false; + } + } + + private function hashPath( $key, $separator ) { + $parts = array(); + for( $i = 0; $i < $this->mHashLevel; $i++ ) { + $parts[] = $key{$i}; + } + $parts[] = $key; + return implode( $separator, $parts ); + } +} + +/** + * Wrapper for file store transaction stuff. + * + * FileStore methods may return one of these for undoable operations; + * you can then call its rollback() or commit() methods to perform + * final cleanup if dependent database work fails or succeeds. + */ +class FSTransaction { + const DELETE_FILE = 1; + + /** + * Combine more items into a fancier transaction + */ + function add( FSTransaction $transaction ) { + $this->mOnCommit = array_merge( + $this->mOnCommit, $transaction->mOnCommit ); + $this->mOnRollback = array_merge( + $this->mOnRollback, $transaction->mOnRollback ); + } + + /** + * Perform final actions for success. + * @return true if actions applied ok, false if errors + */ + function commit() { + return $this->apply( $this->mOnCommit ); + } + + /** + * Perform final actions for failure. + * @return true if actions applied ok, false if errors + */ + function rollback() { + return $this->apply( $this->mOnRollback ); + } + + // --- Private and friend functions below... + + function __construct() { + $this->mOnCommit = array(); + $this->mOnRollback = array(); + } + + function addCommit( $action, $path ) { + $this->mOnCommit[] = array( $action, $path ); + } + + function addRollback( $action, $path ) { + $this->mOnRollback[] = array( $action, $path ); + } + + private function apply( $actions ) { + $fname = __CLASS__ . '::' . __FUNCTION__; + $result = true; + foreach( $actions as $item ) { + list( $action, $path ) = $item; + if( $action == self::DELETE_FILE ) { + wfSuppressWarnings(); + $ok = unlink( $path ); + wfRestoreWarnings(); + if( $ok ) + wfDebug( "$fname: deleting file '$path'\n" ); + else + wfDebug( "$fname: failed to delete file '$path'\n" ); + $result = $result && $ok; + } + } + return $result; + } +} + +class FSException extends MWException { } + +?>
\ No newline at end of file |