MediaWiki  master
FileOp.php
Go to the documentation of this file.
00001 <?php
00036 abstract class FileOp {
00038         protected $params = array();
00040         protected $backend;
00041 
00042         protected $state = self::STATE_NEW; // integer
00043         protected $failed = false; // boolean
00044         protected $async = false; // boolean
00045         protected $useLatest = true; // boolean
00046         protected $batchId; // string
00047 
00048         protected $doOperation = true; // boolean; operation is not a no-op
00049         protected $sourceSha1; // string
00050         protected $destSameAsSource; // boolean
00051 
00052         /* Object life-cycle */
00053         const STATE_NEW = 1;
00054         const STATE_CHECKED = 2;
00055         const STATE_ATTEMPTED = 3;
00056 
00064         final public function __construct( FileBackendStore $backend, array $params ) {
00065                 $this->backend = $backend;
00066                 list( $required, $optional ) = $this->allowedParams();
00067                 foreach ( $required as $name ) {
00068                         if ( isset( $params[$name] ) ) {
00069                                 $this->params[$name] = self::normalizeAnyStoragePaths( $params[$name] );
00070                         } else {
00071                                 throw new MWException( "File operation missing parameter '$name'." );
00072                         }
00073                 }
00074                 foreach ( $optional as $name ) {
00075                         if ( isset( $params[$name] ) ) {
00076                                 $this->params[$name] = self::normalizeAnyStoragePaths( $params[$name] );
00077                         }
00078                 }
00079                 $this->params = $params;
00080         }
00081 
00088         protected function normalizeAnyStoragePaths( $item ) {
00089                 if ( is_array( $item ) ) {
00090                         $res = array();
00091                         foreach ( $item as $k => $v ) {
00092                                 $k = self::normalizeIfValidStoragePath( $k );
00093                                 $v = self::normalizeIfValidStoragePath( $v );
00094                                 $res[$k] = $v;
00095                         }
00096                         return $res;
00097                 } else {
00098                         return self::normalizeIfValidStoragePath( $item );
00099                 }
00100         }
00101 
00108         protected static function normalizeIfValidStoragePath( $path ) {
00109                 if ( FileBackend::isStoragePath( $path ) ) {
00110                         $res = FileBackend::normalizeStoragePath( $path );
00111                         return ( $res !== null ) ? $res : $path;
00112                 }
00113                 return $path;
00114         }
00115 
00122         final public function setBatchId( $batchId ) {
00123                 $this->batchId = $batchId;
00124         }
00125 
00132         final public function allowStaleReads( $allowStale ) {
00133                 $this->useLatest = !$allowStale;
00134         }
00135 
00142         final public function getParam( $name ) {
00143                 return isset( $this->params[$name] ) ? $this->params[$name] : null;
00144         }
00145 
00151         final public function failed() {
00152                 return $this->failed;
00153         }
00154 
00160         final public static function newPredicates() {
00161                 return array( 'exists' => array(), 'sha1' => array() );
00162         }
00163 
00169         final public static function newDependencies() {
00170                 return array( 'read' => array(), 'write' => array() );
00171         }
00172 
00179         final public function applyDependencies( array $deps ) {
00180                 $deps['read']  += array_fill_keys( $this->storagePathsRead(), 1 );
00181                 $deps['write'] += array_fill_keys( $this->storagePathsChanged(), 1 );
00182                 return $deps;
00183         }
00184 
00191         final public function dependsOn( array $deps ) {
00192                 foreach ( $this->storagePathsChanged() as $path ) {
00193                         if ( isset( $deps['read'][$path] ) || isset( $deps['write'][$path] ) ) {
00194                                 return true; // "output" or "anti" dependency
00195                         }
00196                 }
00197                 foreach ( $this->storagePathsRead() as $path ) {
00198                         if ( isset( $deps['write'][$path] ) ) {
00199                                 return true; // "flow" dependency
00200                         }
00201                 }
00202                 return false;
00203         }
00204 
00212         final public function getJournalEntries( array $oPredicates, array $nPredicates ) {
00213                 if ( !$this->doOperation ) {
00214                         return array(); // this is a no-op
00215                 }
00216                 $nullEntries = array();
00217                 $updateEntries = array();
00218                 $deleteEntries = array();
00219                 $pathsUsed = array_merge( $this->storagePathsRead(), $this->storagePathsChanged() );
00220                 foreach ( array_unique( $pathsUsed ) as $path ) {
00221                         $nullEntries[] = array( // assertion for recovery
00222                                 'op'      => 'null',
00223                                 'path'    => $path,
00224                                 'newSha1' => $this->fileSha1( $path, $oPredicates )
00225                         );
00226                 }
00227                 foreach ( $this->storagePathsChanged() as $path ) {
00228                         if ( $nPredicates['sha1'][$path] === false ) { // deleted
00229                                 $deleteEntries[] = array(
00230                                         'op'      => 'delete',
00231                                         'path'    => $path,
00232                                         'newSha1' => ''
00233                                 );
00234                         } else { // created/updated
00235                                 $updateEntries[] = array(
00236                                         'op'      => $this->fileExists( $path, $oPredicates ) ? 'update' : 'create',
00237                                         'path'    => $path,
00238                                         'newSha1' => $nPredicates['sha1'][$path]
00239                                 );
00240                         }
00241                 }
00242                 return array_merge( $nullEntries, $updateEntries, $deleteEntries );
00243         }
00244 
00253         final public function precheck( array &$predicates ) {
00254                 if ( $this->state !== self::STATE_NEW ) {
00255                         return Status::newFatal( 'fileop-fail-state', self::STATE_NEW, $this->state );
00256                 }
00257                 $this->state = self::STATE_CHECKED;
00258                 $status = $this->doPrecheck( $predicates );
00259                 if ( !$status->isOK() ) {
00260                         $this->failed = true;
00261                 }
00262                 return $status;
00263         }
00264 
00268         protected function doPrecheck( array &$predicates ) {
00269                 return Status::newGood();
00270         }
00271 
00277         final public function attempt() {
00278                 if ( $this->state !== self::STATE_CHECKED ) {
00279                         return Status::newFatal( 'fileop-fail-state', self::STATE_CHECKED, $this->state );
00280                 } elseif ( $this->failed ) { // failed precheck
00281                         return Status::newFatal( 'fileop-fail-attempt-precheck' );
00282                 }
00283                 $this->state = self::STATE_ATTEMPTED;
00284                 if ( $this->doOperation ) {
00285                         $status = $this->doAttempt();
00286                         if ( !$status->isOK() ) {
00287                                 $this->failed = true;
00288                                 $this->logFailure( 'attempt' );
00289                         }
00290                 } else { // no-op
00291                         $status = Status::newGood();
00292                 }
00293                 return $status;
00294         }
00295 
00299         protected function doAttempt() {
00300                 return Status::newGood();
00301         }
00302 
00308         final public function attemptAsync() {
00309                 $this->async = true;
00310                 $result = $this->attempt();
00311                 $this->async = false;
00312                 return $result;
00313         }
00314 
00320         protected function allowedParams() {
00321                 return array( array(), array() );
00322         }
00323 
00330         protected function setFlags( array $params ) {
00331                 return array( 'async' => $this->async ) + $params;
00332         }
00333 
00339         public function storagePathsRead() {
00340                 return array();
00341         }
00342 
00348         public function storagePathsChanged() {
00349                 return array();
00350         }
00351 
00360         protected function precheckDestExistence( array $predicates ) {
00361                 $status = Status::newGood();
00362                 // Get hash of source file/string and the destination file
00363                 $this->sourceSha1 = $this->getSourceSha1Base36(); // FS file or data string
00364                 if ( $this->sourceSha1 === null ) { // file in storage?
00365                         $this->sourceSha1 = $this->fileSha1( $this->params['src'], $predicates );
00366                 }
00367                 $this->destSameAsSource = false;
00368                 if ( $this->fileExists( $this->params['dst'], $predicates ) ) {
00369                         if ( $this->getParam( 'overwrite' ) ) {
00370                                 return $status; // OK
00371                         } elseif ( $this->getParam( 'overwriteSame' ) ) {
00372                                 $dhash = $this->fileSha1( $this->params['dst'], $predicates );
00373                                 // Check if hashes are valid and match each other...
00374                                 if ( !strlen( $this->sourceSha1 ) || !strlen( $dhash ) ) {
00375                                         $status->fatal( 'backend-fail-hashes' );
00376                                 } elseif ( $this->sourceSha1 !== $dhash ) {
00377                                         // Give an error if the files are not identical
00378                                         $status->fatal( 'backend-fail-notsame', $this->params['dst'] );
00379                                 } else {
00380                                         $this->destSameAsSource = true; // OK
00381                                 }
00382                                 return $status; // do nothing; either OK or bad status
00383                         } else {
00384                                 $status->fatal( 'backend-fail-alreadyexists', $this->params['dst'] );
00385                                 return $status;
00386                         }
00387                 }
00388                 return $status;
00389         }
00390 
00397         protected function getSourceSha1Base36() {
00398                 return null; // N/A
00399         }
00400 
00408         final protected function fileExists( $source, array $predicates ) {
00409                 if ( isset( $predicates['exists'][$source] ) ) {
00410                         return $predicates['exists'][$source]; // previous op assures this
00411                 } else {
00412                         $params = array( 'src' => $source, 'latest' => $this->useLatest );
00413                         return $this->backend->fileExists( $params );
00414                 }
00415         }
00416 
00424         final protected function fileSha1( $source, array $predicates ) {
00425                 if ( isset( $predicates['sha1'][$source] ) ) {
00426                         return $predicates['sha1'][$source]; // previous op assures this
00427                 } elseif ( isset( $predicates['exists'][$source] ) && !$predicates['exists'][$source] ) {
00428                         return false; // previous op assures this
00429                 } else {
00430                         $params = array( 'src' => $source, 'latest' => $this->useLatest );
00431                         return $this->backend->getFileSha1Base36( $params );
00432                 }
00433         }
00434 
00440         public function getBackend() {
00441                 return $this->backend;
00442         }
00443 
00450         final public function logFailure( $action ) {
00451                 $params = $this->params;
00452                 $params['failedAction'] = $action;
00453                 try {
00454                         wfDebugLog( 'FileOperation', get_class( $this ) .
00455                                 " failed (batch #{$this->batchId}): " . FormatJson::encode( $params ) );
00456                 } catch ( Exception $e ) {
00457                         // bad config? debug log error?
00458                 }
00459         }
00460 }
00461 
00466 class StoreFileOp extends FileOp {
00470         protected function allowedParams() {
00471                 return array( array( 'src', 'dst' ),
00472                         array( 'overwrite', 'overwriteSame', 'disposition' ) );
00473         }
00474 
00479         protected function doPrecheck( array &$predicates ) {
00480                 $status = Status::newGood();
00481                 // Check if the source file exists on the file system
00482                 if ( !is_file( $this->params['src'] ) ) {
00483                         $status->fatal( 'backend-fail-notexists', $this->params['src'] );
00484                         return $status;
00485                 // Check if the source file is too big
00486                 } elseif ( filesize( $this->params['src'] ) > $this->backend->maxFileSizeInternal() ) {
00487                         $status->fatal( 'backend-fail-maxsize',
00488                                 $this->params['dst'], $this->backend->maxFileSizeInternal() );
00489                         $status->fatal( 'backend-fail-store', $this->params['src'], $this->params['dst'] );
00490                         return $status;
00491                 // Check if a file can be placed/changed at the destination
00492                 } elseif ( !$this->backend->isPathUsableInternal( $this->params['dst'] ) ) {
00493                         $status->fatal( 'backend-fail-usable', $this->params['dst'] );
00494                         $status->fatal( 'backend-fail-store', $this->params['src'], $this->params['dst'] );
00495                         return $status;
00496                 }
00497                 // Check if destination file exists
00498                 $status->merge( $this->precheckDestExistence( $predicates ) );
00499                 if ( $status->isOK() ) {
00500                         // Update file existence predicates
00501                         $predicates['exists'][$this->params['dst']] = true;
00502                         $predicates['sha1'][$this->params['dst']] = $this->sourceSha1;
00503                 }
00504                 return $status; // safe to call attempt()
00505         }
00506 
00510         protected function doAttempt() {
00511                 // Store the file at the destination
00512                 if ( !$this->destSameAsSource ) {
00513                         return $this->backend->storeInternal( $this->setFlags( $this->params ) );
00514                 }
00515                 return Status::newGood();
00516         }
00517 
00521         protected function getSourceSha1Base36() {
00522                 wfSuppressWarnings();
00523                 $hash = sha1_file( $this->params['src'] );
00524                 wfRestoreWarnings();
00525                 if ( $hash !== false ) {
00526                         $hash = wfBaseConvert( $hash, 16, 36, 31 );
00527                 }
00528                 return $hash;
00529         }
00530 
00531         public function storagePathsChanged() {
00532                 return array( $this->params['dst'] );
00533         }
00534 }
00535 
00540 class CreateFileOp extends FileOp {
00541         protected function allowedParams() {
00542                 return array( array( 'content', 'dst' ),
00543                         array( 'overwrite', 'overwriteSame', 'disposition' ) );
00544         }
00545 
00546         protected function doPrecheck( array &$predicates ) {
00547                 $status = Status::newGood();
00548                 // Check if the source data is too big
00549                 if ( strlen( $this->getParam( 'content' ) ) > $this->backend->maxFileSizeInternal() ) {
00550                         $status->fatal( 'backend-fail-maxsize',
00551                                 $this->params['dst'], $this->backend->maxFileSizeInternal() );
00552                         $status->fatal( 'backend-fail-create', $this->params['dst'] );
00553                         return $status;
00554                 // Check if a file can be placed/changed at the destination
00555                 } elseif ( !$this->backend->isPathUsableInternal( $this->params['dst'] ) ) {
00556                         $status->fatal( 'backend-fail-usable', $this->params['dst'] );
00557                         $status->fatal( 'backend-fail-create', $this->params['dst'] );
00558                         return $status;
00559                 }
00560                 // Check if destination file exists
00561                 $status->merge( $this->precheckDestExistence( $predicates ) );
00562                 if ( $status->isOK() ) {
00563                         // Update file existence predicates
00564                         $predicates['exists'][$this->params['dst']] = true;
00565                         $predicates['sha1'][$this->params['dst']] = $this->sourceSha1;
00566                 }
00567                 return $status; // safe to call attempt()
00568         }
00569 
00573         protected function doAttempt() {
00574                 if ( !$this->destSameAsSource ) {
00575                         // Create the file at the destination
00576                         return $this->backend->createInternal( $this->setFlags( $this->params ) );
00577                 }
00578                 return Status::newGood();
00579         }
00580 
00584         protected function getSourceSha1Base36() {
00585                 return wfBaseConvert( sha1( $this->params['content'] ), 16, 36, 31 );
00586         }
00587 
00591         public function storagePathsChanged() {
00592                 return array( $this->params['dst'] );
00593         }
00594 }
00595 
00600 class CopyFileOp extends FileOp {
00604         protected function allowedParams() {
00605                 return array( array( 'src', 'dst' ),
00606                         array( 'overwrite', 'overwriteSame', 'ignoreMissingSource', 'disposition' ) );
00607         }
00608 
00613         protected function doPrecheck( array &$predicates ) {
00614                 $status = Status::newGood();
00615                 // Check if the source file exists
00616                 if ( !$this->fileExists( $this->params['src'], $predicates ) ) {
00617                         if ( $this->getParam( 'ignoreMissingSource' ) ) {
00618                                 $this->doOperation = false; // no-op
00619                                 // Update file existence predicates (cache 404s)
00620                                 $predicates['exists'][$this->params['src']] = false;
00621                                 $predicates['sha1'][$this->params['src']] = false;
00622                                 return $status; // nothing to do
00623                         } else {
00624                                 $status->fatal( 'backend-fail-notexists', $this->params['src'] );
00625                                 return $status;
00626                         }
00627                 // Check if a file can be placed/changed at the destination
00628                 } elseif ( !$this->backend->isPathUsableInternal( $this->params['dst'] ) ) {
00629                         $status->fatal( 'backend-fail-usable', $this->params['dst'] );
00630                         $status->fatal( 'backend-fail-copy', $this->params['src'], $this->params['dst'] );
00631                         return $status;
00632                 }
00633                 // Check if destination file exists
00634                 $status->merge( $this->precheckDestExistence( $predicates ) );
00635                 if ( $status->isOK() ) {
00636                         // Update file existence predicates
00637                         $predicates['exists'][$this->params['dst']] = true;
00638                         $predicates['sha1'][$this->params['dst']] = $this->sourceSha1;
00639                 }
00640                 return $status; // safe to call attempt()
00641         }
00642 
00646         protected function doAttempt() {
00647                 // Do nothing if the src/dst paths are the same
00648                 if ( $this->params['src'] !== $this->params['dst'] ) {
00649                         // Copy the file into the destination
00650                         if ( !$this->destSameAsSource ) {
00651                                 return $this->backend->copyInternal( $this->setFlags( $this->params ) );
00652                         }
00653                 }
00654                 return Status::newGood();
00655         }
00656 
00660         public function storagePathsRead() {
00661                 return array( $this->params['src'] );
00662         }
00663 
00667         public function storagePathsChanged() {
00668                 return array( $this->params['dst'] );
00669         }
00670 }
00671 
00676 class MoveFileOp extends FileOp {
00680         protected function allowedParams() {
00681                 return array( array( 'src', 'dst' ),
00682                         array( 'overwrite', 'overwriteSame', 'ignoreMissingSource', 'disposition' ) );
00683         }
00684 
00689         protected function doPrecheck( array &$predicates ) {
00690                 $status = Status::newGood();
00691                 // Check if the source file exists
00692                 if ( !$this->fileExists( $this->params['src'], $predicates ) ) {
00693                         if ( $this->getParam( 'ignoreMissingSource' ) ) {
00694                                 $this->doOperation = false; // no-op
00695                                 // Update file existence predicates (cache 404s)
00696                                 $predicates['exists'][$this->params['src']] = false;
00697                                 $predicates['sha1'][$this->params['src']] = false;
00698                                 return $status; // nothing to do
00699                         } else {
00700                                 $status->fatal( 'backend-fail-notexists', $this->params['src'] );
00701                                 return $status;
00702                         }
00703                 // Check if a file can be placed/changed at the destination
00704                 } elseif ( !$this->backend->isPathUsableInternal( $this->params['dst'] ) ) {
00705                         $status->fatal( 'backend-fail-usable', $this->params['dst'] );
00706                         $status->fatal( 'backend-fail-move', $this->params['src'], $this->params['dst'] );
00707                         return $status;
00708                 }
00709                 // Check if destination file exists
00710                 $status->merge( $this->precheckDestExistence( $predicates ) );
00711                 if ( $status->isOK() ) {
00712                         // Update file existence predicates
00713                         $predicates['exists'][$this->params['src']] = false;
00714                         $predicates['sha1'][$this->params['src']] = false;
00715                         $predicates['exists'][$this->params['dst']] = true;
00716                         $predicates['sha1'][$this->params['dst']] = $this->sourceSha1;
00717                 }
00718                 return $status; // safe to call attempt()
00719         }
00720 
00724         protected function doAttempt() {
00725                 // Do nothing if the src/dst paths are the same
00726                 if ( $this->params['src'] !== $this->params['dst'] ) {
00727                         if ( !$this->destSameAsSource ) {
00728                                 // Move the file into the destination
00729                                 return $this->backend->moveInternal( $this->setFlags( $this->params ) );
00730                         } else {
00731                                 // Just delete source as the destination needs no changes
00732                                 $params = array( 'src' => $this->params['src'] );
00733                                 return $this->backend->deleteInternal( $this->setFlags( $params ) );
00734                         }
00735                 }
00736                 return Status::newGood();
00737         }
00738 
00742         public function storagePathsRead() {
00743                 return array( $this->params['src'] );
00744         }
00745 
00749         public function storagePathsChanged() {
00750                 return array( $this->params['src'], $this->params['dst'] );
00751         }
00752 }
00753 
00758 class DeleteFileOp extends FileOp {
00762         protected function allowedParams() {
00763                 return array( array( 'src' ), array( 'ignoreMissingSource' ) );
00764         }
00765 
00770         protected function doPrecheck( array &$predicates ) {
00771                 $status = Status::newGood();
00772                 // Check if the source file exists
00773                 if ( !$this->fileExists( $this->params['src'], $predicates ) ) {
00774                         if ( $this->getParam( 'ignoreMissingSource' ) ) {
00775                                 $this->doOperation = false; // no-op
00776                                 // Update file existence predicates (cache 404s)
00777                                 $predicates['exists'][$this->params['src']] = false;
00778                                 $predicates['sha1'][$this->params['src']] = false;
00779                                 return $status; // nothing to do
00780                         } else {
00781                                 $status->fatal( 'backend-fail-notexists', $this->params['src'] );
00782                                 return $status;
00783                         }
00784                 // Check if a file can be placed/changed at the source
00785                 } elseif ( !$this->backend->isPathUsableInternal( $this->params['src'] ) ) {
00786                         $status->fatal( 'backend-fail-usable', $this->params['src'] );
00787                         $status->fatal( 'backend-fail-delete', $this->params['src'] );
00788                         return $status;
00789                 }
00790                 // Update file existence predicates
00791                 $predicates['exists'][$this->params['src']] = false;
00792                 $predicates['sha1'][$this->params['src']] = false;
00793                 return $status; // safe to call attempt()
00794         }
00795 
00799         protected function doAttempt() {
00800                 // Delete the source file
00801                 return $this->backend->deleteInternal( $this->setFlags( $this->params ) );
00802         }
00803 
00807         public function storagePathsChanged() {
00808                 return array( $this->params['src'] );
00809         }
00810 }
00811 
00815 class NullFileOp extends FileOp {}