MediaWiki  REL1_20
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 $sourceSha1; // string
00049         protected $destSameAsSource; // boolean
00050 
00051         /* Object life-cycle */
00052         const STATE_NEW = 1;
00053         const STATE_CHECKED = 2;
00054         const STATE_ATTEMPTED = 3;
00055 
00063         final public function __construct( FileBackendStore $backend, array $params ) {
00064                 $this->backend = $backend;
00065                 list( $required, $optional ) = $this->allowedParams();
00066                 foreach ( $required as $name ) {
00067                         if ( isset( $params[$name] ) ) {
00068                                 $this->params[$name] = $params[$name];
00069                         } else {
00070                                 throw new MWException( "File operation missing parameter '$name'." );
00071                         }
00072                 }
00073                 foreach ( $optional as $name ) {
00074                         if ( isset( $params[$name] ) ) {
00075                                 $this->params[$name] = $params[$name];
00076                         }
00077                 }
00078                 $this->params = $params;
00079         }
00080 
00087         final public function setBatchId( $batchId ) {
00088                 $this->batchId = $batchId;
00089         }
00090 
00097         final public function allowStaleReads( $allowStale ) {
00098                 $this->useLatest = !$allowStale;
00099         }
00100 
00107         final public function getParam( $name ) {
00108                 return isset( $this->params[$name] ) ? $this->params[$name] : null;
00109         }
00110 
00116         final public function failed() {
00117                 return $this->failed;
00118         }
00119 
00125         final public static function newPredicates() {
00126                 return array( 'exists' => array(), 'sha1' => array() );
00127         }
00128 
00134         final public static function newDependencies() {
00135                 return array( 'read' => array(), 'write' => array() );
00136         }
00137 
00144         final public function applyDependencies( array $deps ) {
00145                 $deps['read']  += array_fill_keys( $this->storagePathsRead(), 1 );
00146                 $deps['write'] += array_fill_keys( $this->storagePathsChanged(), 1 );
00147                 return $deps;
00148         }
00149 
00156         final public function dependsOn( array $deps ) {
00157                 foreach ( $this->storagePathsChanged() as $path ) {
00158                         if ( isset( $deps['read'][$path] ) || isset( $deps['write'][$path] ) ) {
00159                                 return true; // "output" or "anti" dependency
00160                         }
00161                 }
00162                 foreach ( $this->storagePathsRead() as $path ) {
00163                         if ( isset( $deps['write'][$path] ) ) {
00164                                 return true; // "flow" dependency
00165                         }
00166                 }
00167                 return false;
00168         }
00169 
00177         final public function getJournalEntries( array $oPredicates, array $nPredicates ) {
00178                 $nullEntries = array();
00179                 $updateEntries = array();
00180                 $deleteEntries = array();
00181                 $pathsUsed = array_merge( $this->storagePathsRead(), $this->storagePathsChanged() );
00182                 foreach ( $pathsUsed as $path ) {
00183                         $nullEntries[] = array( // assertion for recovery
00184                                 'op'      => 'null',
00185                                 'path'    => $path,
00186                                 'newSha1' => $this->fileSha1( $path, $oPredicates )
00187                         );
00188                 }
00189                 foreach ( $this->storagePathsChanged() as $path ) {
00190                         if ( $nPredicates['sha1'][$path] === false ) { // deleted
00191                                 $deleteEntries[] = array(
00192                                         'op'      => 'delete',
00193                                         'path'    => $path,
00194                                         'newSha1' => ''
00195                                 );
00196                         } else { // created/updated
00197                                 $updateEntries[] = array(
00198                                         'op'      => $this->fileExists( $path, $oPredicates ) ? 'update' : 'create',
00199                                         'path'    => $path,
00200                                         'newSha1' => $nPredicates['sha1'][$path]
00201                                 );
00202                         }
00203                 }
00204                 return array_merge( $nullEntries, $updateEntries, $deleteEntries );
00205         }
00206 
00213         final public function precheck( array &$predicates ) {
00214                 if ( $this->state !== self::STATE_NEW ) {
00215                         return Status::newFatal( 'fileop-fail-state', self::STATE_NEW, $this->state );
00216                 }
00217                 $this->state = self::STATE_CHECKED;
00218                 $status = $this->doPrecheck( $predicates );
00219                 if ( !$status->isOK() ) {
00220                         $this->failed = true;
00221                 }
00222                 return $status;
00223         }
00224 
00228         protected function doPrecheck( array &$predicates ) {
00229                 return Status::newGood();
00230         }
00231 
00237         final public function attempt() {
00238                 if ( $this->state !== self::STATE_CHECKED ) {
00239                         return Status::newFatal( 'fileop-fail-state', self::STATE_CHECKED, $this->state );
00240                 } elseif ( $this->failed ) { // failed precheck
00241                         return Status::newFatal( 'fileop-fail-attempt-precheck' );
00242                 }
00243                 $this->state = self::STATE_ATTEMPTED;
00244                 $status = $this->doAttempt();
00245                 if ( !$status->isOK() ) {
00246                         $this->failed = true;
00247                         $this->logFailure( 'attempt' );
00248                 }
00249                 return $status;
00250         }
00251 
00255         protected function doAttempt() {
00256                 return Status::newGood();
00257         }
00258 
00264         final public function attemptAsync() {
00265                 $this->async = true;
00266                 $result = $this->attempt();
00267                 $this->async = false;
00268                 return $result;
00269         }
00270 
00276         protected function allowedParams() {
00277                 return array( array(), array() );
00278         }
00279 
00286         protected function setFlags( array $params ) {
00287                 return array( 'async' => $this->async ) + $params;
00288         }
00289 
00295         final public function storagePathsRead() {
00296                 return array_map( 'FileBackend::normalizeStoragePath', $this->doStoragePathsRead() );
00297         }
00298 
00303         protected function doStoragePathsRead() {
00304                 return array();
00305         }
00306 
00312         final public function storagePathsChanged() {
00313                 return array_map( 'FileBackend::normalizeStoragePath', $this->doStoragePathsChanged() );
00314         }
00315 
00320         protected function doStoragePathsChanged() {
00321                 return array();
00322         }
00323 
00332         protected function precheckDestExistence( array $predicates ) {
00333                 $status = Status::newGood();
00334                 // Get hash of source file/string and the destination file
00335                 $this->sourceSha1 = $this->getSourceSha1Base36(); // FS file or data string
00336                 if ( $this->sourceSha1 === null ) { // file in storage?
00337                         $this->sourceSha1 = $this->fileSha1( $this->params['src'], $predicates );
00338                 }
00339                 $this->destSameAsSource = false;
00340                 if ( $this->fileExists( $this->params['dst'], $predicates ) ) {
00341                         if ( $this->getParam( 'overwrite' ) ) {
00342                                 return $status; // OK
00343                         } elseif ( $this->getParam( 'overwriteSame' ) ) {
00344                                 $dhash = $this->fileSha1( $this->params['dst'], $predicates );
00345                                 // Check if hashes are valid and match each other...
00346                                 if ( !strlen( $this->sourceSha1 ) || !strlen( $dhash ) ) {
00347                                         $status->fatal( 'backend-fail-hashes' );
00348                                 } elseif ( $this->sourceSha1 !== $dhash ) {
00349                                         // Give an error if the files are not identical
00350                                         $status->fatal( 'backend-fail-notsame', $this->params['dst'] );
00351                                 } else {
00352                                         $this->destSameAsSource = true; // OK
00353                                 }
00354                                 return $status; // do nothing; either OK or bad status
00355                         } else {
00356                                 $status->fatal( 'backend-fail-alreadyexists', $this->params['dst'] );
00357                                 return $status;
00358                         }
00359                 }
00360                 return $status;
00361         }
00362 
00369         protected function getSourceSha1Base36() {
00370                 return null; // N/A
00371         }
00372 
00380         final protected function fileExists( $source, array $predicates ) {
00381                 if ( isset( $predicates['exists'][$source] ) ) {
00382                         return $predicates['exists'][$source]; // previous op assures this
00383                 } else {
00384                         $params = array( 'src' => $source, 'latest' => $this->useLatest );
00385                         return $this->backend->fileExists( $params );
00386                 }
00387         }
00388 
00396         final protected function fileSha1( $source, array $predicates ) {
00397                 if ( isset( $predicates['sha1'][$source] ) ) {
00398                         return $predicates['sha1'][$source]; // previous op assures this
00399                 } else {
00400                         $params = array( 'src' => $source, 'latest' => $this->useLatest );
00401                         return $this->backend->getFileSha1Base36( $params );
00402                 }
00403         }
00404 
00410         public function getBackend() {
00411                 return $this->backend;
00412         }
00413 
00420         final public function logFailure( $action ) {
00421                 $params = $this->params;
00422                 $params['failedAction'] = $action;
00423                 try {
00424                         wfDebugLog( 'FileOperation', get_class( $this ) .
00425                                 " failed (batch #{$this->batchId}): " . FormatJson::encode( $params ) );
00426                 } catch ( Exception $e ) {
00427                         // bad config? debug log error?
00428                 }
00429         }
00430 }
00431 
00436 class StoreFileOp extends FileOp {
00440         protected function allowedParams() {
00441                 return array( array( 'src', 'dst' ),
00442                         array( 'overwrite', 'overwriteSame', 'disposition' ) );
00443         }
00444 
00449         protected function doPrecheck( array &$predicates ) {
00450                 $status = Status::newGood();
00451                 // Check if the source file exists on the file system
00452                 if ( !is_file( $this->params['src'] ) ) {
00453                         $status->fatal( 'backend-fail-notexists', $this->params['src'] );
00454                         return $status;
00455                 // Check if the source file is too big
00456                 } elseif ( filesize( $this->params['src'] ) > $this->backend->maxFileSizeInternal() ) {
00457                         $status->fatal( 'backend-fail-maxsize',
00458                                 $this->params['dst'], $this->backend->maxFileSizeInternal() );
00459                         $status->fatal( 'backend-fail-store', $this->params['src'], $this->params['dst'] );
00460                         return $status;
00461                 // Check if a file can be placed at the destination
00462                 } elseif ( !$this->backend->isPathUsableInternal( $this->params['dst'] ) ) {
00463                         $status->fatal( 'backend-fail-usable', $this->params['dst'] );
00464                         $status->fatal( 'backend-fail-store', $this->params['src'], $this->params['dst'] );
00465                         return $status;
00466                 }
00467                 // Check if destination file exists
00468                 $status->merge( $this->precheckDestExistence( $predicates ) );
00469                 if ( $status->isOK() ) {
00470                         // Update file existence predicates
00471                         $predicates['exists'][$this->params['dst']] = true;
00472                         $predicates['sha1'][$this->params['dst']] = $this->sourceSha1;
00473                 }
00474                 return $status; // safe to call attempt()
00475         }
00476 
00480         protected function doAttempt() {
00481                 // Store the file at the destination
00482                 if ( !$this->destSameAsSource ) {
00483                         return $this->backend->storeInternal( $this->setFlags( $this->params ) );
00484                 }
00485                 return Status::newGood();
00486         }
00487 
00491         protected function getSourceSha1Base36() {
00492                 wfSuppressWarnings();
00493                 $hash = sha1_file( $this->params['src'] );
00494                 wfRestoreWarnings();
00495                 if ( $hash !== false ) {
00496                         $hash = wfBaseConvert( $hash, 16, 36, 31 );
00497                 }
00498                 return $hash;
00499         }
00500 
00501         protected function doStoragePathsChanged() {
00502                 return array( $this->params['dst'] );
00503         }
00504 }
00505 
00510 class CreateFileOp extends FileOp {
00511         protected function allowedParams() {
00512                 return array( array( 'content', 'dst' ),
00513                         array( 'overwrite', 'overwriteSame', 'disposition' ) );
00514         }
00515 
00516         protected function doPrecheck( array &$predicates ) {
00517                 $status = Status::newGood();
00518                 // Check if the source data is too big
00519                 if ( strlen( $this->getParam( 'content' ) ) > $this->backend->maxFileSizeInternal() ) {
00520                         $status->fatal( 'backend-fail-maxsize',
00521                                 $this->params['dst'], $this->backend->maxFileSizeInternal() );
00522                         $status->fatal( 'backend-fail-create', $this->params['dst'] );
00523                         return $status;
00524                 // Check if a file can be placed at the destination
00525                 } elseif ( !$this->backend->isPathUsableInternal( $this->params['dst'] ) ) {
00526                         $status->fatal( 'backend-fail-usable', $this->params['dst'] );
00527                         $status->fatal( 'backend-fail-create', $this->params['dst'] );
00528                         return $status;
00529                 }
00530                 // Check if destination file exists
00531                 $status->merge( $this->precheckDestExistence( $predicates ) );
00532                 if ( $status->isOK() ) {
00533                         // Update file existence predicates
00534                         $predicates['exists'][$this->params['dst']] = true;
00535                         $predicates['sha1'][$this->params['dst']] = $this->sourceSha1;
00536                 }
00537                 return $status; // safe to call attempt()
00538         }
00539 
00543         protected function doAttempt() {
00544                 if ( !$this->destSameAsSource ) {
00545                         // Create the file at the destination
00546                         return $this->backend->createInternal( $this->setFlags( $this->params ) );
00547                 }
00548                 return Status::newGood();
00549         }
00550 
00554         protected function getSourceSha1Base36() {
00555                 return wfBaseConvert( sha1( $this->params['content'] ), 16, 36, 31 );
00556         }
00557 
00561         protected function doStoragePathsChanged() {
00562                 return array( $this->params['dst'] );
00563         }
00564 }
00565 
00570 class CopyFileOp extends FileOp {
00574         protected function allowedParams() {
00575                 return array( array( 'src', 'dst' ),
00576                         array( 'overwrite', 'overwriteSame', 'disposition' ) );
00577         }
00578 
00583         protected function doPrecheck( array &$predicates ) {
00584                 $status = Status::newGood();
00585                 // Check if the source file exists
00586                 if ( !$this->fileExists( $this->params['src'], $predicates ) ) {
00587                         $status->fatal( 'backend-fail-notexists', $this->params['src'] );
00588                         return $status;
00589                 // Check if a file can be placed at the destination
00590                 } elseif ( !$this->backend->isPathUsableInternal( $this->params['dst'] ) ) {
00591                         $status->fatal( 'backend-fail-usable', $this->params['dst'] );
00592                         $status->fatal( 'backend-fail-copy', $this->params['src'], $this->params['dst'] );
00593                         return $status;
00594                 }
00595                 // Check if destination file exists
00596                 $status->merge( $this->precheckDestExistence( $predicates ) );
00597                 if ( $status->isOK() ) {
00598                         // Update file existence predicates
00599                         $predicates['exists'][$this->params['dst']] = true;
00600                         $predicates['sha1'][$this->params['dst']] = $this->sourceSha1;
00601                 }
00602                 return $status; // safe to call attempt()
00603         }
00604 
00608         protected function doAttempt() {
00609                 // Do nothing if the src/dst paths are the same
00610                 if ( $this->params['src'] !== $this->params['dst'] ) {
00611                         // Copy the file into the destination
00612                         if ( !$this->destSameAsSource ) {
00613                                 return $this->backend->copyInternal( $this->setFlags( $this->params ) );
00614                         }
00615                 }
00616                 return Status::newGood();
00617         }
00618 
00622         protected function doStoragePathsRead() {
00623                 return array( $this->params['src'] );
00624         }
00625 
00629         protected function doStoragePathsChanged() {
00630                 return array( $this->params['dst'] );
00631         }
00632 }
00633 
00638 class MoveFileOp extends FileOp {
00642         protected function allowedParams() {
00643                 return array( array( 'src', 'dst' ),
00644                         array( 'overwrite', 'overwriteSame', 'disposition' ) );
00645         }
00646 
00651         protected function doPrecheck( array &$predicates ) {
00652                 $status = Status::newGood();
00653                 // Check if the source file exists
00654                 if ( !$this->fileExists( $this->params['src'], $predicates ) ) {
00655                         $status->fatal( 'backend-fail-notexists', $this->params['src'] );
00656                         return $status;
00657                 // Check if a file can be placed at the destination
00658                 } elseif ( !$this->backend->isPathUsableInternal( $this->params['dst'] ) ) {
00659                         $status->fatal( 'backend-fail-usable', $this->params['dst'] );
00660                         $status->fatal( 'backend-fail-move', $this->params['src'], $this->params['dst'] );
00661                         return $status;
00662                 }
00663                 // Check if destination file exists
00664                 $status->merge( $this->precheckDestExistence( $predicates ) );
00665                 if ( $status->isOK() ) {
00666                         // Update file existence predicates
00667                         $predicates['exists'][$this->params['src']] = false;
00668                         $predicates['sha1'][$this->params['src']] = false;
00669                         $predicates['exists'][$this->params['dst']] = true;
00670                         $predicates['sha1'][$this->params['dst']] = $this->sourceSha1;
00671                 }
00672                 return $status; // safe to call attempt()
00673         }
00674 
00678         protected function doAttempt() {
00679                 // Do nothing if the src/dst paths are the same
00680                 if ( $this->params['src'] !== $this->params['dst'] ) {
00681                         if ( !$this->destSameAsSource ) {
00682                                 // Move the file into the destination
00683                                 return $this->backend->moveInternal( $this->setFlags( $this->params ) );
00684                         } else {
00685                                 // Just delete source as the destination needs no changes
00686                                 $params = array( 'src' => $this->params['src'] );
00687                                 return $this->backend->deleteInternal( $this->setFlags( $params ) );
00688                         }
00689                 }
00690                 return Status::newGood();
00691         }
00692 
00696         protected function doStoragePathsRead() {
00697                 return array( $this->params['src'] );
00698         }
00699 
00703         protected function doStoragePathsChanged() {
00704                 return array( $this->params['src'], $this->params['dst'] );
00705         }
00706 }
00707 
00712 class DeleteFileOp extends FileOp {
00716         protected function allowedParams() {
00717                 return array( array( 'src' ), array( 'ignoreMissingSource' ) );
00718         }
00719 
00720         protected $needsDelete = true;
00721 
00726         protected function doPrecheck( array &$predicates ) {
00727                 $status = Status::newGood();
00728                 // Check if the source file exists
00729                 if ( !$this->fileExists( $this->params['src'], $predicates ) ) {
00730                         if ( !$this->getParam( 'ignoreMissingSource' ) ) {
00731                                 $status->fatal( 'backend-fail-notexists', $this->params['src'] );
00732                                 return $status;
00733                         }
00734                         $this->needsDelete = false;
00735                 }
00736                 // Update file existence predicates
00737                 $predicates['exists'][$this->params['src']] = false;
00738                 $predicates['sha1'][$this->params['src']] = false;
00739                 return $status; // safe to call attempt()
00740         }
00741 
00745         protected function doAttempt() {
00746                 if ( $this->needsDelete ) {
00747                         // Delete the source file
00748                         return $this->backend->deleteInternal( $this->setFlags( $this->params ) );
00749                 }
00750                 return Status::newGood();
00751         }
00752 
00756         protected function doStoragePathsChanged() {
00757                 return array( $this->params['src'] );
00758         }
00759 }
00760 
00764 class NullFileOp extends FileOp {}