wiki.techinc.nl/includes/FileStore.php
Tim Starling e174a4ddfb Abolished $wgDBname as a unique wiki identifier, it doesn't work with the new-fangled feature we call "table prefixes". Instead use wfWikiID() for an identifier containing the DB name and the prefix if there is one, and wfMemcKey() for cache key construction.
Caches for wikis with table prefixes will be lost on upgrade, caches for wikis without table prefixes will be preserved. Custom cache keys in extensions can be migrated at leisure. Extensions which write to core cache keys should be migrated ASAP, as I have done with Special:Makesysop.
2006-10-04 09:06:18 +00:00

367 lines
9.5 KiB
PHP

<?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() {
$dbw = wfGetDB( DB_MASTER );
$lockname = $dbw->addQuotes( FileStore::lockName() );
$result = $dbw->query( "SELECT GET_LOCK($lockname, 5) AS lockstatus", __METHOD__ );
$row = $dbw->fetchObject( $result );
$dbw->freeResult( $result );
if( $row->lockstatus == 1 ) {
return true;
} else {
wfDebug( __METHOD__." failed to acquire lock\n" );
return false;
}
}
/**
* Release the global file store lock.
*/
static function unlock() {
$dbw = wfGetDB( DB_MASTER );
$lockname = $dbw->addQuotes( FileStore::lockName() );
$result = $dbw->query( "SELECT RELEASE_LOCK($lockname)", __METHOD__ );
$row = $dbw->fetchObject( $result );
$dbw->freeResult( $result );
}
private static function lockName() {
return 'MediaWiki.' . wfWikiID() . '.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 ) {
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( __METHOD__." copied '$sourcePath' to '$destPath'\n" );
$transaction->addRollback( FSTransaction::DELETE_FILE, $destPath );
} else {
throw new FSException(
__METHOD__." 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 ) {
wfSuppressWarnings();
$hash = sha1_file( $path );
wfRestoreWarnings();
if( $hash === false ) {
wfDebug( __METHOD__.": 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( __METHOD__.": 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 ) {
$result = true;
foreach( $actions as $item ) {
list( $action, $path ) = $item;
if( $action == self::DELETE_FILE ) {
wfSuppressWarnings();
$ok = unlink( $path );
wfRestoreWarnings();
if( $ok )
wfDebug( __METHOD__.": deleting file '$path'\n" );
else
wfDebug( __METHOD__.": failed to delete file '$path'\n" );
$result = $result && $ok;
}
}
return $result;
}
}
class FSException extends MWException { }
?>