MediaWiki  REL1_21
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 $batchId; // string
00046 
00047         protected $doOperation = true; // boolean; operation is not a no-op
00048         protected $sourceSha1; // string
00049         protected $destSameAsSource; // boolean
00050         protected $destExists; // 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 getParam( $name ) {
00133                 return isset( $this->params[$name] ) ? $this->params[$name] : null;
00134         }
00135 
00141         final public function failed() {
00142                 return $this->failed;
00143         }
00144 
00150         final public static function newPredicates() {
00151                 return array( 'exists' => array(), 'sha1' => array() );
00152         }
00153 
00159         final public static function newDependencies() {
00160                 return array( 'read' => array(), 'write' => array() );
00161         }
00162 
00169         final public function applyDependencies( array $deps ) {
00170                 $deps['read'] += array_fill_keys( $this->storagePathsRead(), 1 );
00171                 $deps['write'] += array_fill_keys( $this->storagePathsChanged(), 1 );
00172                 return $deps;
00173         }
00174 
00181         final public function dependsOn( array $deps ) {
00182                 foreach ( $this->storagePathsChanged() as $path ) {
00183                         if ( isset( $deps['read'][$path] ) || isset( $deps['write'][$path] ) ) {
00184                                 return true; // "output" or "anti" dependency
00185                         }
00186                 }
00187                 foreach ( $this->storagePathsRead() as $path ) {
00188                         if ( isset( $deps['write'][$path] ) ) {
00189                                 return true; // "flow" dependency
00190                         }
00191                 }
00192                 return false;
00193         }
00194 
00202         final public function getJournalEntries( array $oPredicates, array $nPredicates ) {
00203                 if ( !$this->doOperation ) {
00204                         return array(); // this is a no-op
00205                 }
00206                 $nullEntries = array();
00207                 $updateEntries = array();
00208                 $deleteEntries = array();
00209                 $pathsUsed = array_merge( $this->storagePathsRead(), $this->storagePathsChanged() );
00210                 foreach ( array_unique( $pathsUsed ) as $path ) {
00211                         $nullEntries[] = array( // assertion for recovery
00212                                 'op'      => 'null',
00213                                 'path'    => $path,
00214                                 'newSha1' => $this->fileSha1( $path, $oPredicates )
00215                         );
00216                 }
00217                 foreach ( $this->storagePathsChanged() as $path ) {
00218                         if ( $nPredicates['sha1'][$path] === false ) { // deleted
00219                                 $deleteEntries[] = array(
00220                                         'op'      => 'delete',
00221                                         'path'    => $path,
00222                                         'newSha1' => ''
00223                                 );
00224                         } else { // created/updated
00225                                 $updateEntries[] = array(
00226                                         'op'      => $this->fileExists( $path, $oPredicates ) ? 'update' : 'create',
00227                                         'path'    => $path,
00228                                         'newSha1' => $nPredicates['sha1'][$path]
00229                                 );
00230                         }
00231                 }
00232                 return array_merge( $nullEntries, $updateEntries, $deleteEntries );
00233         }
00234 
00243         final public function precheck( array &$predicates ) {
00244                 if ( $this->state !== self::STATE_NEW ) {
00245                         return Status::newFatal( 'fileop-fail-state', self::STATE_NEW, $this->state );
00246                 }
00247                 $this->state = self::STATE_CHECKED;
00248                 $status = $this->doPrecheck( $predicates );
00249                 if ( !$status->isOK() ) {
00250                         $this->failed = true;
00251                 }
00252                 return $status;
00253         }
00254 
00258         protected function doPrecheck( array &$predicates ) {
00259                 return Status::newGood();
00260         }
00261 
00267         final public function attempt() {
00268                 if ( $this->state !== self::STATE_CHECKED ) {
00269                         return Status::newFatal( 'fileop-fail-state', self::STATE_CHECKED, $this->state );
00270                 } elseif ( $this->failed ) { // failed precheck
00271                         return Status::newFatal( 'fileop-fail-attempt-precheck' );
00272                 }
00273                 $this->state = self::STATE_ATTEMPTED;
00274                 if ( $this->doOperation ) {
00275                         $status = $this->doAttempt();
00276                         if ( !$status->isOK() ) {
00277                                 $this->failed = true;
00278                                 $this->logFailure( 'attempt' );
00279                         }
00280                 } else { // no-op
00281                         $status = Status::newGood();
00282                 }
00283                 return $status;
00284         }
00285 
00289         protected function doAttempt() {
00290                 return Status::newGood();
00291         }
00292 
00298         final public function attemptAsync() {
00299                 $this->async = true;
00300                 $result = $this->attempt();
00301                 $this->async = false;
00302                 return $result;
00303         }
00304 
00310         protected function allowedParams() {
00311                 return array( array(), array() );
00312         }
00313 
00320         protected function setFlags( array $params ) {
00321                 return array( 'async' => $this->async ) + $params;
00322         }
00323 
00329         public function storagePathsRead() {
00330                 return array();
00331         }
00332 
00338         public function storagePathsChanged() {
00339                 return array();
00340         }
00341 
00350         protected function precheckDestExistence( array $predicates ) {
00351                 $status = Status::newGood();
00352                 // Get hash of source file/string and the destination file
00353                 $this->sourceSha1 = $this->getSourceSha1Base36(); // FS file or data string
00354                 if ( $this->sourceSha1 === null ) { // file in storage?
00355                         $this->sourceSha1 = $this->fileSha1( $this->params['src'], $predicates );
00356                 }
00357                 $this->destSameAsSource = false;
00358                 $this->destExists = $this->fileExists( $this->params['dst'], $predicates );
00359                 if ( $this->destExists ) {
00360                         if ( $this->getParam( 'overwrite' ) ) {
00361                                 return $status; // OK
00362                         } elseif ( $this->getParam( 'overwriteSame' ) ) {
00363                                 $dhash = $this->fileSha1( $this->params['dst'], $predicates );
00364                                 // Check if hashes are valid and match each other...
00365                                 if ( !strlen( $this->sourceSha1 ) || !strlen( $dhash ) ) {
00366                                         $status->fatal( 'backend-fail-hashes' );
00367                                 } elseif ( $this->sourceSha1 !== $dhash ) {
00368                                         // Give an error if the files are not identical
00369                                         $status->fatal( 'backend-fail-notsame', $this->params['dst'] );
00370                                 } else {
00371                                         $this->destSameAsSource = true; // OK
00372                                 }
00373                                 return $status; // do nothing; either OK or bad status
00374                         } else {
00375                                 $status->fatal( 'backend-fail-alreadyexists', $this->params['dst'] );
00376                                 return $status;
00377                         }
00378                 }
00379                 return $status;
00380         }
00381 
00388         protected function getSourceSha1Base36() {
00389                 return null; // N/A
00390         }
00391 
00399         final protected function fileExists( $source, array $predicates ) {
00400                 if ( isset( $predicates['exists'][$source] ) ) {
00401                         return $predicates['exists'][$source]; // previous op assures this
00402                 } else {
00403                         $params = array( 'src' => $source, 'latest' => true );
00404                         return $this->backend->fileExists( $params );
00405                 }
00406         }
00407 
00415         final protected function fileSha1( $source, array $predicates ) {
00416                 if ( isset( $predicates['sha1'][$source] ) ) {
00417                         return $predicates['sha1'][$source]; // previous op assures this
00418                 } elseif ( isset( $predicates['exists'][$source] ) && !$predicates['exists'][$source] ) {
00419                         return false; // previous op assures this
00420                 } else {
00421                         $params = array( 'src' => $source, 'latest' => true );
00422                         return $this->backend->getFileSha1Base36( $params );
00423                 }
00424         }
00425 
00431         public function getBackend() {
00432                 return $this->backend;
00433         }
00434 
00441         final public function logFailure( $action ) {
00442                 $params = $this->params;
00443                 $params['failedAction'] = $action;
00444                 try {
00445                         wfDebugLog( 'FileOperation', get_class( $this ) .
00446                                 " failed (batch #{$this->batchId}): " . FormatJson::encode( $params ) );
00447                 } catch ( Exception $e ) {
00448                         // bad config? debug log error?
00449                 }
00450         }
00451 }
00452 
00457 class CreateFileOp extends FileOp {
00458         protected function allowedParams() {
00459                 return array( array( 'content', 'dst' ),
00460                         array( 'overwrite', 'overwriteSame', 'disposition', 'headers' ) );
00461         }
00462 
00463         protected function doPrecheck( array &$predicates ) {
00464                 $status = Status::newGood();
00465                 // Check if the source data is too big
00466                 if ( strlen( $this->getParam( 'content' ) ) > $this->backend->maxFileSizeInternal() ) {
00467                         $status->fatal( 'backend-fail-maxsize',
00468                                 $this->params['dst'], $this->backend->maxFileSizeInternal() );
00469                         $status->fatal( 'backend-fail-create', $this->params['dst'] );
00470                         return $status;
00471                 // Check if a file can be placed/changed at the destination
00472                 } elseif ( !$this->backend->isPathUsableInternal( $this->params['dst'] ) ) {
00473                         $status->fatal( 'backend-fail-usable', $this->params['dst'] );
00474                         $status->fatal( 'backend-fail-create', $this->params['dst'] );
00475                         return $status;
00476                 }
00477                 // Check if destination file exists
00478                 $status->merge( $this->precheckDestExistence( $predicates ) );
00479                 $this->params['dstExists'] = $this->destExists; // see FileBackendStore::setFileCache()
00480                 if ( $status->isOK() ) {
00481                         // Update file existence predicates
00482                         $predicates['exists'][$this->params['dst']] = true;
00483                         $predicates['sha1'][$this->params['dst']] = $this->sourceSha1;
00484                 }
00485                 return $status; // safe to call attempt()
00486         }
00487 
00491         protected function doAttempt() {
00492                 if ( !$this->destSameAsSource ) {
00493                         // Create the file at the destination
00494                         return $this->backend->createInternal( $this->setFlags( $this->params ) );
00495                 }
00496                 return Status::newGood();
00497         }
00498 
00502         protected function getSourceSha1Base36() {
00503                 return wfBaseConvert( sha1( $this->params['content'] ), 16, 36, 31 );
00504         }
00505 
00509         public function storagePathsChanged() {
00510                 return array( $this->params['dst'] );
00511         }
00512 }
00513 
00518 class StoreFileOp extends FileOp {
00522         protected function allowedParams() {
00523                 return array( array( 'src', 'dst' ),
00524                         array( 'overwrite', 'overwriteSame', 'disposition', 'headers' ) );
00525         }
00526 
00531         protected function doPrecheck( array &$predicates ) {
00532                 $status = Status::newGood();
00533                 // Check if the source file exists on the file system
00534                 if ( !is_file( $this->params['src'] ) ) {
00535                         $status->fatal( 'backend-fail-notexists', $this->params['src'] );
00536                         return $status;
00537                 // Check if the source file is too big
00538                 } elseif ( filesize( $this->params['src'] ) > $this->backend->maxFileSizeInternal() ) {
00539                         $status->fatal( 'backend-fail-maxsize',
00540                                 $this->params['dst'], $this->backend->maxFileSizeInternal() );
00541                         $status->fatal( 'backend-fail-store', $this->params['src'], $this->params['dst'] );
00542                         return $status;
00543                 // Check if a file can be placed/changed at the destination
00544                 } elseif ( !$this->backend->isPathUsableInternal( $this->params['dst'] ) ) {
00545                         $status->fatal( 'backend-fail-usable', $this->params['dst'] );
00546                         $status->fatal( 'backend-fail-store', $this->params['src'], $this->params['dst'] );
00547                         return $status;
00548                 }
00549                 // Check if destination file exists
00550                 $status->merge( $this->precheckDestExistence( $predicates ) );
00551                 $this->params['dstExists'] = $this->destExists; // see FileBackendStore::setFileCache()
00552                 if ( $status->isOK() ) {
00553                         // Update file existence predicates
00554                         $predicates['exists'][$this->params['dst']] = true;
00555                         $predicates['sha1'][$this->params['dst']] = $this->sourceSha1;
00556                 }
00557                 return $status; // safe to call attempt()
00558         }
00559 
00563         protected function doAttempt() {
00564                 // Store the file at the destination
00565                 if ( !$this->destSameAsSource ) {
00566                         return $this->backend->storeInternal( $this->setFlags( $this->params ) );
00567                 }
00568                 return Status::newGood();
00569         }
00570 
00574         protected function getSourceSha1Base36() {
00575                 wfSuppressWarnings();
00576                 $hash = sha1_file( $this->params['src'] );
00577                 wfRestoreWarnings();
00578                 if ( $hash !== false ) {
00579                         $hash = wfBaseConvert( $hash, 16, 36, 31 );
00580                 }
00581                 return $hash;
00582         }
00583 
00584         public function storagePathsChanged() {
00585                 return array( $this->params['dst'] );
00586         }
00587 }
00588 
00593 class CopyFileOp extends FileOp {
00597         protected function allowedParams() {
00598                 return array( array( 'src', 'dst' ),
00599                         array( 'overwrite', 'overwriteSame', 'ignoreMissingSource', 'disposition' ) );
00600         }
00601 
00606         protected function doPrecheck( array &$predicates ) {
00607                 $status = Status::newGood();
00608                 // Check if the source file exists
00609                 if ( !$this->fileExists( $this->params['src'], $predicates ) ) {
00610                         if ( $this->getParam( 'ignoreMissingSource' ) ) {
00611                                 $this->doOperation = false; // no-op
00612                                 // Update file existence predicates (cache 404s)
00613                                 $predicates['exists'][$this->params['src']] = false;
00614                                 $predicates['sha1'][$this->params['src']] = false;
00615                                 return $status; // nothing to do
00616                         } else {
00617                                 $status->fatal( 'backend-fail-notexists', $this->params['src'] );
00618                                 return $status;
00619                         }
00620                 // Check if a file can be placed/changed at the destination
00621                 } elseif ( !$this->backend->isPathUsableInternal( $this->params['dst'] ) ) {
00622                         $status->fatal( 'backend-fail-usable', $this->params['dst'] );
00623                         $status->fatal( 'backend-fail-copy', $this->params['src'], $this->params['dst'] );
00624                         return $status;
00625                 }
00626                 // Check if destination file exists
00627                 $status->merge( $this->precheckDestExistence( $predicates ) );
00628                 $this->params['dstExists'] = $this->destExists; // see FileBackendStore::setFileCache()
00629                 if ( $status->isOK() ) {
00630                         // Update file existence predicates
00631                         $predicates['exists'][$this->params['dst']] = true;
00632                         $predicates['sha1'][$this->params['dst']] = $this->sourceSha1;
00633                 }
00634                 return $status; // safe to call attempt()
00635         }
00636 
00640         protected function doAttempt() {
00641                 // Do nothing if the src/dst paths are the same
00642                 if ( $this->params['src'] !== $this->params['dst'] ) {
00643                         // Copy the file into the destination
00644                         if ( !$this->destSameAsSource ) {
00645                                 return $this->backend->copyInternal( $this->setFlags( $this->params ) );
00646                         }
00647                 }
00648                 return Status::newGood();
00649         }
00650 
00654         public function storagePathsRead() {
00655                 return array( $this->params['src'] );
00656         }
00657 
00661         public function storagePathsChanged() {
00662                 return array( $this->params['dst'] );
00663         }
00664 }
00665 
00670 class MoveFileOp extends FileOp {
00674         protected function allowedParams() {
00675                 return array( array( 'src', 'dst' ),
00676                         array( 'overwrite', 'overwriteSame', 'ignoreMissingSource', 'disposition' ) );
00677         }
00678 
00683         protected function doPrecheck( array &$predicates ) {
00684                 $status = Status::newGood();
00685                 // Check if the source file exists
00686                 if ( !$this->fileExists( $this->params['src'], $predicates ) ) {
00687                         if ( $this->getParam( 'ignoreMissingSource' ) ) {
00688                                 $this->doOperation = false; // no-op
00689                                 // Update file existence predicates (cache 404s)
00690                                 $predicates['exists'][$this->params['src']] = false;
00691                                 $predicates['sha1'][$this->params['src']] = false;
00692                                 return $status; // nothing to do
00693                         } else {
00694                                 $status->fatal( 'backend-fail-notexists', $this->params['src'] );
00695                                 return $status;
00696                         }
00697                 // Check if a file can be placed/changed at the destination
00698                 } elseif ( !$this->backend->isPathUsableInternal( $this->params['dst'] ) ) {
00699                         $status->fatal( 'backend-fail-usable', $this->params['dst'] );
00700                         $status->fatal( 'backend-fail-move', $this->params['src'], $this->params['dst'] );
00701                         return $status;
00702                 }
00703                 // Check if destination file exists
00704                 $status->merge( $this->precheckDestExistence( $predicates ) );
00705                 $this->params['dstExists'] = $this->destExists; // see FileBackendStore::setFileCache()
00706                 if ( $status->isOK() ) {
00707                         // Update file existence predicates
00708                         $predicates['exists'][$this->params['src']] = false;
00709                         $predicates['sha1'][$this->params['src']] = false;
00710                         $predicates['exists'][$this->params['dst']] = true;
00711                         $predicates['sha1'][$this->params['dst']] = $this->sourceSha1;
00712                 }
00713                 return $status; // safe to call attempt()
00714         }
00715 
00719         protected function doAttempt() {
00720                 // Do nothing if the src/dst paths are the same
00721                 if ( $this->params['src'] !== $this->params['dst'] ) {
00722                         if ( !$this->destSameAsSource ) {
00723                                 // Move the file into the destination
00724                                 return $this->backend->moveInternal( $this->setFlags( $this->params ) );
00725                         } else {
00726                                 // Just delete source as the destination needs no changes
00727                                 $params = array( 'src' => $this->params['src'] );
00728                                 return $this->backend->deleteInternal( $this->setFlags( $params ) );
00729                         }
00730                 }
00731                 return Status::newGood();
00732         }
00733 
00737         public function storagePathsRead() {
00738                 return array( $this->params['src'] );
00739         }
00740 
00744         public function storagePathsChanged() {
00745                 return array( $this->params['src'], $this->params['dst'] );
00746         }
00747 }
00748 
00753 class DeleteFileOp extends FileOp {
00757         protected function allowedParams() {
00758                 return array( array( 'src' ), array( 'ignoreMissingSource' ) );
00759         }
00760 
00765         protected function doPrecheck( array &$predicates ) {
00766                 $status = Status::newGood();
00767                 // Check if the source file exists
00768                 if ( !$this->fileExists( $this->params['src'], $predicates ) ) {
00769                         if ( $this->getParam( 'ignoreMissingSource' ) ) {
00770                                 $this->doOperation = false; // no-op
00771                                 // Update file existence predicates (cache 404s)
00772                                 $predicates['exists'][$this->params['src']] = false;
00773                                 $predicates['sha1'][$this->params['src']] = false;
00774                                 return $status; // nothing to do
00775                         } else {
00776                                 $status->fatal( 'backend-fail-notexists', $this->params['src'] );
00777                                 return $status;
00778                         }
00779                 // Check if a file can be placed/changed at the source
00780                 } elseif ( !$this->backend->isPathUsableInternal( $this->params['src'] ) ) {
00781                         $status->fatal( 'backend-fail-usable', $this->params['src'] );
00782                         $status->fatal( 'backend-fail-delete', $this->params['src'] );
00783                         return $status;
00784                 }
00785                 // Update file existence predicates
00786                 $predicates['exists'][$this->params['src']] = false;
00787                 $predicates['sha1'][$this->params['src']] = false;
00788                 return $status; // safe to call attempt()
00789         }
00790 
00794         protected function doAttempt() {
00795                 // Delete the source file
00796                 return $this->backend->deleteInternal( $this->setFlags( $this->params ) );
00797         }
00798 
00802         public function storagePathsChanged() {
00803                 return array( $this->params['src'] );
00804         }
00805 }
00806 
00811 class DescribeFileOp extends FileOp {
00815         protected function allowedParams() {
00816                 return array( array( 'src' ), array( 'disposition', 'headers' ) );
00817         }
00818 
00823         protected function doPrecheck( array &$predicates ) {
00824                 $status = Status::newGood();
00825                 // Check if the source file exists
00826                 if ( !$this->fileExists( $this->params['src'], $predicates ) ) {
00827                         $status->fatal( 'backend-fail-notexists', $this->params['src'] );
00828                         return $status;
00829                 // Check if a file can be placed/changed at the source
00830                 } elseif ( !$this->backend->isPathUsableInternal( $this->params['src'] ) ) {
00831                         $status->fatal( 'backend-fail-usable', $this->params['src'] );
00832                         $status->fatal( 'backend-fail-describe', $this->params['src'] );
00833                         return $status;
00834                 }
00835                 // Update file existence predicates
00836                 $predicates['exists'][$this->params['src']] =
00837                         $this->fileExists( $this->params['src'], $predicates );
00838                 $predicates['sha1'][$this->params['src']] =
00839                         $this->fileSha1( $this->params['src'], $predicates );
00840                 return $status; // safe to call attempt()
00841         }
00842 
00846         protected function doAttempt() {
00847                 // Update the source file's metadata
00848                 return $this->backend->describeInternal( $this->setFlags( $this->params ) );
00849         }
00850 
00854         public function storagePathsChanged() {
00855                 return array( $this->params['src'] );
00856         }
00857 }
00858 
00862 class NullFileOp extends FileOp {}