MediaWiki
master
|
00001 <?php 00036 abstract class FileOp { 00038 protected $params = array(); 00040 protected $backend; 00041 00042 protected $state = self::STATE_NEW; // integer 00043 protected $failed = false; // boolean 00044 protected $async = false; // boolean 00045 protected $useLatest = true; // boolean 00046 protected $batchId; // string 00047 00048 protected $doOperation = true; // boolean; operation is not a no-op 00049 protected $sourceSha1; // string 00050 protected $destSameAsSource; // 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 foreach ( $required as $name ) { 00068 if ( isset( $params[$name] ) ) { 00069 $this->params[$name] = self::normalizeAnyStoragePaths( $params[$name] ); 00070 } else { 00071 throw new MWException( "File operation missing parameter '$name'." ); 00072 } 00073 } 00074 foreach ( $optional as $name ) { 00075 if ( isset( $params[$name] ) ) { 00076 $this->params[$name] = self::normalizeAnyStoragePaths( $params[$name] ); 00077 } 00078 } 00079 $this->params = $params; 00080 } 00081 00088 protected function normalizeAnyStoragePaths( $item ) { 00089 if ( is_array( $item ) ) { 00090 $res = array(); 00091 foreach ( $item as $k => $v ) { 00092 $k = self::normalizeIfValidStoragePath( $k ); 00093 $v = self::normalizeIfValidStoragePath( $v ); 00094 $res[$k] = $v; 00095 } 00096 return $res; 00097 } else { 00098 return self::normalizeIfValidStoragePath( $item ); 00099 } 00100 } 00101 00108 protected static function normalizeIfValidStoragePath( $path ) { 00109 if ( FileBackend::isStoragePath( $path ) ) { 00110 $res = FileBackend::normalizeStoragePath( $path ); 00111 return ( $res !== null ) ? $res : $path; 00112 } 00113 return $path; 00114 } 00115 00122 final public function setBatchId( $batchId ) { 00123 $this->batchId = $batchId; 00124 } 00125 00132 final public function allowStaleReads( $allowStale ) { 00133 $this->useLatest = !$allowStale; 00134 } 00135 00142 final public function getParam( $name ) { 00143 return isset( $this->params[$name] ) ? $this->params[$name] : null; 00144 } 00145 00151 final public function failed() { 00152 return $this->failed; 00153 } 00154 00160 final public static function newPredicates() { 00161 return array( 'exists' => array(), 'sha1' => array() ); 00162 } 00163 00169 final public static function newDependencies() { 00170 return array( 'read' => array(), 'write' => array() ); 00171 } 00172 00179 final public function applyDependencies( array $deps ) { 00180 $deps['read'] += array_fill_keys( $this->storagePathsRead(), 1 ); 00181 $deps['write'] += array_fill_keys( $this->storagePathsChanged(), 1 ); 00182 return $deps; 00183 } 00184 00191 final public function dependsOn( array $deps ) { 00192 foreach ( $this->storagePathsChanged() as $path ) { 00193 if ( isset( $deps['read'][$path] ) || isset( $deps['write'][$path] ) ) { 00194 return true; // "output" or "anti" dependency 00195 } 00196 } 00197 foreach ( $this->storagePathsRead() as $path ) { 00198 if ( isset( $deps['write'][$path] ) ) { 00199 return true; // "flow" dependency 00200 } 00201 } 00202 return false; 00203 } 00204 00212 final public function getJournalEntries( array $oPredicates, array $nPredicates ) { 00213 if ( !$this->doOperation ) { 00214 return array(); // this is a no-op 00215 } 00216 $nullEntries = array(); 00217 $updateEntries = array(); 00218 $deleteEntries = array(); 00219 $pathsUsed = array_merge( $this->storagePathsRead(), $this->storagePathsChanged() ); 00220 foreach ( array_unique( $pathsUsed ) as $path ) { 00221 $nullEntries[] = array( // assertion for recovery 00222 'op' => 'null', 00223 'path' => $path, 00224 'newSha1' => $this->fileSha1( $path, $oPredicates ) 00225 ); 00226 } 00227 foreach ( $this->storagePathsChanged() as $path ) { 00228 if ( $nPredicates['sha1'][$path] === false ) { // deleted 00229 $deleteEntries[] = array( 00230 'op' => 'delete', 00231 'path' => $path, 00232 'newSha1' => '' 00233 ); 00234 } else { // created/updated 00235 $updateEntries[] = array( 00236 'op' => $this->fileExists( $path, $oPredicates ) ? 'update' : 'create', 00237 'path' => $path, 00238 'newSha1' => $nPredicates['sha1'][$path] 00239 ); 00240 } 00241 } 00242 return array_merge( $nullEntries, $updateEntries, $deleteEntries ); 00243 } 00244 00253 final public function precheck( array &$predicates ) { 00254 if ( $this->state !== self::STATE_NEW ) { 00255 return Status::newFatal( 'fileop-fail-state', self::STATE_NEW, $this->state ); 00256 } 00257 $this->state = self::STATE_CHECKED; 00258 $status = $this->doPrecheck( $predicates ); 00259 if ( !$status->isOK() ) { 00260 $this->failed = true; 00261 } 00262 return $status; 00263 } 00264 00268 protected function doPrecheck( array &$predicates ) { 00269 return Status::newGood(); 00270 } 00271 00277 final public function attempt() { 00278 if ( $this->state !== self::STATE_CHECKED ) { 00279 return Status::newFatal( 'fileop-fail-state', self::STATE_CHECKED, $this->state ); 00280 } elseif ( $this->failed ) { // failed precheck 00281 return Status::newFatal( 'fileop-fail-attempt-precheck' ); 00282 } 00283 $this->state = self::STATE_ATTEMPTED; 00284 if ( $this->doOperation ) { 00285 $status = $this->doAttempt(); 00286 if ( !$status->isOK() ) { 00287 $this->failed = true; 00288 $this->logFailure( 'attempt' ); 00289 } 00290 } else { // no-op 00291 $status = Status::newGood(); 00292 } 00293 return $status; 00294 } 00295 00299 protected function doAttempt() { 00300 return Status::newGood(); 00301 } 00302 00308 final public function attemptAsync() { 00309 $this->async = true; 00310 $result = $this->attempt(); 00311 $this->async = false; 00312 return $result; 00313 } 00314 00320 protected function allowedParams() { 00321 return array( array(), array() ); 00322 } 00323 00330 protected function setFlags( array $params ) { 00331 return array( 'async' => $this->async ) + $params; 00332 } 00333 00339 public function storagePathsRead() { 00340 return array(); 00341 } 00342 00348 public function storagePathsChanged() { 00349 return array(); 00350 } 00351 00360 protected function precheckDestExistence( array $predicates ) { 00361 $status = Status::newGood(); 00362 // Get hash of source file/string and the destination file 00363 $this->sourceSha1 = $this->getSourceSha1Base36(); // FS file or data string 00364 if ( $this->sourceSha1 === null ) { // file in storage? 00365 $this->sourceSha1 = $this->fileSha1( $this->params['src'], $predicates ); 00366 } 00367 $this->destSameAsSource = false; 00368 if ( $this->fileExists( $this->params['dst'], $predicates ) ) { 00369 if ( $this->getParam( 'overwrite' ) ) { 00370 return $status; // OK 00371 } elseif ( $this->getParam( 'overwriteSame' ) ) { 00372 $dhash = $this->fileSha1( $this->params['dst'], $predicates ); 00373 // Check if hashes are valid and match each other... 00374 if ( !strlen( $this->sourceSha1 ) || !strlen( $dhash ) ) { 00375 $status->fatal( 'backend-fail-hashes' ); 00376 } elseif ( $this->sourceSha1 !== $dhash ) { 00377 // Give an error if the files are not identical 00378 $status->fatal( 'backend-fail-notsame', $this->params['dst'] ); 00379 } else { 00380 $this->destSameAsSource = true; // OK 00381 } 00382 return $status; // do nothing; either OK or bad status 00383 } else { 00384 $status->fatal( 'backend-fail-alreadyexists', $this->params['dst'] ); 00385 return $status; 00386 } 00387 } 00388 return $status; 00389 } 00390 00397 protected function getSourceSha1Base36() { 00398 return null; // N/A 00399 } 00400 00408 final protected function fileExists( $source, array $predicates ) { 00409 if ( isset( $predicates['exists'][$source] ) ) { 00410 return $predicates['exists'][$source]; // previous op assures this 00411 } else { 00412 $params = array( 'src' => $source, 'latest' => $this->useLatest ); 00413 return $this->backend->fileExists( $params ); 00414 } 00415 } 00416 00424 final protected function fileSha1( $source, array $predicates ) { 00425 if ( isset( $predicates['sha1'][$source] ) ) { 00426 return $predicates['sha1'][$source]; // previous op assures this 00427 } elseif ( isset( $predicates['exists'][$source] ) && !$predicates['exists'][$source] ) { 00428 return false; // previous op assures this 00429 } else { 00430 $params = array( 'src' => $source, 'latest' => $this->useLatest ); 00431 return $this->backend->getFileSha1Base36( $params ); 00432 } 00433 } 00434 00440 public function getBackend() { 00441 return $this->backend; 00442 } 00443 00450 final public function logFailure( $action ) { 00451 $params = $this->params; 00452 $params['failedAction'] = $action; 00453 try { 00454 wfDebugLog( 'FileOperation', get_class( $this ) . 00455 " failed (batch #{$this->batchId}): " . FormatJson::encode( $params ) ); 00456 } catch ( Exception $e ) { 00457 // bad config? debug log error? 00458 } 00459 } 00460 } 00461 00466 class StoreFileOp extends FileOp { 00470 protected function allowedParams() { 00471 return array( array( 'src', 'dst' ), 00472 array( 'overwrite', 'overwriteSame', 'disposition' ) ); 00473 } 00474 00479 protected function doPrecheck( array &$predicates ) { 00480 $status = Status::newGood(); 00481 // Check if the source file exists on the file system 00482 if ( !is_file( $this->params['src'] ) ) { 00483 $status->fatal( 'backend-fail-notexists', $this->params['src'] ); 00484 return $status; 00485 // Check if the source file is too big 00486 } elseif ( filesize( $this->params['src'] ) > $this->backend->maxFileSizeInternal() ) { 00487 $status->fatal( 'backend-fail-maxsize', 00488 $this->params['dst'], $this->backend->maxFileSizeInternal() ); 00489 $status->fatal( 'backend-fail-store', $this->params['src'], $this->params['dst'] ); 00490 return $status; 00491 // Check if a file can be placed/changed at the destination 00492 } elseif ( !$this->backend->isPathUsableInternal( $this->params['dst'] ) ) { 00493 $status->fatal( 'backend-fail-usable', $this->params['dst'] ); 00494 $status->fatal( 'backend-fail-store', $this->params['src'], $this->params['dst'] ); 00495 return $status; 00496 } 00497 // Check if destination file exists 00498 $status->merge( $this->precheckDestExistence( $predicates ) ); 00499 if ( $status->isOK() ) { 00500 // Update file existence predicates 00501 $predicates['exists'][$this->params['dst']] = true; 00502 $predicates['sha1'][$this->params['dst']] = $this->sourceSha1; 00503 } 00504 return $status; // safe to call attempt() 00505 } 00506 00510 protected function doAttempt() { 00511 // Store the file at the destination 00512 if ( !$this->destSameAsSource ) { 00513 return $this->backend->storeInternal( $this->setFlags( $this->params ) ); 00514 } 00515 return Status::newGood(); 00516 } 00517 00521 protected function getSourceSha1Base36() { 00522 wfSuppressWarnings(); 00523 $hash = sha1_file( $this->params['src'] ); 00524 wfRestoreWarnings(); 00525 if ( $hash !== false ) { 00526 $hash = wfBaseConvert( $hash, 16, 36, 31 ); 00527 } 00528 return $hash; 00529 } 00530 00531 public function storagePathsChanged() { 00532 return array( $this->params['dst'] ); 00533 } 00534 } 00535 00540 class CreateFileOp extends FileOp { 00541 protected function allowedParams() { 00542 return array( array( 'content', 'dst' ), 00543 array( 'overwrite', 'overwriteSame', 'disposition' ) ); 00544 } 00545 00546 protected function doPrecheck( array &$predicates ) { 00547 $status = Status::newGood(); 00548 // Check if the source data is too big 00549 if ( strlen( $this->getParam( 'content' ) ) > $this->backend->maxFileSizeInternal() ) { 00550 $status->fatal( 'backend-fail-maxsize', 00551 $this->params['dst'], $this->backend->maxFileSizeInternal() ); 00552 $status->fatal( 'backend-fail-create', $this->params['dst'] ); 00553 return $status; 00554 // Check if a file can be placed/changed at the destination 00555 } elseif ( !$this->backend->isPathUsableInternal( $this->params['dst'] ) ) { 00556 $status->fatal( 'backend-fail-usable', $this->params['dst'] ); 00557 $status->fatal( 'backend-fail-create', $this->params['dst'] ); 00558 return $status; 00559 } 00560 // Check if destination file exists 00561 $status->merge( $this->precheckDestExistence( $predicates ) ); 00562 if ( $status->isOK() ) { 00563 // Update file existence predicates 00564 $predicates['exists'][$this->params['dst']] = true; 00565 $predicates['sha1'][$this->params['dst']] = $this->sourceSha1; 00566 } 00567 return $status; // safe to call attempt() 00568 } 00569 00573 protected function doAttempt() { 00574 if ( !$this->destSameAsSource ) { 00575 // Create the file at the destination 00576 return $this->backend->createInternal( $this->setFlags( $this->params ) ); 00577 } 00578 return Status::newGood(); 00579 } 00580 00584 protected function getSourceSha1Base36() { 00585 return wfBaseConvert( sha1( $this->params['content'] ), 16, 36, 31 ); 00586 } 00587 00591 public function storagePathsChanged() { 00592 return array( $this->params['dst'] ); 00593 } 00594 } 00595 00600 class CopyFileOp extends FileOp { 00604 protected function allowedParams() { 00605 return array( array( 'src', 'dst' ), 00606 array( 'overwrite', 'overwriteSame', 'ignoreMissingSource', 'disposition' ) ); 00607 } 00608 00613 protected function doPrecheck( array &$predicates ) { 00614 $status = Status::newGood(); 00615 // Check if the source file exists 00616 if ( !$this->fileExists( $this->params['src'], $predicates ) ) { 00617 if ( $this->getParam( 'ignoreMissingSource' ) ) { 00618 $this->doOperation = false; // no-op 00619 // Update file existence predicates (cache 404s) 00620 $predicates['exists'][$this->params['src']] = false; 00621 $predicates['sha1'][$this->params['src']] = false; 00622 return $status; // nothing to do 00623 } else { 00624 $status->fatal( 'backend-fail-notexists', $this->params['src'] ); 00625 return $status; 00626 } 00627 // Check if a file can be placed/changed at the destination 00628 } elseif ( !$this->backend->isPathUsableInternal( $this->params['dst'] ) ) { 00629 $status->fatal( 'backend-fail-usable', $this->params['dst'] ); 00630 $status->fatal( 'backend-fail-copy', $this->params['src'], $this->params['dst'] ); 00631 return $status; 00632 } 00633 // Check if destination file exists 00634 $status->merge( $this->precheckDestExistence( $predicates ) ); 00635 if ( $status->isOK() ) { 00636 // Update file existence predicates 00637 $predicates['exists'][$this->params['dst']] = true; 00638 $predicates['sha1'][$this->params['dst']] = $this->sourceSha1; 00639 } 00640 return $status; // safe to call attempt() 00641 } 00642 00646 protected function doAttempt() { 00647 // Do nothing if the src/dst paths are the same 00648 if ( $this->params['src'] !== $this->params['dst'] ) { 00649 // Copy the file into the destination 00650 if ( !$this->destSameAsSource ) { 00651 return $this->backend->copyInternal( $this->setFlags( $this->params ) ); 00652 } 00653 } 00654 return Status::newGood(); 00655 } 00656 00660 public function storagePathsRead() { 00661 return array( $this->params['src'] ); 00662 } 00663 00667 public function storagePathsChanged() { 00668 return array( $this->params['dst'] ); 00669 } 00670 } 00671 00676 class MoveFileOp extends FileOp { 00680 protected function allowedParams() { 00681 return array( array( 'src', 'dst' ), 00682 array( 'overwrite', 'overwriteSame', 'ignoreMissingSource', 'disposition' ) ); 00683 } 00684 00689 protected function doPrecheck( array &$predicates ) { 00690 $status = Status::newGood(); 00691 // Check if the source file exists 00692 if ( !$this->fileExists( $this->params['src'], $predicates ) ) { 00693 if ( $this->getParam( 'ignoreMissingSource' ) ) { 00694 $this->doOperation = false; // no-op 00695 // Update file existence predicates (cache 404s) 00696 $predicates['exists'][$this->params['src']] = false; 00697 $predicates['sha1'][$this->params['src']] = false; 00698 return $status; // nothing to do 00699 } else { 00700 $status->fatal( 'backend-fail-notexists', $this->params['src'] ); 00701 return $status; 00702 } 00703 // Check if a file can be placed/changed at the destination 00704 } elseif ( !$this->backend->isPathUsableInternal( $this->params['dst'] ) ) { 00705 $status->fatal( 'backend-fail-usable', $this->params['dst'] ); 00706 $status->fatal( 'backend-fail-move', $this->params['src'], $this->params['dst'] ); 00707 return $status; 00708 } 00709 // Check if destination file exists 00710 $status->merge( $this->precheckDestExistence( $predicates ) ); 00711 if ( $status->isOK() ) { 00712 // Update file existence predicates 00713 $predicates['exists'][$this->params['src']] = false; 00714 $predicates['sha1'][$this->params['src']] = false; 00715 $predicates['exists'][$this->params['dst']] = true; 00716 $predicates['sha1'][$this->params['dst']] = $this->sourceSha1; 00717 } 00718 return $status; // safe to call attempt() 00719 } 00720 00724 protected function doAttempt() { 00725 // Do nothing if the src/dst paths are the same 00726 if ( $this->params['src'] !== $this->params['dst'] ) { 00727 if ( !$this->destSameAsSource ) { 00728 // Move the file into the destination 00729 return $this->backend->moveInternal( $this->setFlags( $this->params ) ); 00730 } else { 00731 // Just delete source as the destination needs no changes 00732 $params = array( 'src' => $this->params['src'] ); 00733 return $this->backend->deleteInternal( $this->setFlags( $params ) ); 00734 } 00735 } 00736 return Status::newGood(); 00737 } 00738 00742 public function storagePathsRead() { 00743 return array( $this->params['src'] ); 00744 } 00745 00749 public function storagePathsChanged() { 00750 return array( $this->params['src'], $this->params['dst'] ); 00751 } 00752 } 00753 00758 class DeleteFileOp extends FileOp { 00762 protected function allowedParams() { 00763 return array( array( 'src' ), array( 'ignoreMissingSource' ) ); 00764 } 00765 00770 protected function doPrecheck( array &$predicates ) { 00771 $status = Status::newGood(); 00772 // Check if the source file exists 00773 if ( !$this->fileExists( $this->params['src'], $predicates ) ) { 00774 if ( $this->getParam( 'ignoreMissingSource' ) ) { 00775 $this->doOperation = false; // no-op 00776 // Update file existence predicates (cache 404s) 00777 $predicates['exists'][$this->params['src']] = false; 00778 $predicates['sha1'][$this->params['src']] = false; 00779 return $status; // nothing to do 00780 } else { 00781 $status->fatal( 'backend-fail-notexists', $this->params['src'] ); 00782 return $status; 00783 } 00784 // Check if a file can be placed/changed at the source 00785 } elseif ( !$this->backend->isPathUsableInternal( $this->params['src'] ) ) { 00786 $status->fatal( 'backend-fail-usable', $this->params['src'] ); 00787 $status->fatal( 'backend-fail-delete', $this->params['src'] ); 00788 return $status; 00789 } 00790 // Update file existence predicates 00791 $predicates['exists'][$this->params['src']] = false; 00792 $predicates['sha1'][$this->params['src']] = false; 00793 return $status; // safe to call attempt() 00794 } 00795 00799 protected function doAttempt() { 00800 // Delete the source file 00801 return $this->backend->deleteInternal( $this->setFlags( $this->params ) ); 00802 } 00803 00807 public function storagePathsChanged() { 00808 return array( $this->params['src'] ); 00809 } 00810 } 00811 00815 class NullFileOp extends FileOp {}