MediaWiki  REL1_24
FileOp.php
Go to the documentation of this file.
00001 <?php
00036 abstract class FileOp {
00038     protected $params = array();
00039 
00041     protected $backend;
00042 
00044     protected $state = self::STATE_NEW;
00045 
00047     protected $failed = false;
00048 
00050     protected $async = false;
00051 
00053     protected $batchId;
00054 
00056     protected $doOperation = true;
00057 
00059     protected $sourceSha1;
00060 
00062     protected $overwriteSameCase;
00063 
00065     protected $destExists;
00066 
00067     /* Object life-cycle */
00068     const STATE_NEW = 1;
00069     const STATE_CHECKED = 2;
00070     const STATE_ATTEMPTED = 3;
00071 
00079     final public function __construct( FileBackendStore $backend, array $params ) {
00080         $this->backend = $backend;
00081         list( $required, $optional, $paths ) = $this->allowedParams();
00082         foreach ( $required as $name ) {
00083             if ( isset( $params[$name] ) ) {
00084                 $this->params[$name] = $params[$name];
00085             } else {
00086                 throw new FileBackendError( "File operation missing parameter '$name'." );
00087             }
00088         }
00089         foreach ( $optional as $name ) {
00090             if ( isset( $params[$name] ) ) {
00091                 $this->params[$name] = $params[$name];
00092             }
00093         }
00094         foreach ( $paths as $name ) {
00095             if ( isset( $this->params[$name] ) ) {
00096                 // Normalize paths so the paths to the same file have the same string
00097                 $this->params[$name] = self::normalizeIfValidStoragePath( $this->params[$name] );
00098             }
00099         }
00100     }
00101 
00108     protected static function normalizeIfValidStoragePath( $path ) {
00109         if ( FileBackend::isStoragePath( $path ) ) {
00110             $res = FileBackend::normalizeStoragePath( $path );
00111 
00112             return ( $res !== null ) ? $res : $path;
00113         }
00114 
00115         return $path;
00116     }
00117 
00123     final public function setBatchId( $batchId ) {
00124         $this->batchId = $batchId;
00125     }
00126 
00133     final public function getParam( $name ) {
00134         return isset( $this->params[$name] ) ? $this->params[$name] : null;
00135     }
00136 
00142     final public function failed() {
00143         return $this->failed;
00144     }
00145 
00151     final public static function newPredicates() {
00152         return array( 'exists' => array(), 'sha1' => array() );
00153     }
00154 
00160     final public static function newDependencies() {
00161         return array( 'read' => array(), 'write' => array() );
00162     }
00163 
00170     final public function applyDependencies( array $deps ) {
00171         $deps['read'] += array_fill_keys( $this->storagePathsRead(), 1 );
00172         $deps['write'] += array_fill_keys( $this->storagePathsChanged(), 1 );
00173 
00174         return $deps;
00175     }
00176 
00183     final public function dependsOn( array $deps ) {
00184         foreach ( $this->storagePathsChanged() as $path ) {
00185             if ( isset( $deps['read'][$path] ) || isset( $deps['write'][$path] ) ) {
00186                 return true; // "output" or "anti" dependency
00187             }
00188         }
00189         foreach ( $this->storagePathsRead() as $path ) {
00190             if ( isset( $deps['write'][$path] ) ) {
00191                 return true; // "flow" dependency
00192             }
00193         }
00194 
00195         return false;
00196     }
00197 
00205     final public function getJournalEntries( array $oPredicates, array $nPredicates ) {
00206         if ( !$this->doOperation ) {
00207             return array(); // this is a no-op
00208         }
00209         $nullEntries = array();
00210         $updateEntries = array();
00211         $deleteEntries = array();
00212         $pathsUsed = array_merge( $this->storagePathsRead(), $this->storagePathsChanged() );
00213         foreach ( array_unique( $pathsUsed ) as $path ) {
00214             $nullEntries[] = array( // assertion for recovery
00215                 'op' => 'null',
00216                 'path' => $path,
00217                 'newSha1' => $this->fileSha1( $path, $oPredicates )
00218             );
00219         }
00220         foreach ( $this->storagePathsChanged() as $path ) {
00221             if ( $nPredicates['sha1'][$path] === false ) { // deleted
00222                 $deleteEntries[] = array(
00223                     'op' => 'delete',
00224                     'path' => $path,
00225                     'newSha1' => ''
00226                 );
00227             } else { // created/updated
00228                 $updateEntries[] = array(
00229                     'op' => $this->fileExists( $path, $oPredicates ) ? 'update' : 'create',
00230                     'path' => $path,
00231                     'newSha1' => $nPredicates['sha1'][$path]
00232                 );
00233             }
00234         }
00235 
00236         return array_merge( $nullEntries, $updateEntries, $deleteEntries );
00237     }
00238 
00247     final public function precheck( array &$predicates ) {
00248         if ( $this->state !== self::STATE_NEW ) {
00249             return Status::newFatal( 'fileop-fail-state', self::STATE_NEW, $this->state );
00250         }
00251         $this->state = self::STATE_CHECKED;
00252         $status = $this->doPrecheck( $predicates );
00253         if ( !$status->isOK() ) {
00254             $this->failed = true;
00255         }
00256 
00257         return $status;
00258     }
00259 
00264     protected function doPrecheck( array &$predicates ) {
00265         return Status::newGood();
00266     }
00267 
00273     final public function attempt() {
00274         if ( $this->state !== self::STATE_CHECKED ) {
00275             return Status::newFatal( 'fileop-fail-state', self::STATE_CHECKED, $this->state );
00276         } elseif ( $this->failed ) { // failed precheck
00277             return Status::newFatal( 'fileop-fail-attempt-precheck' );
00278         }
00279         $this->state = self::STATE_ATTEMPTED;
00280         if ( $this->doOperation ) {
00281             $status = $this->doAttempt();
00282             if ( !$status->isOK() ) {
00283                 $this->failed = true;
00284                 $this->logFailure( 'attempt' );
00285             }
00286         } else { // no-op
00287             $status = Status::newGood();
00288         }
00289 
00290         return $status;
00291     }
00292 
00296     protected function doAttempt() {
00297         return Status::newGood();
00298     }
00299 
00305     final public function attemptAsync() {
00306         $this->async = true;
00307         $result = $this->attempt();
00308         $this->async = false;
00309 
00310         return $result;
00311     }
00312 
00318     protected function allowedParams() {
00319         return array( array(), array(), array() );
00320     }
00321 
00328     protected function setFlags( array $params ) {
00329         return array( 'async' => $this->async ) + $params;
00330     }
00331 
00337     public function storagePathsRead() {
00338         return array();
00339     }
00340 
00346     public function storagePathsChanged() {
00347         return array();
00348     }
00349 
00358     protected function precheckDestExistence( array $predicates ) {
00359         $status = Status::newGood();
00360         // Get hash of source file/string and the destination file
00361         $this->sourceSha1 = $this->getSourceSha1Base36(); // FS file or data string
00362         if ( $this->sourceSha1 === null ) { // file in storage?
00363             $this->sourceSha1 = $this->fileSha1( $this->params['src'], $predicates );
00364         }
00365         $this->overwriteSameCase = false;
00366         $this->destExists = $this->fileExists( $this->params['dst'], $predicates );
00367         if ( $this->destExists ) {
00368             if ( $this->getParam( 'overwrite' ) ) {
00369                 return $status; // OK
00370             } elseif ( $this->getParam( 'overwriteSame' ) ) {
00371                 $dhash = $this->fileSha1( $this->params['dst'], $predicates );
00372                 // Check if hashes are valid and match each other...
00373                 if ( !strlen( $this->sourceSha1 ) || !strlen( $dhash ) ) {
00374                     $status->fatal( 'backend-fail-hashes' );
00375                 } elseif ( $this->sourceSha1 !== $dhash ) {
00376                     // Give an error if the files are not identical
00377                     $status->fatal( 'backend-fail-notsame', $this->params['dst'] );
00378                 } else {
00379                     $this->overwriteSameCase = true; // OK
00380                 }
00381 
00382                 return $status; // do nothing; either OK or bad status
00383             } else {
00384                 $status->fatal( 'backend-fail-alreadyexists', $this->params['dst'] );
00385 
00386                 return $status;
00387             }
00388         }
00389 
00390         return $status;
00391     }
00392 
00399     protected function getSourceSha1Base36() {
00400         return null; // N/A
00401     }
00402 
00410     final protected function fileExists( $source, array $predicates ) {
00411         if ( isset( $predicates['exists'][$source] ) ) {
00412             return $predicates['exists'][$source]; // previous op assures this
00413         } else {
00414             $params = array( 'src' => $source, 'latest' => true );
00415 
00416             return $this->backend->fileExists( $params );
00417         }
00418     }
00419 
00427     final protected function fileSha1( $source, array $predicates ) {
00428         if ( isset( $predicates['sha1'][$source] ) ) {
00429             return $predicates['sha1'][$source]; // previous op assures this
00430         } elseif ( isset( $predicates['exists'][$source] ) && !$predicates['exists'][$source] ) {
00431             return false; // previous op assures this
00432         } else {
00433             $params = array( 'src' => $source, 'latest' => true );
00434 
00435             return $this->backend->getFileSha1Base36( $params );
00436         }
00437     }
00438 
00444     public function getBackend() {
00445         return $this->backend;
00446     }
00447 
00453     final public function logFailure( $action ) {
00454         $params = $this->params;
00455         $params['failedAction'] = $action;
00456         try {
00457             wfDebugLog( 'FileOperation', get_class( $this ) .
00458                 " failed (batch #{$this->batchId}): " . FormatJson::encode( $params ) );
00459         } catch ( Exception $e ) {
00460             // bad config? debug log error?
00461         }
00462     }
00463 }
00464 
00469 class CreateFileOp extends FileOp {
00470     protected function allowedParams() {
00471         return array(
00472             array( 'content', 'dst' ),
00473             array( 'overwrite', 'overwriteSame', 'headers' ),
00474             array( 'dst' )
00475         );
00476     }
00477 
00478     protected function doPrecheck( array &$predicates ) {
00479         $status = Status::newGood();
00480         // Check if the source data is too big
00481         if ( strlen( $this->getParam( 'content' ) ) > $this->backend->maxFileSizeInternal() ) {
00482             $status->fatal( 'backend-fail-maxsize',
00483                 $this->params['dst'], $this->backend->maxFileSizeInternal() );
00484             $status->fatal( 'backend-fail-create', $this->params['dst'] );
00485 
00486             return $status;
00487         // Check if a file can be placed/changed at the destination
00488         } elseif ( !$this->backend->isPathUsableInternal( $this->params['dst'] ) ) {
00489             $status->fatal( 'backend-fail-usable', $this->params['dst'] );
00490             $status->fatal( 'backend-fail-create', $this->params['dst'] );
00491 
00492             return $status;
00493         }
00494         // Check if destination file exists
00495         $status->merge( $this->precheckDestExistence( $predicates ) );
00496         $this->params['dstExists'] = $this->destExists; // see FileBackendStore::setFileCache()
00497         if ( $status->isOK() ) {
00498             // Update file existence predicates
00499             $predicates['exists'][$this->params['dst']] = true;
00500             $predicates['sha1'][$this->params['dst']] = $this->sourceSha1;
00501         }
00502 
00503         return $status; // safe to call attempt()
00504     }
00505 
00506     protected function doAttempt() {
00507         if ( !$this->overwriteSameCase ) {
00508             // Create the file at the destination
00509             return $this->backend->createInternal( $this->setFlags( $this->params ) );
00510         }
00511 
00512         return Status::newGood();
00513     }
00514 
00515     protected function getSourceSha1Base36() {
00516         return wfBaseConvert( sha1( $this->params['content'] ), 16, 36, 31 );
00517     }
00518 
00519     public function storagePathsChanged() {
00520         return array( $this->params['dst'] );
00521     }
00522 }
00523 
00528 class StoreFileOp extends FileOp {
00529     protected function allowedParams() {
00530         return array(
00531             array( 'src', 'dst' ),
00532             array( 'overwrite', 'overwriteSame', 'headers' ),
00533             array( 'src', 'dst' )
00534         );
00535     }
00536 
00537     protected function doPrecheck( array &$predicates ) {
00538         $status = Status::newGood();
00539         // Check if the source file exists on the file system
00540         if ( !is_file( $this->params['src'] ) ) {
00541             $status->fatal( 'backend-fail-notexists', $this->params['src'] );
00542 
00543             return $status;
00544         // Check if the source file is too big
00545         } elseif ( filesize( $this->params['src'] ) > $this->backend->maxFileSizeInternal() ) {
00546             $status->fatal( 'backend-fail-maxsize',
00547                 $this->params['dst'], $this->backend->maxFileSizeInternal() );
00548             $status->fatal( 'backend-fail-store', $this->params['src'], $this->params['dst'] );
00549 
00550             return $status;
00551         // Check if a file can be placed/changed at the destination
00552         } elseif ( !$this->backend->isPathUsableInternal( $this->params['dst'] ) ) {
00553             $status->fatal( 'backend-fail-usable', $this->params['dst'] );
00554             $status->fatal( 'backend-fail-store', $this->params['src'], $this->params['dst'] );
00555 
00556             return $status;
00557         }
00558         // Check if destination file exists
00559         $status->merge( $this->precheckDestExistence( $predicates ) );
00560         $this->params['dstExists'] = $this->destExists; // see FileBackendStore::setFileCache()
00561         if ( $status->isOK() ) {
00562             // Update file existence predicates
00563             $predicates['exists'][$this->params['dst']] = true;
00564             $predicates['sha1'][$this->params['dst']] = $this->sourceSha1;
00565         }
00566 
00567         return $status; // safe to call attempt()
00568     }
00569 
00570     protected function doAttempt() {
00571         if ( !$this->overwriteSameCase ) {
00572             // Store the file at the destination
00573             return $this->backend->storeInternal( $this->setFlags( $this->params ) );
00574         }
00575 
00576         return Status::newGood();
00577     }
00578 
00579     protected function getSourceSha1Base36() {
00580         wfSuppressWarnings();
00581         $hash = sha1_file( $this->params['src'] );
00582         wfRestoreWarnings();
00583         if ( $hash !== false ) {
00584             $hash = wfBaseConvert( $hash, 16, 36, 31 );
00585         }
00586 
00587         return $hash;
00588     }
00589 
00590     public function storagePathsChanged() {
00591         return array( $this->params['dst'] );
00592     }
00593 }
00594 
00599 class CopyFileOp extends FileOp {
00600     protected function allowedParams() {
00601         return array(
00602             array( 'src', 'dst' ),
00603             array( 'overwrite', 'overwriteSame', 'ignoreMissingSource', 'headers' ),
00604             array( 'src', 'dst' )
00605         );
00606     }
00607 
00608     protected function doPrecheck( array &$predicates ) {
00609         $status = Status::newGood();
00610         // Check if the source file exists
00611         if ( !$this->fileExists( $this->params['src'], $predicates ) ) {
00612             if ( $this->getParam( 'ignoreMissingSource' ) ) {
00613                 $this->doOperation = false; // no-op
00614                 // Update file existence predicates (cache 404s)
00615                 $predicates['exists'][$this->params['src']] = false;
00616                 $predicates['sha1'][$this->params['src']] = false;
00617 
00618                 return $status; // nothing to do
00619             } else {
00620                 $status->fatal( 'backend-fail-notexists', $this->params['src'] );
00621 
00622                 return $status;
00623             }
00624             // Check if a file can be placed/changed at the destination
00625         } elseif ( !$this->backend->isPathUsableInternal( $this->params['dst'] ) ) {
00626             $status->fatal( 'backend-fail-usable', $this->params['dst'] );
00627             $status->fatal( 'backend-fail-copy', $this->params['src'], $this->params['dst'] );
00628 
00629             return $status;
00630         }
00631         // Check if destination file exists
00632         $status->merge( $this->precheckDestExistence( $predicates ) );
00633         $this->params['dstExists'] = $this->destExists; // see FileBackendStore::setFileCache()
00634         if ( $status->isOK() ) {
00635             // Update file existence predicates
00636             $predicates['exists'][$this->params['dst']] = true;
00637             $predicates['sha1'][$this->params['dst']] = $this->sourceSha1;
00638         }
00639 
00640         return $status; // safe to call attempt()
00641     }
00642 
00643     protected function doAttempt() {
00644         if ( $this->overwriteSameCase ) {
00645             $status = Status::newGood(); // nothing to do
00646         } elseif ( $this->params['src'] === $this->params['dst'] ) {
00647             // Just update the destination file headers
00648             $headers = $this->getParam( 'headers' ) ?: array();
00649             $status = $this->backend->describeInternal( $this->setFlags( array(
00650                 'src' => $this->params['dst'], 'headers' => $headers
00651             ) ) );
00652         } else {
00653             // Copy the file to the destination
00654             $status = $this->backend->copyInternal( $this->setFlags( $this->params ) );
00655         }
00656 
00657         return $status;
00658     }
00659 
00660     public function storagePathsRead() {
00661         return array( $this->params['src'] );
00662     }
00663 
00664     public function storagePathsChanged() {
00665         return array( $this->params['dst'] );
00666     }
00667 }
00668 
00673 class MoveFileOp extends FileOp {
00674     protected function allowedParams() {
00675         return array(
00676             array( 'src', 'dst' ),
00677             array( 'overwrite', 'overwriteSame', 'ignoreMissingSource', 'headers' ),
00678             array( 'src', 'dst' )
00679         );
00680     }
00681 
00682     protected function doPrecheck( array &$predicates ) {
00683         $status = Status::newGood();
00684         // Check if the source file exists
00685         if ( !$this->fileExists( $this->params['src'], $predicates ) ) {
00686             if ( $this->getParam( 'ignoreMissingSource' ) ) {
00687                 $this->doOperation = false; // no-op
00688                 // Update file existence predicates (cache 404s)
00689                 $predicates['exists'][$this->params['src']] = false;
00690                 $predicates['sha1'][$this->params['src']] = false;
00691 
00692                 return $status; // nothing to do
00693             } else {
00694                 $status->fatal( 'backend-fail-notexists', $this->params['src'] );
00695 
00696                 return $status;
00697             }
00698         // Check if a file can be placed/changed at the destination
00699         } elseif ( !$this->backend->isPathUsableInternal( $this->params['dst'] ) ) {
00700             $status->fatal( 'backend-fail-usable', $this->params['dst'] );
00701             $status->fatal( 'backend-fail-move', $this->params['src'], $this->params['dst'] );
00702 
00703             return $status;
00704         }
00705         // Check if destination file exists
00706         $status->merge( $this->precheckDestExistence( $predicates ) );
00707         $this->params['dstExists'] = $this->destExists; // see FileBackendStore::setFileCache()
00708         if ( $status->isOK() ) {
00709             // Update file existence predicates
00710             $predicates['exists'][$this->params['src']] = false;
00711             $predicates['sha1'][$this->params['src']] = false;
00712             $predicates['exists'][$this->params['dst']] = true;
00713             $predicates['sha1'][$this->params['dst']] = $this->sourceSha1;
00714         }
00715 
00716         return $status; // safe to call attempt()
00717     }
00718 
00719     protected function doAttempt() {
00720         if ( $this->overwriteSameCase ) {
00721             if ( $this->params['src'] === $this->params['dst'] ) {
00722                 // Do nothing to the destination (which is also the source)
00723                 $status = Status::newGood();
00724             } else {
00725                 // Just delete the source as the destination file needs no changes
00726                 $status = $this->backend->deleteInternal( $this->setFlags(
00727                     array( 'src' => $this->params['src'] )
00728                 ) );
00729             }
00730         } elseif ( $this->params['src'] === $this->params['dst'] ) {
00731             // Just update the destination file headers
00732             $headers = $this->getParam( 'headers' ) ?: array();
00733             $status = $this->backend->describeInternal( $this->setFlags(
00734                 array( 'src' => $this->params['dst'], 'headers' => $headers )
00735             ) );
00736         } else {
00737             // Move the file to the destination
00738             $status = $this->backend->moveInternal( $this->setFlags( $this->params ) );
00739         }
00740 
00741         return $status;
00742     }
00743 
00744     public function storagePathsRead() {
00745         return array( $this->params['src'] );
00746     }
00747 
00748     public function storagePathsChanged() {
00749         return array( $this->params['src'], $this->params['dst'] );
00750     }
00751 }
00752 
00757 class DeleteFileOp extends FileOp {
00758     protected function allowedParams() {
00759         return array( array( 'src' ), array( 'ignoreMissingSource' ), array( 'src' ) );
00760     }
00761 
00762     protected function doPrecheck( array &$predicates ) {
00763         $status = Status::newGood();
00764         // Check if the source file exists
00765         if ( !$this->fileExists( $this->params['src'], $predicates ) ) {
00766             if ( $this->getParam( 'ignoreMissingSource' ) ) {
00767                 $this->doOperation = false; // no-op
00768                 // Update file existence predicates (cache 404s)
00769                 $predicates['exists'][$this->params['src']] = false;
00770                 $predicates['sha1'][$this->params['src']] = false;
00771 
00772                 return $status; // nothing to do
00773             } else {
00774                 $status->fatal( 'backend-fail-notexists', $this->params['src'] );
00775 
00776                 return $status;
00777             }
00778         // Check if a file can be placed/changed at the source
00779         } elseif ( !$this->backend->isPathUsableInternal( $this->params['src'] ) ) {
00780             $status->fatal( 'backend-fail-usable', $this->params['src'] );
00781             $status->fatal( 'backend-fail-delete', $this->params['src'] );
00782 
00783             return $status;
00784         }
00785         // Update file existence predicates
00786         $predicates['exists'][$this->params['src']] = false;
00787         $predicates['sha1'][$this->params['src']] = false;
00788 
00789         return $status; // safe to call attempt()
00790     }
00791 
00792     protected function doAttempt() {
00793         // Delete the source file
00794         return $this->backend->deleteInternal( $this->setFlags( $this->params ) );
00795     }
00796 
00797     public function storagePathsChanged() {
00798         return array( $this->params['src'] );
00799     }
00800 }
00801 
00806 class DescribeFileOp extends FileOp {
00807     protected function allowedParams() {
00808         return array( array( 'src' ), array( 'headers' ), array( 'src' ) );
00809     }
00810 
00811     protected function doPrecheck( array &$predicates ) {
00812         $status = Status::newGood();
00813         // Check if the source file exists
00814         if ( !$this->fileExists( $this->params['src'], $predicates ) ) {
00815             $status->fatal( 'backend-fail-notexists', $this->params['src'] );
00816 
00817             return $status;
00818         // Check if a file can be placed/changed at the source
00819         } elseif ( !$this->backend->isPathUsableInternal( $this->params['src'] ) ) {
00820             $status->fatal( 'backend-fail-usable', $this->params['src'] );
00821             $status->fatal( 'backend-fail-describe', $this->params['src'] );
00822 
00823             return $status;
00824         }
00825         // Update file existence predicates
00826         $predicates['exists'][$this->params['src']] =
00827             $this->fileExists( $this->params['src'], $predicates );
00828         $predicates['sha1'][$this->params['src']] =
00829             $this->fileSha1( $this->params['src'], $predicates );
00830 
00831         return $status; // safe to call attempt()
00832     }
00833 
00834     protected function doAttempt() {
00835         // Update the source file's metadata
00836         return $this->backend->describeInternal( $this->setFlags( $this->params ) );
00837     }
00838 
00839     public function storagePathsChanged() {
00840         return array( $this->params['src'] );
00841     }
00842 }
00843 
00847 class NullFileOp extends FileOp {
00848 }