MediaWiki  REL1_22
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 $overwriteSameCase; // 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         // @todo normalizeAnyStoragePaths() calls are overzealous, use a parameter list
00068         foreach ( $required as $name ) {
00069             if ( isset( $params[$name] ) ) {
00070                 // Normalize paths so the paths to the same file have the same string
00071                 $this->params[$name] = self::normalizeAnyStoragePaths( $params[$name] );
00072             } else {
00073                 throw new MWException( "File operation missing parameter '$name'." );
00074             }
00075         }
00076         foreach ( $optional as $name ) {
00077             if ( isset( $params[$name] ) ) {
00078                 // Normalize paths so the paths to the same file have the same string
00079                 $this->params[$name] = self::normalizeAnyStoragePaths( $params[$name] );
00080             }
00081         }
00082         $this->params = $params;
00083     }
00084 
00091     protected function normalizeAnyStoragePaths( $item ) {
00092         if ( is_array( $item ) ) {
00093             $res = array();
00094             foreach ( $item as $k => $v ) {
00095                 $k = self::normalizeIfValidStoragePath( $k );
00096                 $v = self::normalizeIfValidStoragePath( $v );
00097                 $res[$k] = $v;
00098             }
00099             return $res;
00100         } else {
00101             return self::normalizeIfValidStoragePath( $item );
00102         }
00103     }
00104 
00111     protected static function normalizeIfValidStoragePath( $path ) {
00112         if ( FileBackend::isStoragePath( $path ) ) {
00113             $res = FileBackend::normalizeStoragePath( $path );
00114             return ( $res !== null ) ? $res : $path;
00115         }
00116         return $path;
00117     }
00118 
00125     final public function setBatchId( $batchId ) {
00126         $this->batchId = $batchId;
00127     }
00128 
00135     final public function getParam( $name ) {
00136         return isset( $this->params[$name] ) ? $this->params[$name] : null;
00137     }
00138 
00144     final public function failed() {
00145         return $this->failed;
00146     }
00147 
00153     final public static function newPredicates() {
00154         return array( 'exists' => array(), 'sha1' => array() );
00155     }
00156 
00162     final public static function newDependencies() {
00163         return array( 'read' => array(), 'write' => array() );
00164     }
00165 
00172     final public function applyDependencies( array $deps ) {
00173         $deps['read'] += array_fill_keys( $this->storagePathsRead(), 1 );
00174         $deps['write'] += array_fill_keys( $this->storagePathsChanged(), 1 );
00175         return $deps;
00176     }
00177 
00184     final public function dependsOn( array $deps ) {
00185         foreach ( $this->storagePathsChanged() as $path ) {
00186             if ( isset( $deps['read'][$path] ) || isset( $deps['write'][$path] ) ) {
00187                 return true; // "output" or "anti" dependency
00188             }
00189         }
00190         foreach ( $this->storagePathsRead() as $path ) {
00191             if ( isset( $deps['write'][$path] ) ) {
00192                 return true; // "flow" dependency
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         return array_merge( $nullEntries, $updateEntries, $deleteEntries );
00236     }
00237 
00246     final public function precheck( array &$predicates ) {
00247         if ( $this->state !== self::STATE_NEW ) {
00248             return Status::newFatal( 'fileop-fail-state', self::STATE_NEW, $this->state );
00249         }
00250         $this->state = self::STATE_CHECKED;
00251         $status = $this->doPrecheck( $predicates );
00252         if ( !$status->isOK() ) {
00253             $this->failed = true;
00254         }
00255         return $status;
00256     }
00257 
00261     protected function doPrecheck( array &$predicates ) {
00262         return Status::newGood();
00263     }
00264 
00270     final public function attempt() {
00271         if ( $this->state !== self::STATE_CHECKED ) {
00272             return Status::newFatal( 'fileop-fail-state', self::STATE_CHECKED, $this->state );
00273         } elseif ( $this->failed ) { // failed precheck
00274             return Status::newFatal( 'fileop-fail-attempt-precheck' );
00275         }
00276         $this->state = self::STATE_ATTEMPTED;
00277         if ( $this->doOperation ) {
00278             $status = $this->doAttempt();
00279             if ( !$status->isOK() ) {
00280                 $this->failed = true;
00281                 $this->logFailure( 'attempt' );
00282             }
00283         } else { // no-op
00284             $status = Status::newGood();
00285         }
00286         return $status;
00287     }
00288 
00292     protected function doAttempt() {
00293         return Status::newGood();
00294     }
00295 
00301     final public function attemptAsync() {
00302         $this->async = true;
00303         $result = $this->attempt();
00304         $this->async = false;
00305         return $result;
00306     }
00307 
00313     protected function allowedParams() {
00314         return array( array(), array() );
00315     }
00316 
00323     protected function setFlags( array $params ) {
00324         return array( 'async' => $this->async ) + $params;
00325     }
00326 
00332     public function storagePathsRead() {
00333         return array();
00334     }
00335 
00341     public function storagePathsChanged() {
00342         return array();
00343     }
00344 
00353     protected function precheckDestExistence( array $predicates ) {
00354         $status = Status::newGood();
00355         // Get hash of source file/string and the destination file
00356         $this->sourceSha1 = $this->getSourceSha1Base36(); // FS file or data string
00357         if ( $this->sourceSha1 === null ) { // file in storage?
00358             $this->sourceSha1 = $this->fileSha1( $this->params['src'], $predicates );
00359         }
00360         $this->overwriteSameCase = false;
00361         $this->destExists = $this->fileExists( $this->params['dst'], $predicates );
00362         if ( $this->destExists ) {
00363             if ( $this->getParam( 'overwrite' ) ) {
00364                 return $status; // OK
00365             } elseif ( $this->getParam( 'overwriteSame' ) ) {
00366                 $dhash = $this->fileSha1( $this->params['dst'], $predicates );
00367                 // Check if hashes are valid and match each other...
00368                 if ( !strlen( $this->sourceSha1 ) || !strlen( $dhash ) ) {
00369                     $status->fatal( 'backend-fail-hashes' );
00370                 } elseif ( $this->sourceSha1 !== $dhash ) {
00371                     // Give an error if the files are not identical
00372                     $status->fatal( 'backend-fail-notsame', $this->params['dst'] );
00373                 } else {
00374                     $this->overwriteSameCase = true; // OK
00375                 }
00376                 return $status; // do nothing; either OK or bad status
00377             } else {
00378                 $status->fatal( 'backend-fail-alreadyexists', $this->params['dst'] );
00379                 return $status;
00380             }
00381         }
00382         return $status;
00383     }
00384 
00391     protected function getSourceSha1Base36() {
00392         return null; // N/A
00393     }
00394 
00402     final protected function fileExists( $source, array $predicates ) {
00403         if ( isset( $predicates['exists'][$source] ) ) {
00404             return $predicates['exists'][$source]; // previous op assures this
00405         } else {
00406             $params = array( 'src' => $source, 'latest' => true );
00407             return $this->backend->fileExists( $params );
00408         }
00409     }
00410 
00418     final protected function fileSha1( $source, array $predicates ) {
00419         if ( isset( $predicates['sha1'][$source] ) ) {
00420             return $predicates['sha1'][$source]; // previous op assures this
00421         } elseif ( isset( $predicates['exists'][$source] ) && !$predicates['exists'][$source] ) {
00422             return false; // previous op assures this
00423         } else {
00424             $params = array( 'src' => $source, 'latest' => true );
00425             return $this->backend->getFileSha1Base36( $params );
00426         }
00427     }
00428 
00434     public function getBackend() {
00435         return $this->backend;
00436     }
00437 
00444     final public function logFailure( $action ) {
00445         $params = $this->params;
00446         $params['failedAction'] = $action;
00447         try {
00448             wfDebugLog( 'FileOperation', get_class( $this ) .
00449                 " failed (batch #{$this->batchId}): " . FormatJson::encode( $params ) );
00450         } catch ( Exception $e ) {
00451             // bad config? debug log error?
00452         }
00453     }
00454 }
00455 
00460 class CreateFileOp extends FileOp {
00461     protected function allowedParams() {
00462         return array( array( 'content', 'dst' ),
00463             array( 'overwrite', 'overwriteSame', 'headers' ) );
00464     }
00465 
00466     protected function doPrecheck( array &$predicates ) {
00467         $status = Status::newGood();
00468         // Check if the source data is too big
00469         if ( strlen( $this->getParam( 'content' ) ) > $this->backend->maxFileSizeInternal() ) {
00470             $status->fatal( 'backend-fail-maxsize',
00471                 $this->params['dst'], $this->backend->maxFileSizeInternal() );
00472             $status->fatal( 'backend-fail-create', $this->params['dst'] );
00473             return $status;
00474         // Check if a file can be placed/changed at the destination
00475         } elseif ( !$this->backend->isPathUsableInternal( $this->params['dst'] ) ) {
00476             $status->fatal( 'backend-fail-usable', $this->params['dst'] );
00477             $status->fatal( 'backend-fail-create', $this->params['dst'] );
00478             return $status;
00479         }
00480         // Check if destination file exists
00481         $status->merge( $this->precheckDestExistence( $predicates ) );
00482         $this->params['dstExists'] = $this->destExists; // see FileBackendStore::setFileCache()
00483         if ( $status->isOK() ) {
00484             // Update file existence predicates
00485             $predicates['exists'][$this->params['dst']] = true;
00486             $predicates['sha1'][$this->params['dst']] = $this->sourceSha1;
00487         }
00488         return $status; // safe to call attempt()
00489     }
00490 
00491     protected function doAttempt() {
00492         if ( !$this->overwriteSameCase ) {
00493             // Create the file at the destination
00494             return $this->backend->createInternal( $this->setFlags( $this->params ) );
00495         }
00496         return Status::newGood();
00497     }
00498 
00499     protected function getSourceSha1Base36() {
00500         return wfBaseConvert( sha1( $this->params['content'] ), 16, 36, 31 );
00501     }
00502 
00503     public function storagePathsChanged() {
00504         return array( $this->params['dst'] );
00505     }
00506 }
00507 
00512 class StoreFileOp extends FileOp {
00513     protected function allowedParams() {
00514         return array( array( 'src', 'dst' ),
00515             array( 'overwrite', 'overwriteSame', 'headers' ) );
00516     }
00517 
00518     protected function doPrecheck( array &$predicates ) {
00519         $status = Status::newGood();
00520         // Check if the source file exists on the file system
00521         if ( !is_file( $this->params['src'] ) ) {
00522             $status->fatal( 'backend-fail-notexists', $this->params['src'] );
00523             return $status;
00524         // Check if the source file is too big
00525         } elseif ( filesize( $this->params['src'] ) > $this->backend->maxFileSizeInternal() ) {
00526             $status->fatal( 'backend-fail-maxsize',
00527                 $this->params['dst'], $this->backend->maxFileSizeInternal() );
00528             $status->fatal( 'backend-fail-store', $this->params['src'], $this->params['dst'] );
00529             return $status;
00530         // Check if a file can be placed/changed at the destination
00531         } elseif ( !$this->backend->isPathUsableInternal( $this->params['dst'] ) ) {
00532             $status->fatal( 'backend-fail-usable', $this->params['dst'] );
00533             $status->fatal( 'backend-fail-store', $this->params['src'], $this->params['dst'] );
00534             return $status;
00535         }
00536         // Check if destination file exists
00537         $status->merge( $this->precheckDestExistence( $predicates ) );
00538         $this->params['dstExists'] = $this->destExists; // see FileBackendStore::setFileCache()
00539         if ( $status->isOK() ) {
00540             // Update file existence predicates
00541             $predicates['exists'][$this->params['dst']] = true;
00542             $predicates['sha1'][$this->params['dst']] = $this->sourceSha1;
00543         }
00544         return $status; // safe to call attempt()
00545     }
00546 
00547     protected function doAttempt() {
00548         if ( !$this->overwriteSameCase ) {
00549             // Store the file at the destination
00550             return $this->backend->storeInternal( $this->setFlags( $this->params ) );
00551         }
00552         return Status::newGood();
00553     }
00554 
00555     protected function getSourceSha1Base36() {
00556         wfSuppressWarnings();
00557         $hash = sha1_file( $this->params['src'] );
00558         wfRestoreWarnings();
00559         if ( $hash !== false ) {
00560             $hash = wfBaseConvert( $hash, 16, 36, 31 );
00561         }
00562         return $hash;
00563     }
00564 
00565     public function storagePathsChanged() {
00566         return array( $this->params['dst'] );
00567     }
00568 }
00569 
00574 class CopyFileOp extends FileOp {
00575     protected function allowedParams() {
00576         return array( array( 'src', 'dst' ),
00577             array( 'overwrite', 'overwriteSame', 'ignoreMissingSource', 'headers' ) );
00578     }
00579 
00580     protected function doPrecheck( array &$predicates ) {
00581         $status = Status::newGood();
00582         // Check if the source file exists
00583         if ( !$this->fileExists( $this->params['src'], $predicates ) ) {
00584             if ( $this->getParam( 'ignoreMissingSource' ) ) {
00585                 $this->doOperation = false; // no-op
00586                 // Update file existence predicates (cache 404s)
00587                 $predicates['exists'][$this->params['src']] = false;
00588                 $predicates['sha1'][$this->params['src']] = false;
00589                 return $status; // nothing to do
00590             } else {
00591                 $status->fatal( 'backend-fail-notexists', $this->params['src'] );
00592                 return $status;
00593             }
00594         // Check if a file can be placed/changed at the destination
00595         } elseif ( !$this->backend->isPathUsableInternal( $this->params['dst'] ) ) {
00596             $status->fatal( 'backend-fail-usable', $this->params['dst'] );
00597             $status->fatal( 'backend-fail-copy', $this->params['src'], $this->params['dst'] );
00598             return $status;
00599         }
00600         // Check if destination file exists
00601         $status->merge( $this->precheckDestExistence( $predicates ) );
00602         $this->params['dstExists'] = $this->destExists; // see FileBackendStore::setFileCache()
00603         if ( $status->isOK() ) {
00604             // Update file existence predicates
00605             $predicates['exists'][$this->params['dst']] = true;
00606             $predicates['sha1'][$this->params['dst']] = $this->sourceSha1;
00607         }
00608         return $status; // safe to call attempt()
00609     }
00610 
00611     protected function doAttempt() {
00612         if ( $this->overwriteSameCase ) {
00613             $status = Status::newGood(); // nothing to do
00614         } elseif ( $this->params['src'] === $this->params['dst'] ) {
00615             // Just update the destination file headers
00616             $headers = $this->getParam( 'headers' ) ?: array();
00617             $status = $this->backend->describeInternal( $this->setFlags( array(
00618                 'src' => $this->params['dst'], 'headers' => $headers
00619             ) ) );
00620         } else {
00621             // Copy the file to the destination
00622             $status = $this->backend->copyInternal( $this->setFlags( $this->params ) );
00623         }
00624         return $status;
00625     }
00626 
00627     public function storagePathsRead() {
00628         return array( $this->params['src'] );
00629     }
00630 
00631     public function storagePathsChanged() {
00632         return array( $this->params['dst'] );
00633     }
00634 }
00635 
00640 class MoveFileOp extends FileOp {
00641     protected function allowedParams() {
00642         return array( array( 'src', 'dst' ),
00643             array( 'overwrite', 'overwriteSame', 'ignoreMissingSource', 'headers' ) );
00644     }
00645 
00646     protected function doPrecheck( array &$predicates ) {
00647         $status = Status::newGood();
00648         // Check if the source file exists
00649         if ( !$this->fileExists( $this->params['src'], $predicates ) ) {
00650             if ( $this->getParam( 'ignoreMissingSource' ) ) {
00651                 $this->doOperation = false; // no-op
00652                 // Update file existence predicates (cache 404s)
00653                 $predicates['exists'][$this->params['src']] = false;
00654                 $predicates['sha1'][$this->params['src']] = false;
00655                 return $status; // nothing to do
00656             } else {
00657                 $status->fatal( 'backend-fail-notexists', $this->params['src'] );
00658                 return $status;
00659             }
00660         // Check if a file can be placed/changed at the destination
00661         } elseif ( !$this->backend->isPathUsableInternal( $this->params['dst'] ) ) {
00662             $status->fatal( 'backend-fail-usable', $this->params['dst'] );
00663             $status->fatal( 'backend-fail-move', $this->params['src'], $this->params['dst'] );
00664             return $status;
00665         }
00666         // Check if destination file exists
00667         $status->merge( $this->precheckDestExistence( $predicates ) );
00668         $this->params['dstExists'] = $this->destExists; // see FileBackendStore::setFileCache()
00669         if ( $status->isOK() ) {
00670             // Update file existence predicates
00671             $predicates['exists'][$this->params['src']] = false;
00672             $predicates['sha1'][$this->params['src']] = false;
00673             $predicates['exists'][$this->params['dst']] = true;
00674             $predicates['sha1'][$this->params['dst']] = $this->sourceSha1;
00675         }
00676         return $status; // safe to call attempt()
00677     }
00678 
00679     protected function doAttempt() {
00680         if ( $this->overwriteSameCase ) {
00681             if ( $this->params['src'] === $this->params['dst'] ) {
00682                 // Do nothing to the destination (which is also the source)
00683                 $status = Status::newGood();
00684             } else {
00685                 // Just delete the source as the destination file needs no changes
00686                 $status = $this->backend->deleteInternal( $this->setFlags(
00687                     array( 'src' => $this->params['src'] )
00688                 ) );
00689             }
00690         } elseif ( $this->params['src'] === $this->params['dst'] ) {
00691             // Just update the destination file headers
00692             $headers = $this->getParam( 'headers' ) ?: array();
00693             $status = $this->backend->describeInternal( $this->setFlags(
00694                 array( 'src' => $this->params['dst'], 'headers' => $headers )
00695             ) );
00696         } else {
00697             // Move the file to the destination
00698             $status = $this->backend->moveInternal( $this->setFlags( $this->params ) );
00699         }
00700         return $status;
00701     }
00702 
00703     public function storagePathsRead() {
00704         return array( $this->params['src'] );
00705     }
00706 
00707     public function storagePathsChanged() {
00708         return array( $this->params['src'], $this->params['dst'] );
00709     }
00710 }
00711 
00716 class DeleteFileOp extends FileOp {
00717     protected function allowedParams() {
00718         return array( array( 'src' ), array( 'ignoreMissingSource' ) );
00719     }
00720 
00721     protected function doPrecheck( array &$predicates ) {
00722         $status = Status::newGood();
00723         // Check if the source file exists
00724         if ( !$this->fileExists( $this->params['src'], $predicates ) ) {
00725             if ( $this->getParam( 'ignoreMissingSource' ) ) {
00726                 $this->doOperation = false; // no-op
00727                 // Update file existence predicates (cache 404s)
00728                 $predicates['exists'][$this->params['src']] = false;
00729                 $predicates['sha1'][$this->params['src']] = false;
00730                 return $status; // nothing to do
00731             } else {
00732                 $status->fatal( 'backend-fail-notexists', $this->params['src'] );
00733                 return $status;
00734             }
00735         // Check if a file can be placed/changed at the source
00736         } elseif ( !$this->backend->isPathUsableInternal( $this->params['src'] ) ) {
00737             $status->fatal( 'backend-fail-usable', $this->params['src'] );
00738             $status->fatal( 'backend-fail-delete', $this->params['src'] );
00739             return $status;
00740         }
00741         // Update file existence predicates
00742         $predicates['exists'][$this->params['src']] = false;
00743         $predicates['sha1'][$this->params['src']] = false;
00744         return $status; // safe to call attempt()
00745     }
00746 
00747     protected function doAttempt() {
00748         // Delete the source file
00749         return $this->backend->deleteInternal( $this->setFlags( $this->params ) );
00750     }
00751 
00752     public function storagePathsChanged() {
00753         return array( $this->params['src'] );
00754     }
00755 }
00756 
00761 class DescribeFileOp extends FileOp {
00762     protected function allowedParams() {
00763         return array( array( 'src' ), array( 'headers' ) );
00764     }
00765 
00766     protected function doPrecheck( array &$predicates ) {
00767         $status = Status::newGood();
00768         // Check if the source file exists
00769         if ( !$this->fileExists( $this->params['src'], $predicates ) ) {
00770             $status->fatal( 'backend-fail-notexists', $this->params['src'] );
00771             return $status;
00772         // Check if a file can be placed/changed at the source
00773         } elseif ( !$this->backend->isPathUsableInternal( $this->params['src'] ) ) {
00774             $status->fatal( 'backend-fail-usable', $this->params['src'] );
00775             $status->fatal( 'backend-fail-describe', $this->params['src'] );
00776             return $status;
00777         }
00778         // Update file existence predicates
00779         $predicates['exists'][$this->params['src']] =
00780             $this->fileExists( $this->params['src'], $predicates );
00781         $predicates['sha1'][$this->params['src']] =
00782             $this->fileSha1( $this->params['src'], $predicates );
00783         return $status; // safe to call attempt()
00784     }
00785 
00786     protected function doAttempt() {
00787         // Update the source file's metadata
00788         return $this->backend->describeInternal( $this->setFlags( $this->params ) );
00789     }
00790 
00791     public function storagePathsChanged() {
00792         return array( $this->params['src'] );
00793     }
00794 }
00795 
00799 class NullFileOp extends FileOp {}