MediaWiki
REL1_19
|
00001 <?php 00018 abstract class FileOp { 00020 protected $params = array(); 00022 protected $backend; 00023 00024 protected $state = self::STATE_NEW; // integer 00025 protected $failed = false; // boolean 00026 protected $useLatest = true; // boolean 00027 00028 protected $sourceSha1; // string 00029 protected $destSameAsSource; // boolean 00030 00031 /* Object life-cycle */ 00032 const STATE_NEW = 1; 00033 const STATE_CHECKED = 2; 00034 const STATE_ATTEMPTED = 3; 00035 00036 /* Timeout related parameters */ 00037 const MAX_BATCH_SIZE = 1000; 00038 const TIME_LIMIT_SEC = 300; // 5 minutes 00039 00047 final public function __construct( FileBackendStore $backend, array $params ) { 00048 $this->backend = $backend; 00049 list( $required, $optional ) = $this->allowedParams(); 00050 foreach ( $required as $name ) { 00051 if ( isset( $params[$name] ) ) { 00052 $this->params[$name] = $params[$name]; 00053 } else { 00054 throw new MWException( "File operation missing parameter '$name'." ); 00055 } 00056 } 00057 foreach ( $optional as $name ) { 00058 if ( isset( $params[$name] ) ) { 00059 $this->params[$name] = $params[$name]; 00060 } 00061 } 00062 $this->params = $params; 00063 } 00064 00070 final protected function allowStaleReads() { 00071 $this->useLatest = false; 00072 } 00073 00093 final public static function attemptBatch( array $performOps, array $opts ) { 00094 $status = Status::newGood(); 00095 00096 $allowStale = !empty( $opts['allowStale'] ); 00097 $ignoreErrors = !empty( $opts['force'] ); 00098 00099 $n = count( $performOps ); 00100 if ( $n > self::MAX_BATCH_SIZE ) { 00101 $status->fatal( 'backend-fail-batchsize', $n, self::MAX_BATCH_SIZE ); 00102 return $status; 00103 } 00104 00105 $predicates = FileOp::newPredicates(); // account for previous op in prechecks 00106 // Do pre-checks for each operation; abort on failure... 00107 foreach ( $performOps as $index => $fileOp ) { 00108 if ( $allowStale ) { 00109 $fileOp->allowStaleReads(); // allow potentially stale reads 00110 } 00111 $subStatus = $fileOp->precheck( $predicates ); 00112 $status->merge( $subStatus ); 00113 if ( !$subStatus->isOK() ) { // operation failed? 00114 $status->success[$index] = false; 00115 ++$status->failCount; 00116 if ( !$ignoreErrors ) { 00117 return $status; // abort 00118 } 00119 } 00120 } 00121 00122 if ( $ignoreErrors ) { 00123 # Treat all precheck() fatals as merely warnings 00124 $status->setResult( true, $status->value ); 00125 } 00126 00127 // Restart PHP's execution timer and set the timeout to safe amount. 00128 // This handles cases where the operations take a long time or where we are 00129 // already running low on time left. The old timeout is restored afterwards. 00130 # @TODO: re-enable this for when the number of batches is high. 00131 #$scopedTimeLimit = new FileOpScopedPHPTimeout( self::TIME_LIMIT_SEC ); 00132 00133 // Attempt each operation... 00134 foreach ( $performOps as $index => $fileOp ) { 00135 if ( $fileOp->failed() ) { 00136 continue; // nothing to do 00137 } 00138 $subStatus = $fileOp->attempt(); 00139 $status->merge( $subStatus ); 00140 if ( $subStatus->isOK() ) { 00141 $status->success[$index] = true; 00142 ++$status->successCount; 00143 } else { 00144 $status->success[$index] = false; 00145 ++$status->failCount; 00146 // We can't continue (even with $ignoreErrors) as $predicates is wrong. 00147 // Log the remaining ops as failed for recovery... 00148 for ( $i = ($index + 1); $i < count( $performOps ); $i++ ) { 00149 $performOps[$i]->logFailure( 'attempt_aborted' ); 00150 } 00151 return $status; // bail out 00152 } 00153 } 00154 00155 return $status; 00156 } 00157 00164 final public function getParam( $name ) { 00165 return isset( $this->params[$name] ) ? $this->params[$name] : null; 00166 } 00167 00173 final public function failed() { 00174 return $this->failed; 00175 } 00176 00182 final public static function newPredicates() { 00183 return array( 'exists' => array(), 'sha1' => array() ); 00184 } 00185 00192 final public function precheck( array &$predicates ) { 00193 if ( $this->state !== self::STATE_NEW ) { 00194 return Status::newFatal( 'fileop-fail-state', self::STATE_NEW, $this->state ); 00195 } 00196 $this->state = self::STATE_CHECKED; 00197 $status = $this->doPrecheck( $predicates ); 00198 if ( !$status->isOK() ) { 00199 $this->failed = true; 00200 } 00201 return $status; 00202 } 00203 00209 final public function attempt() { 00210 if ( $this->state !== self::STATE_CHECKED ) { 00211 return Status::newFatal( 'fileop-fail-state', self::STATE_CHECKED, $this->state ); 00212 } elseif ( $this->failed ) { // failed precheck 00213 return Status::newFatal( 'fileop-fail-attempt-precheck' ); 00214 } 00215 $this->state = self::STATE_ATTEMPTED; 00216 $status = $this->doAttempt(); 00217 if ( !$status->isOK() ) { 00218 $this->failed = true; 00219 $this->logFailure( 'attempt' ); 00220 } 00221 return $status; 00222 } 00223 00229 protected function allowedParams() { 00230 return array( array(), array() ); 00231 } 00232 00238 public function storagePathsRead() { 00239 return array(); 00240 } 00241 00247 public function storagePathsChanged() { 00248 return array(); 00249 } 00250 00254 protected function doPrecheck( array &$predicates ) { 00255 return Status::newGood(); 00256 } 00257 00261 protected function doAttempt() { 00262 return Status::newGood(); 00263 } 00264 00273 protected function precheckDestExistence( array $predicates ) { 00274 $status = Status::newGood(); 00275 // Get hash of source file/string and the destination file 00276 $this->sourceSha1 = $this->getSourceSha1Base36(); // FS file or data string 00277 if ( $this->sourceSha1 === null ) { // file in storage? 00278 $this->sourceSha1 = $this->fileSha1( $this->params['src'], $predicates ); 00279 } 00280 $this->destSameAsSource = false; 00281 if ( $this->fileExists( $this->params['dst'], $predicates ) ) { 00282 if ( $this->getParam( 'overwrite' ) ) { 00283 return $status; // OK 00284 } elseif ( $this->getParam( 'overwriteSame' ) ) { 00285 $dhash = $this->fileSha1( $this->params['dst'], $predicates ); 00286 // Check if hashes are valid and match each other... 00287 if ( !strlen( $this->sourceSha1 ) || !strlen( $dhash ) ) { 00288 $status->fatal( 'backend-fail-hashes' ); 00289 } elseif ( $this->sourceSha1 !== $dhash ) { 00290 // Give an error if the files are not identical 00291 $status->fatal( 'backend-fail-notsame', $this->params['dst'] ); 00292 } else { 00293 $this->destSameAsSource = true; // OK 00294 } 00295 return $status; // do nothing; either OK or bad status 00296 } else { 00297 $status->fatal( 'backend-fail-alreadyexists', $this->params['dst'] ); 00298 return $status; 00299 } 00300 } 00301 return $status; 00302 } 00303 00310 protected function getSourceSha1Base36() { 00311 return null; // N/A 00312 } 00313 00321 final protected function fileExists( $source, array $predicates ) { 00322 if ( isset( $predicates['exists'][$source] ) ) { 00323 return $predicates['exists'][$source]; // previous op assures this 00324 } else { 00325 $params = array( 'src' => $source, 'latest' => $this->useLatest ); 00326 return $this->backend->fileExists( $params ); 00327 } 00328 } 00329 00337 final protected function fileSha1( $source, array $predicates ) { 00338 if ( isset( $predicates['sha1'][$source] ) ) { 00339 return $predicates['sha1'][$source]; // previous op assures this 00340 } else { 00341 $params = array( 'src' => $source, 'latest' => $this->useLatest ); 00342 return $this->backend->getFileSha1Base36( $params ); 00343 } 00344 } 00345 00352 final protected function logFailure( $action ) { 00353 $params = $this->params; 00354 $params['failedAction'] = $action; 00355 try { 00356 wfDebugLog( 'FileOperation', 00357 get_class( $this ) . ' failed:' . serialize( $params ) ); 00358 } catch ( Exception $e ) { 00359 // bad config? debug log error? 00360 } 00361 } 00362 } 00363 00370 class FileOpScopedPHPTimeout { 00371 protected $startTime; // float; seconds 00372 protected $oldTimeout; // integer; seconds 00373 00374 protected static $stackDepth = 0; // integer 00375 protected static $totalCalls = 0; // integer 00376 protected static $totalElapsed = 0; // float; seconds 00377 00378 /* Prevent callers in infinite loops from running forever */ 00379 const MAX_TOTAL_CALLS = 1000000; 00380 const MAX_TOTAL_TIME = 300; // seconds 00381 00385 public function __construct( $seconds ) { 00386 if ( ini_get( 'max_execution_time' ) > 0 ) { // CLI uses 0 00387 if ( self::$totalCalls >= self::MAX_TOTAL_CALLS ) { 00388 trigger_error( "Maximum invocations of " . __CLASS__ . " exceeded." ); 00389 } elseif ( self::$totalElapsed >= self::MAX_TOTAL_TIME ) { 00390 trigger_error( "Time limit within invocations of " . __CLASS__ . " exceeded." ); 00391 } elseif ( self::$stackDepth > 0 ) { // recursion guard 00392 trigger_error( "Resursive invocation of " . __CLASS__ . " attempted." ); 00393 } else { 00394 $this->oldTimeout = ini_set( 'max_execution_time', $seconds ); 00395 $this->startTime = microtime( true ); 00396 ++self::$stackDepth; 00397 ++self::$totalCalls; // proof against < 1us scopes 00398 } 00399 } 00400 } 00401 00406 public function __destruct() { 00407 if ( $this->oldTimeout ) { 00408 $elapsed = microtime( true ) - $this->startTime; 00409 // Note: a limit of 0 is treated as "forever" 00410 set_time_limit( max( 1, $this->oldTimeout - (int)$elapsed ) ); 00411 // If each scoped timeout is for less than one second, we end up 00412 // restoring the original timeout without any decrease in value. 00413 // Thus web scripts in an infinite loop can run forever unless we 00414 // take some measures to prevent this. Track total time and calls. 00415 self::$totalElapsed += $elapsed; 00416 --self::$stackDepth; 00417 } 00418 } 00419 } 00420 00429 class StoreFileOp extends FileOp { 00430 protected function allowedParams() { 00431 return array( array( 'src', 'dst' ), array( 'overwrite', 'overwriteSame' ) ); 00432 } 00433 00434 protected function doPrecheck( array &$predicates ) { 00435 $status = Status::newGood(); 00436 // Check if the source file exists on the file system 00437 if ( !is_file( $this->params['src'] ) ) { 00438 $status->fatal( 'backend-fail-notexists', $this->params['src'] ); 00439 return $status; 00440 // Check if the source file is too big 00441 } elseif ( filesize( $this->params['src'] ) > $this->backend->maxFileSizeInternal() ) { 00442 $status->fatal( 'backend-fail-store', $this->params['src'], $this->params['dst'] ); 00443 return $status; 00444 // Check if a file can be placed at the destination 00445 } elseif ( !$this->backend->isPathUsableInternal( $this->params['dst'] ) ) { 00446 $status->fatal( 'backend-fail-store', $this->params['src'], $this->params['dst'] ); 00447 return $status; 00448 } 00449 // Check if destination file exists 00450 $status->merge( $this->precheckDestExistence( $predicates ) ); 00451 if ( $status->isOK() ) { 00452 // Update file existence predicates 00453 $predicates['exists'][$this->params['dst']] = true; 00454 $predicates['sha1'][$this->params['dst']] = $this->sourceSha1; 00455 } 00456 return $status; // safe to call attempt() 00457 } 00458 00459 protected function doAttempt() { 00460 $status = Status::newGood(); 00461 // Store the file at the destination 00462 if ( !$this->destSameAsSource ) { 00463 $status->merge( $this->backend->storeInternal( $this->params ) ); 00464 } 00465 return $status; 00466 } 00467 00468 protected function getSourceSha1Base36() { 00469 wfSuppressWarnings(); 00470 $hash = sha1_file( $this->params['src'] ); 00471 wfRestoreWarnings(); 00472 if ( $hash !== false ) { 00473 $hash = wfBaseConvert( $hash, 16, 36, 31 ); 00474 } 00475 return $hash; 00476 } 00477 00478 public function storagePathsChanged() { 00479 return array( $this->params['dst'] ); 00480 } 00481 } 00482 00491 class CreateFileOp extends FileOp { 00492 protected function allowedParams() { 00493 return array( array( 'content', 'dst' ), array( 'overwrite', 'overwriteSame' ) ); 00494 } 00495 00496 protected function doPrecheck( array &$predicates ) { 00497 $status = Status::newGood(); 00498 // Check if the source data is too big 00499 if ( strlen( $this->getParam( 'content' ) ) > $this->backend->maxFileSizeInternal() ) { 00500 $status->fatal( 'backend-fail-create', $this->params['dst'] ); 00501 return $status; 00502 // Check if a file can be placed at the destination 00503 } elseif ( !$this->backend->isPathUsableInternal( $this->params['dst'] ) ) { 00504 $status->fatal( 'backend-fail-create', $this->params['dst'] ); 00505 return $status; 00506 } 00507 // Check if destination file exists 00508 $status->merge( $this->precheckDestExistence( $predicates ) ); 00509 if ( $status->isOK() ) { 00510 // Update file existence predicates 00511 $predicates['exists'][$this->params['dst']] = true; 00512 $predicates['sha1'][$this->params['dst']] = $this->sourceSha1; 00513 } 00514 return $status; // safe to call attempt() 00515 } 00516 00517 protected function doAttempt() { 00518 $status = Status::newGood(); 00519 // Create the file at the destination 00520 if ( !$this->destSameAsSource ) { 00521 $status->merge( $this->backend->createInternal( $this->params ) ); 00522 } 00523 return $status; 00524 } 00525 00526 protected function getSourceSha1Base36() { 00527 return wfBaseConvert( sha1( $this->params['content'] ), 16, 36, 31 ); 00528 } 00529 00530 public function storagePathsChanged() { 00531 return array( $this->params['dst'] ); 00532 } 00533 } 00534 00543 class CopyFileOp extends FileOp { 00544 protected function allowedParams() { 00545 return array( array( 'src', 'dst' ), array( 'overwrite', 'overwriteSame' ) ); 00546 } 00547 00548 protected function doPrecheck( array &$predicates ) { 00549 $status = Status::newGood(); 00550 // Check if the source file exists 00551 if ( !$this->fileExists( $this->params['src'], $predicates ) ) { 00552 $status->fatal( 'backend-fail-notexists', $this->params['src'] ); 00553 return $status; 00554 // Check if a file can be placed at the destination 00555 } elseif ( !$this->backend->isPathUsableInternal( $this->params['dst'] ) ) { 00556 $status->fatal( 'backend-fail-copy', $this->params['src'], $this->params['dst'] ); 00557 return $status; 00558 } 00559 // Check if destination file exists 00560 $status->merge( $this->precheckDestExistence( $predicates ) ); 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 return $status; // safe to call attempt() 00567 } 00568 00569 protected function doAttempt() { 00570 $status = Status::newGood(); 00571 // Do nothing if the src/dst paths are the same 00572 if ( $this->params['src'] !== $this->params['dst'] ) { 00573 // Copy the file into the destination 00574 if ( !$this->destSameAsSource ) { 00575 $status->merge( $this->backend->copyInternal( $this->params ) ); 00576 } 00577 } 00578 return $status; 00579 } 00580 00581 public function storagePathsRead() { 00582 return array( $this->params['src'] ); 00583 } 00584 00585 public function storagePathsChanged() { 00586 return array( $this->params['dst'] ); 00587 } 00588 } 00589 00598 class MoveFileOp extends FileOp { 00599 protected function allowedParams() { 00600 return array( array( 'src', 'dst' ), array( 'overwrite', 'overwriteSame' ) ); 00601 } 00602 00603 protected function doPrecheck( array &$predicates ) { 00604 $status = Status::newGood(); 00605 // Check if the source file exists 00606 if ( !$this->fileExists( $this->params['src'], $predicates ) ) { 00607 $status->fatal( 'backend-fail-notexists', $this->params['src'] ); 00608 return $status; 00609 // Check if a file can be placed at the destination 00610 } elseif ( !$this->backend->isPathUsableInternal( $this->params['dst'] ) ) { 00611 $status->fatal( 'backend-fail-move', $this->params['src'], $this->params['dst'] ); 00612 return $status; 00613 } 00614 // Check if destination file exists 00615 $status->merge( $this->precheckDestExistence( $predicates ) ); 00616 if ( $status->isOK() ) { 00617 // Update file existence predicates 00618 $predicates['exists'][$this->params['src']] = false; 00619 $predicates['sha1'][$this->params['src']] = false; 00620 $predicates['exists'][$this->params['dst']] = true; 00621 $predicates['sha1'][$this->params['dst']] = $this->sourceSha1; 00622 } 00623 return $status; // safe to call attempt() 00624 } 00625 00626 protected function doAttempt() { 00627 $status = Status::newGood(); 00628 // Do nothing if the src/dst paths are the same 00629 if ( $this->params['src'] !== $this->params['dst'] ) { 00630 if ( !$this->destSameAsSource ) { 00631 // Move the file into the destination 00632 $status->merge( $this->backend->moveInternal( $this->params ) ); 00633 } else { 00634 // Just delete source as the destination needs no changes 00635 $params = array( 'src' => $this->params['src'] ); 00636 $status->merge( $this->backend->deleteInternal( $params ) ); 00637 } 00638 } 00639 return $status; 00640 } 00641 00642 public function storagePathsRead() { 00643 return array( $this->params['src'] ); 00644 } 00645 00646 public function storagePathsChanged() { 00647 return array( $this->params['dst'] ); 00648 } 00649 } 00650 00657 class DeleteFileOp extends FileOp { 00658 protected function allowedParams() { 00659 return array( array( 'src' ), array( 'ignoreMissingSource' ) ); 00660 } 00661 00662 protected $needsDelete = true; 00663 00664 protected function doPrecheck( array &$predicates ) { 00665 $status = Status::newGood(); 00666 // Check if the source file exists 00667 if ( !$this->fileExists( $this->params['src'], $predicates ) ) { 00668 if ( !$this->getParam( 'ignoreMissingSource' ) ) { 00669 $status->fatal( 'backend-fail-notexists', $this->params['src'] ); 00670 return $status; 00671 } 00672 $this->needsDelete = false; 00673 } 00674 // Update file existence predicates 00675 $predicates['exists'][$this->params['src']] = false; 00676 $predicates['sha1'][$this->params['src']] = false; 00677 return $status; // safe to call attempt() 00678 } 00679 00680 protected function doAttempt() { 00681 $status = Status::newGood(); 00682 if ( $this->needsDelete ) { 00683 // Delete the source file 00684 $status->merge( $this->backend->deleteInternal( $this->params ) ); 00685 } 00686 return $status; 00687 } 00688 00689 public function storagePathsChanged() { 00690 return array( $this->params['src'] ); 00691 } 00692 } 00693 00697 class NullFileOp extends FileOp {}