MediaWiki
REL1_22
|
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 {}