summaryrefslogtreecommitdiff
path: root/includes/FileStore.php
diff options
context:
space:
mode:
authorPierre Schmitz <pierre@archlinux.de>2006-10-11 18:12:39 +0000
committerPierre Schmitz <pierre@archlinux.de>2006-10-11 18:12:39 +0000
commit183851b06bd6c52f3cae5375f433da720d410447 (patch)
treea477257decbf3360127f6739c2f9d0ec57a03d39 /includes/FileStore.php
MediaWiki 1.7.1 wiederhergestellt
Diffstat (limited to 'includes/FileStore.php')
-rw-r--r--includes/FileStore.php377
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