MediaWiki
REL1_24
|
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 }