MediaWiki
REL1_20
|
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 $sourceSha1; // string 00049 protected $destSameAsSource; // boolean 00050 00051 /* Object life-cycle */ 00052 const STATE_NEW = 1; 00053 const STATE_CHECKED = 2; 00054 const STATE_ATTEMPTED = 3; 00055 00063 final public function __construct( FileBackendStore $backend, array $params ) { 00064 $this->backend = $backend; 00065 list( $required, $optional ) = $this->allowedParams(); 00066 foreach ( $required as $name ) { 00067 if ( isset( $params[$name] ) ) { 00068 $this->params[$name] = $params[$name]; 00069 } else { 00070 throw new MWException( "File operation missing parameter '$name'." ); 00071 } 00072 } 00073 foreach ( $optional as $name ) { 00074 if ( isset( $params[$name] ) ) { 00075 $this->params[$name] = $params[$name]; 00076 } 00077 } 00078 $this->params = $params; 00079 } 00080 00087 final public function setBatchId( $batchId ) { 00088 $this->batchId = $batchId; 00089 } 00090 00097 final public function allowStaleReads( $allowStale ) { 00098 $this->useLatest = !$allowStale; 00099 } 00100 00107 final public function getParam( $name ) { 00108 return isset( $this->params[$name] ) ? $this->params[$name] : null; 00109 } 00110 00116 final public function failed() { 00117 return $this->failed; 00118 } 00119 00125 final public static function newPredicates() { 00126 return array( 'exists' => array(), 'sha1' => array() ); 00127 } 00128 00134 final public static function newDependencies() { 00135 return array( 'read' => array(), 'write' => array() ); 00136 } 00137 00144 final public function applyDependencies( array $deps ) { 00145 $deps['read'] += array_fill_keys( $this->storagePathsRead(), 1 ); 00146 $deps['write'] += array_fill_keys( $this->storagePathsChanged(), 1 ); 00147 return $deps; 00148 } 00149 00156 final public function dependsOn( array $deps ) { 00157 foreach ( $this->storagePathsChanged() as $path ) { 00158 if ( isset( $deps['read'][$path] ) || isset( $deps['write'][$path] ) ) { 00159 return true; // "output" or "anti" dependency 00160 } 00161 } 00162 foreach ( $this->storagePathsRead() as $path ) { 00163 if ( isset( $deps['write'][$path] ) ) { 00164 return true; // "flow" dependency 00165 } 00166 } 00167 return false; 00168 } 00169 00177 final public function getJournalEntries( array $oPredicates, array $nPredicates ) { 00178 $nullEntries = array(); 00179 $updateEntries = array(); 00180 $deleteEntries = array(); 00181 $pathsUsed = array_merge( $this->storagePathsRead(), $this->storagePathsChanged() ); 00182 foreach ( $pathsUsed as $path ) { 00183 $nullEntries[] = array( // assertion for recovery 00184 'op' => 'null', 00185 'path' => $path, 00186 'newSha1' => $this->fileSha1( $path, $oPredicates ) 00187 ); 00188 } 00189 foreach ( $this->storagePathsChanged() as $path ) { 00190 if ( $nPredicates['sha1'][$path] === false ) { // deleted 00191 $deleteEntries[] = array( 00192 'op' => 'delete', 00193 'path' => $path, 00194 'newSha1' => '' 00195 ); 00196 } else { // created/updated 00197 $updateEntries[] = array( 00198 'op' => $this->fileExists( $path, $oPredicates ) ? 'update' : 'create', 00199 'path' => $path, 00200 'newSha1' => $nPredicates['sha1'][$path] 00201 ); 00202 } 00203 } 00204 return array_merge( $nullEntries, $updateEntries, $deleteEntries ); 00205 } 00206 00213 final public function precheck( array &$predicates ) { 00214 if ( $this->state !== self::STATE_NEW ) { 00215 return Status::newFatal( 'fileop-fail-state', self::STATE_NEW, $this->state ); 00216 } 00217 $this->state = self::STATE_CHECKED; 00218 $status = $this->doPrecheck( $predicates ); 00219 if ( !$status->isOK() ) { 00220 $this->failed = true; 00221 } 00222 return $status; 00223 } 00224 00228 protected function doPrecheck( array &$predicates ) { 00229 return Status::newGood(); 00230 } 00231 00237 final public function attempt() { 00238 if ( $this->state !== self::STATE_CHECKED ) { 00239 return Status::newFatal( 'fileop-fail-state', self::STATE_CHECKED, $this->state ); 00240 } elseif ( $this->failed ) { // failed precheck 00241 return Status::newFatal( 'fileop-fail-attempt-precheck' ); 00242 } 00243 $this->state = self::STATE_ATTEMPTED; 00244 $status = $this->doAttempt(); 00245 if ( !$status->isOK() ) { 00246 $this->failed = true; 00247 $this->logFailure( 'attempt' ); 00248 } 00249 return $status; 00250 } 00251 00255 protected function doAttempt() { 00256 return Status::newGood(); 00257 } 00258 00264 final public function attemptAsync() { 00265 $this->async = true; 00266 $result = $this->attempt(); 00267 $this->async = false; 00268 return $result; 00269 } 00270 00276 protected function allowedParams() { 00277 return array( array(), array() ); 00278 } 00279 00286 protected function setFlags( array $params ) { 00287 return array( 'async' => $this->async ) + $params; 00288 } 00289 00295 final public function storagePathsRead() { 00296 return array_map( 'FileBackend::normalizeStoragePath', $this->doStoragePathsRead() ); 00297 } 00298 00303 protected function doStoragePathsRead() { 00304 return array(); 00305 } 00306 00312 final public function storagePathsChanged() { 00313 return array_map( 'FileBackend::normalizeStoragePath', $this->doStoragePathsChanged() ); 00314 } 00315 00320 protected function doStoragePathsChanged() { 00321 return array(); 00322 } 00323 00332 protected function precheckDestExistence( array $predicates ) { 00333 $status = Status::newGood(); 00334 // Get hash of source file/string and the destination file 00335 $this->sourceSha1 = $this->getSourceSha1Base36(); // FS file or data string 00336 if ( $this->sourceSha1 === null ) { // file in storage? 00337 $this->sourceSha1 = $this->fileSha1( $this->params['src'], $predicates ); 00338 } 00339 $this->destSameAsSource = false; 00340 if ( $this->fileExists( $this->params['dst'], $predicates ) ) { 00341 if ( $this->getParam( 'overwrite' ) ) { 00342 return $status; // OK 00343 } elseif ( $this->getParam( 'overwriteSame' ) ) { 00344 $dhash = $this->fileSha1( $this->params['dst'], $predicates ); 00345 // Check if hashes are valid and match each other... 00346 if ( !strlen( $this->sourceSha1 ) || !strlen( $dhash ) ) { 00347 $status->fatal( 'backend-fail-hashes' ); 00348 } elseif ( $this->sourceSha1 !== $dhash ) { 00349 // Give an error if the files are not identical 00350 $status->fatal( 'backend-fail-notsame', $this->params['dst'] ); 00351 } else { 00352 $this->destSameAsSource = true; // OK 00353 } 00354 return $status; // do nothing; either OK or bad status 00355 } else { 00356 $status->fatal( 'backend-fail-alreadyexists', $this->params['dst'] ); 00357 return $status; 00358 } 00359 } 00360 return $status; 00361 } 00362 00369 protected function getSourceSha1Base36() { 00370 return null; // N/A 00371 } 00372 00380 final protected function fileExists( $source, array $predicates ) { 00381 if ( isset( $predicates['exists'][$source] ) ) { 00382 return $predicates['exists'][$source]; // previous op assures this 00383 } else { 00384 $params = array( 'src' => $source, 'latest' => $this->useLatest ); 00385 return $this->backend->fileExists( $params ); 00386 } 00387 } 00388 00396 final protected function fileSha1( $source, array $predicates ) { 00397 if ( isset( $predicates['sha1'][$source] ) ) { 00398 return $predicates['sha1'][$source]; // previous op assures this 00399 } else { 00400 $params = array( 'src' => $source, 'latest' => $this->useLatest ); 00401 return $this->backend->getFileSha1Base36( $params ); 00402 } 00403 } 00404 00410 public function getBackend() { 00411 return $this->backend; 00412 } 00413 00420 final public function logFailure( $action ) { 00421 $params = $this->params; 00422 $params['failedAction'] = $action; 00423 try { 00424 wfDebugLog( 'FileOperation', get_class( $this ) . 00425 " failed (batch #{$this->batchId}): " . FormatJson::encode( $params ) ); 00426 } catch ( Exception $e ) { 00427 // bad config? debug log error? 00428 } 00429 } 00430 } 00431 00436 class StoreFileOp extends FileOp { 00440 protected function allowedParams() { 00441 return array( array( 'src', 'dst' ), 00442 array( 'overwrite', 'overwriteSame', 'disposition' ) ); 00443 } 00444 00449 protected function doPrecheck( array &$predicates ) { 00450 $status = Status::newGood(); 00451 // Check if the source file exists on the file system 00452 if ( !is_file( $this->params['src'] ) ) { 00453 $status->fatal( 'backend-fail-notexists', $this->params['src'] ); 00454 return $status; 00455 // Check if the source file is too big 00456 } elseif ( filesize( $this->params['src'] ) > $this->backend->maxFileSizeInternal() ) { 00457 $status->fatal( 'backend-fail-maxsize', 00458 $this->params['dst'], $this->backend->maxFileSizeInternal() ); 00459 $status->fatal( 'backend-fail-store', $this->params['src'], $this->params['dst'] ); 00460 return $status; 00461 // Check if a file can be placed at the destination 00462 } elseif ( !$this->backend->isPathUsableInternal( $this->params['dst'] ) ) { 00463 $status->fatal( 'backend-fail-usable', $this->params['dst'] ); 00464 $status->fatal( 'backend-fail-store', $this->params['src'], $this->params['dst'] ); 00465 return $status; 00466 } 00467 // Check if destination file exists 00468 $status->merge( $this->precheckDestExistence( $predicates ) ); 00469 if ( $status->isOK() ) { 00470 // Update file existence predicates 00471 $predicates['exists'][$this->params['dst']] = true; 00472 $predicates['sha1'][$this->params['dst']] = $this->sourceSha1; 00473 } 00474 return $status; // safe to call attempt() 00475 } 00476 00480 protected function doAttempt() { 00481 // Store the file at the destination 00482 if ( !$this->destSameAsSource ) { 00483 return $this->backend->storeInternal( $this->setFlags( $this->params ) ); 00484 } 00485 return Status::newGood(); 00486 } 00487 00491 protected function getSourceSha1Base36() { 00492 wfSuppressWarnings(); 00493 $hash = sha1_file( $this->params['src'] ); 00494 wfRestoreWarnings(); 00495 if ( $hash !== false ) { 00496 $hash = wfBaseConvert( $hash, 16, 36, 31 ); 00497 } 00498 return $hash; 00499 } 00500 00501 protected function doStoragePathsChanged() { 00502 return array( $this->params['dst'] ); 00503 } 00504 } 00505 00510 class CreateFileOp extends FileOp { 00511 protected function allowedParams() { 00512 return array( array( 'content', 'dst' ), 00513 array( 'overwrite', 'overwriteSame', 'disposition' ) ); 00514 } 00515 00516 protected function doPrecheck( array &$predicates ) { 00517 $status = Status::newGood(); 00518 // Check if the source data is too big 00519 if ( strlen( $this->getParam( 'content' ) ) > $this->backend->maxFileSizeInternal() ) { 00520 $status->fatal( 'backend-fail-maxsize', 00521 $this->params['dst'], $this->backend->maxFileSizeInternal() ); 00522 $status->fatal( 'backend-fail-create', $this->params['dst'] ); 00523 return $status; 00524 // Check if a file can be placed at the destination 00525 } elseif ( !$this->backend->isPathUsableInternal( $this->params['dst'] ) ) { 00526 $status->fatal( 'backend-fail-usable', $this->params['dst'] ); 00527 $status->fatal( 'backend-fail-create', $this->params['dst'] ); 00528 return $status; 00529 } 00530 // Check if destination file exists 00531 $status->merge( $this->precheckDestExistence( $predicates ) ); 00532 if ( $status->isOK() ) { 00533 // Update file existence predicates 00534 $predicates['exists'][$this->params['dst']] = true; 00535 $predicates['sha1'][$this->params['dst']] = $this->sourceSha1; 00536 } 00537 return $status; // safe to call attempt() 00538 } 00539 00543 protected function doAttempt() { 00544 if ( !$this->destSameAsSource ) { 00545 // Create the file at the destination 00546 return $this->backend->createInternal( $this->setFlags( $this->params ) ); 00547 } 00548 return Status::newGood(); 00549 } 00550 00554 protected function getSourceSha1Base36() { 00555 return wfBaseConvert( sha1( $this->params['content'] ), 16, 36, 31 ); 00556 } 00557 00561 protected function doStoragePathsChanged() { 00562 return array( $this->params['dst'] ); 00563 } 00564 } 00565 00570 class CopyFileOp extends FileOp { 00574 protected function allowedParams() { 00575 return array( array( 'src', 'dst' ), 00576 array( 'overwrite', 'overwriteSame', 'disposition' ) ); 00577 } 00578 00583 protected function doPrecheck( array &$predicates ) { 00584 $status = Status::newGood(); 00585 // Check if the source file exists 00586 if ( !$this->fileExists( $this->params['src'], $predicates ) ) { 00587 $status->fatal( 'backend-fail-notexists', $this->params['src'] ); 00588 return $status; 00589 // Check if a file can be placed at the destination 00590 } elseif ( !$this->backend->isPathUsableInternal( $this->params['dst'] ) ) { 00591 $status->fatal( 'backend-fail-usable', $this->params['dst'] ); 00592 $status->fatal( 'backend-fail-copy', $this->params['src'], $this->params['dst'] ); 00593 return $status; 00594 } 00595 // Check if destination file exists 00596 $status->merge( $this->precheckDestExistence( $predicates ) ); 00597 if ( $status->isOK() ) { 00598 // Update file existence predicates 00599 $predicates['exists'][$this->params['dst']] = true; 00600 $predicates['sha1'][$this->params['dst']] = $this->sourceSha1; 00601 } 00602 return $status; // safe to call attempt() 00603 } 00604 00608 protected function doAttempt() { 00609 // Do nothing if the src/dst paths are the same 00610 if ( $this->params['src'] !== $this->params['dst'] ) { 00611 // Copy the file into the destination 00612 if ( !$this->destSameAsSource ) { 00613 return $this->backend->copyInternal( $this->setFlags( $this->params ) ); 00614 } 00615 } 00616 return Status::newGood(); 00617 } 00618 00622 protected function doStoragePathsRead() { 00623 return array( $this->params['src'] ); 00624 } 00625 00629 protected function doStoragePathsChanged() { 00630 return array( $this->params['dst'] ); 00631 } 00632 } 00633 00638 class MoveFileOp extends FileOp { 00642 protected function allowedParams() { 00643 return array( array( 'src', 'dst' ), 00644 array( 'overwrite', 'overwriteSame', 'disposition' ) ); 00645 } 00646 00651 protected function doPrecheck( array &$predicates ) { 00652 $status = Status::newGood(); 00653 // Check if the source file exists 00654 if ( !$this->fileExists( $this->params['src'], $predicates ) ) { 00655 $status->fatal( 'backend-fail-notexists', $this->params['src'] ); 00656 return $status; 00657 // Check if a file can be placed at the destination 00658 } elseif ( !$this->backend->isPathUsableInternal( $this->params['dst'] ) ) { 00659 $status->fatal( 'backend-fail-usable', $this->params['dst'] ); 00660 $status->fatal( 'backend-fail-move', $this->params['src'], $this->params['dst'] ); 00661 return $status; 00662 } 00663 // Check if destination file exists 00664 $status->merge( $this->precheckDestExistence( $predicates ) ); 00665 if ( $status->isOK() ) { 00666 // Update file existence predicates 00667 $predicates['exists'][$this->params['src']] = false; 00668 $predicates['sha1'][$this->params['src']] = false; 00669 $predicates['exists'][$this->params['dst']] = true; 00670 $predicates['sha1'][$this->params['dst']] = $this->sourceSha1; 00671 } 00672 return $status; // safe to call attempt() 00673 } 00674 00678 protected function doAttempt() { 00679 // Do nothing if the src/dst paths are the same 00680 if ( $this->params['src'] !== $this->params['dst'] ) { 00681 if ( !$this->destSameAsSource ) { 00682 // Move the file into the destination 00683 return $this->backend->moveInternal( $this->setFlags( $this->params ) ); 00684 } else { 00685 // Just delete source as the destination needs no changes 00686 $params = array( 'src' => $this->params['src'] ); 00687 return $this->backend->deleteInternal( $this->setFlags( $params ) ); 00688 } 00689 } 00690 return Status::newGood(); 00691 } 00692 00696 protected function doStoragePathsRead() { 00697 return array( $this->params['src'] ); 00698 } 00699 00703 protected function doStoragePathsChanged() { 00704 return array( $this->params['src'], $this->params['dst'] ); 00705 } 00706 } 00707 00712 class DeleteFileOp extends FileOp { 00716 protected function allowedParams() { 00717 return array( array( 'src' ), array( 'ignoreMissingSource' ) ); 00718 } 00719 00720 protected $needsDelete = true; 00721 00726 protected function doPrecheck( array &$predicates ) { 00727 $status = Status::newGood(); 00728 // Check if the source file exists 00729 if ( !$this->fileExists( $this->params['src'], $predicates ) ) { 00730 if ( !$this->getParam( 'ignoreMissingSource' ) ) { 00731 $status->fatal( 'backend-fail-notexists', $this->params['src'] ); 00732 return $status; 00733 } 00734 $this->needsDelete = false; 00735 } 00736 // Update file existence predicates 00737 $predicates['exists'][$this->params['src']] = false; 00738 $predicates['sha1'][$this->params['src']] = false; 00739 return $status; // safe to call attempt() 00740 } 00741 00745 protected function doAttempt() { 00746 if ( $this->needsDelete ) { 00747 // Delete the source file 00748 return $this->backend->deleteInternal( $this->setFlags( $this->params ) ); 00749 } 00750 return Status::newGood(); 00751 } 00752 00756 protected function doStoragePathsChanged() { 00757 return array( $this->params['src'] ); 00758 } 00759 } 00760 00764 class NullFileOp extends FileOp {}