MediaWiki
REL1_21
|
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 $destSameAsSource; // 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 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 getParam( $name ) { 00133 return isset( $this->params[$name] ) ? $this->params[$name] : null; 00134 } 00135 00141 final public function failed() { 00142 return $this->failed; 00143 } 00144 00150 final public static function newPredicates() { 00151 return array( 'exists' => array(), 'sha1' => array() ); 00152 } 00153 00159 final public static function newDependencies() { 00160 return array( 'read' => array(), 'write' => array() ); 00161 } 00162 00169 final public function applyDependencies( array $deps ) { 00170 $deps['read'] += array_fill_keys( $this->storagePathsRead(), 1 ); 00171 $deps['write'] += array_fill_keys( $this->storagePathsChanged(), 1 ); 00172 return $deps; 00173 } 00174 00181 final public function dependsOn( array $deps ) { 00182 foreach ( $this->storagePathsChanged() as $path ) { 00183 if ( isset( $deps['read'][$path] ) || isset( $deps['write'][$path] ) ) { 00184 return true; // "output" or "anti" dependency 00185 } 00186 } 00187 foreach ( $this->storagePathsRead() as $path ) { 00188 if ( isset( $deps['write'][$path] ) ) { 00189 return true; // "flow" dependency 00190 } 00191 } 00192 return false; 00193 } 00194 00202 final public function getJournalEntries( array $oPredicates, array $nPredicates ) { 00203 if ( !$this->doOperation ) { 00204 return array(); // this is a no-op 00205 } 00206 $nullEntries = array(); 00207 $updateEntries = array(); 00208 $deleteEntries = array(); 00209 $pathsUsed = array_merge( $this->storagePathsRead(), $this->storagePathsChanged() ); 00210 foreach ( array_unique( $pathsUsed ) as $path ) { 00211 $nullEntries[] = array( // assertion for recovery 00212 'op' => 'null', 00213 'path' => $path, 00214 'newSha1' => $this->fileSha1( $path, $oPredicates ) 00215 ); 00216 } 00217 foreach ( $this->storagePathsChanged() as $path ) { 00218 if ( $nPredicates['sha1'][$path] === false ) { // deleted 00219 $deleteEntries[] = array( 00220 'op' => 'delete', 00221 'path' => $path, 00222 'newSha1' => '' 00223 ); 00224 } else { // created/updated 00225 $updateEntries[] = array( 00226 'op' => $this->fileExists( $path, $oPredicates ) ? 'update' : 'create', 00227 'path' => $path, 00228 'newSha1' => $nPredicates['sha1'][$path] 00229 ); 00230 } 00231 } 00232 return array_merge( $nullEntries, $updateEntries, $deleteEntries ); 00233 } 00234 00243 final public function precheck( array &$predicates ) { 00244 if ( $this->state !== self::STATE_NEW ) { 00245 return Status::newFatal( 'fileop-fail-state', self::STATE_NEW, $this->state ); 00246 } 00247 $this->state = self::STATE_CHECKED; 00248 $status = $this->doPrecheck( $predicates ); 00249 if ( !$status->isOK() ) { 00250 $this->failed = true; 00251 } 00252 return $status; 00253 } 00254 00258 protected function doPrecheck( array &$predicates ) { 00259 return Status::newGood(); 00260 } 00261 00267 final public function attempt() { 00268 if ( $this->state !== self::STATE_CHECKED ) { 00269 return Status::newFatal( 'fileop-fail-state', self::STATE_CHECKED, $this->state ); 00270 } elseif ( $this->failed ) { // failed precheck 00271 return Status::newFatal( 'fileop-fail-attempt-precheck' ); 00272 } 00273 $this->state = self::STATE_ATTEMPTED; 00274 if ( $this->doOperation ) { 00275 $status = $this->doAttempt(); 00276 if ( !$status->isOK() ) { 00277 $this->failed = true; 00278 $this->logFailure( 'attempt' ); 00279 } 00280 } else { // no-op 00281 $status = Status::newGood(); 00282 } 00283 return $status; 00284 } 00285 00289 protected function doAttempt() { 00290 return Status::newGood(); 00291 } 00292 00298 final public function attemptAsync() { 00299 $this->async = true; 00300 $result = $this->attempt(); 00301 $this->async = false; 00302 return $result; 00303 } 00304 00310 protected function allowedParams() { 00311 return array( array(), array() ); 00312 } 00313 00320 protected function setFlags( array $params ) { 00321 return array( 'async' => $this->async ) + $params; 00322 } 00323 00329 public function storagePathsRead() { 00330 return array(); 00331 } 00332 00338 public function storagePathsChanged() { 00339 return array(); 00340 } 00341 00350 protected function precheckDestExistence( array $predicates ) { 00351 $status = Status::newGood(); 00352 // Get hash of source file/string and the destination file 00353 $this->sourceSha1 = $this->getSourceSha1Base36(); // FS file or data string 00354 if ( $this->sourceSha1 === null ) { // file in storage? 00355 $this->sourceSha1 = $this->fileSha1( $this->params['src'], $predicates ); 00356 } 00357 $this->destSameAsSource = false; 00358 $this->destExists = $this->fileExists( $this->params['dst'], $predicates ); 00359 if ( $this->destExists ) { 00360 if ( $this->getParam( 'overwrite' ) ) { 00361 return $status; // OK 00362 } elseif ( $this->getParam( 'overwriteSame' ) ) { 00363 $dhash = $this->fileSha1( $this->params['dst'], $predicates ); 00364 // Check if hashes are valid and match each other... 00365 if ( !strlen( $this->sourceSha1 ) || !strlen( $dhash ) ) { 00366 $status->fatal( 'backend-fail-hashes' ); 00367 } elseif ( $this->sourceSha1 !== $dhash ) { 00368 // Give an error if the files are not identical 00369 $status->fatal( 'backend-fail-notsame', $this->params['dst'] ); 00370 } else { 00371 $this->destSameAsSource = true; // OK 00372 } 00373 return $status; // do nothing; either OK or bad status 00374 } else { 00375 $status->fatal( 'backend-fail-alreadyexists', $this->params['dst'] ); 00376 return $status; 00377 } 00378 } 00379 return $status; 00380 } 00381 00388 protected function getSourceSha1Base36() { 00389 return null; // N/A 00390 } 00391 00399 final protected function fileExists( $source, array $predicates ) { 00400 if ( isset( $predicates['exists'][$source] ) ) { 00401 return $predicates['exists'][$source]; // previous op assures this 00402 } else { 00403 $params = array( 'src' => $source, 'latest' => true ); 00404 return $this->backend->fileExists( $params ); 00405 } 00406 } 00407 00415 final protected function fileSha1( $source, array $predicates ) { 00416 if ( isset( $predicates['sha1'][$source] ) ) { 00417 return $predicates['sha1'][$source]; // previous op assures this 00418 } elseif ( isset( $predicates['exists'][$source] ) && !$predicates['exists'][$source] ) { 00419 return false; // previous op assures this 00420 } else { 00421 $params = array( 'src' => $source, 'latest' => true ); 00422 return $this->backend->getFileSha1Base36( $params ); 00423 } 00424 } 00425 00431 public function getBackend() { 00432 return $this->backend; 00433 } 00434 00441 final public function logFailure( $action ) { 00442 $params = $this->params; 00443 $params['failedAction'] = $action; 00444 try { 00445 wfDebugLog( 'FileOperation', get_class( $this ) . 00446 " failed (batch #{$this->batchId}): " . FormatJson::encode( $params ) ); 00447 } catch ( Exception $e ) { 00448 // bad config? debug log error? 00449 } 00450 } 00451 } 00452 00457 class CreateFileOp extends FileOp { 00458 protected function allowedParams() { 00459 return array( array( 'content', 'dst' ), 00460 array( 'overwrite', 'overwriteSame', 'disposition', 'headers' ) ); 00461 } 00462 00463 protected function doPrecheck( array &$predicates ) { 00464 $status = Status::newGood(); 00465 // Check if the source data is too big 00466 if ( strlen( $this->getParam( 'content' ) ) > $this->backend->maxFileSizeInternal() ) { 00467 $status->fatal( 'backend-fail-maxsize', 00468 $this->params['dst'], $this->backend->maxFileSizeInternal() ); 00469 $status->fatal( 'backend-fail-create', $this->params['dst'] ); 00470 return $status; 00471 // Check if a file can be placed/changed at the destination 00472 } elseif ( !$this->backend->isPathUsableInternal( $this->params['dst'] ) ) { 00473 $status->fatal( 'backend-fail-usable', $this->params['dst'] ); 00474 $status->fatal( 'backend-fail-create', $this->params['dst'] ); 00475 return $status; 00476 } 00477 // Check if destination file exists 00478 $status->merge( $this->precheckDestExistence( $predicates ) ); 00479 $this->params['dstExists'] = $this->destExists; // see FileBackendStore::setFileCache() 00480 if ( $status->isOK() ) { 00481 // Update file existence predicates 00482 $predicates['exists'][$this->params['dst']] = true; 00483 $predicates['sha1'][$this->params['dst']] = $this->sourceSha1; 00484 } 00485 return $status; // safe to call attempt() 00486 } 00487 00491 protected function doAttempt() { 00492 if ( !$this->destSameAsSource ) { 00493 // Create the file at the destination 00494 return $this->backend->createInternal( $this->setFlags( $this->params ) ); 00495 } 00496 return Status::newGood(); 00497 } 00498 00502 protected function getSourceSha1Base36() { 00503 return wfBaseConvert( sha1( $this->params['content'] ), 16, 36, 31 ); 00504 } 00505 00509 public function storagePathsChanged() { 00510 return array( $this->params['dst'] ); 00511 } 00512 } 00513 00518 class StoreFileOp extends FileOp { 00522 protected function allowedParams() { 00523 return array( array( 'src', 'dst' ), 00524 array( 'overwrite', 'overwriteSame', 'disposition', 'headers' ) ); 00525 } 00526 00531 protected function doPrecheck( array &$predicates ) { 00532 $status = Status::newGood(); 00533 // Check if the source file exists on the file system 00534 if ( !is_file( $this->params['src'] ) ) { 00535 $status->fatal( 'backend-fail-notexists', $this->params['src'] ); 00536 return $status; 00537 // Check if the source file is too big 00538 } elseif ( filesize( $this->params['src'] ) > $this->backend->maxFileSizeInternal() ) { 00539 $status->fatal( 'backend-fail-maxsize', 00540 $this->params['dst'], $this->backend->maxFileSizeInternal() ); 00541 $status->fatal( 'backend-fail-store', $this->params['src'], $this->params['dst'] ); 00542 return $status; 00543 // Check if a file can be placed/changed at the destination 00544 } elseif ( !$this->backend->isPathUsableInternal( $this->params['dst'] ) ) { 00545 $status->fatal( 'backend-fail-usable', $this->params['dst'] ); 00546 $status->fatal( 'backend-fail-store', $this->params['src'], $this->params['dst'] ); 00547 return $status; 00548 } 00549 // Check if destination file exists 00550 $status->merge( $this->precheckDestExistence( $predicates ) ); 00551 $this->params['dstExists'] = $this->destExists; // see FileBackendStore::setFileCache() 00552 if ( $status->isOK() ) { 00553 // Update file existence predicates 00554 $predicates['exists'][$this->params['dst']] = true; 00555 $predicates['sha1'][$this->params['dst']] = $this->sourceSha1; 00556 } 00557 return $status; // safe to call attempt() 00558 } 00559 00563 protected function doAttempt() { 00564 // Store the file at the destination 00565 if ( !$this->destSameAsSource ) { 00566 return $this->backend->storeInternal( $this->setFlags( $this->params ) ); 00567 } 00568 return Status::newGood(); 00569 } 00570 00574 protected function getSourceSha1Base36() { 00575 wfSuppressWarnings(); 00576 $hash = sha1_file( $this->params['src'] ); 00577 wfRestoreWarnings(); 00578 if ( $hash !== false ) { 00579 $hash = wfBaseConvert( $hash, 16, 36, 31 ); 00580 } 00581 return $hash; 00582 } 00583 00584 public function storagePathsChanged() { 00585 return array( $this->params['dst'] ); 00586 } 00587 } 00588 00593 class CopyFileOp extends FileOp { 00597 protected function allowedParams() { 00598 return array( array( 'src', 'dst' ), 00599 array( 'overwrite', 'overwriteSame', 'ignoreMissingSource', 'disposition' ) ); 00600 } 00601 00606 protected function doPrecheck( array &$predicates ) { 00607 $status = Status::newGood(); 00608 // Check if the source file exists 00609 if ( !$this->fileExists( $this->params['src'], $predicates ) ) { 00610 if ( $this->getParam( 'ignoreMissingSource' ) ) { 00611 $this->doOperation = false; // no-op 00612 // Update file existence predicates (cache 404s) 00613 $predicates['exists'][$this->params['src']] = false; 00614 $predicates['sha1'][$this->params['src']] = false; 00615 return $status; // nothing to do 00616 } else { 00617 $status->fatal( 'backend-fail-notexists', $this->params['src'] ); 00618 return $status; 00619 } 00620 // Check if a file can be placed/changed at the destination 00621 } elseif ( !$this->backend->isPathUsableInternal( $this->params['dst'] ) ) { 00622 $status->fatal( 'backend-fail-usable', $this->params['dst'] ); 00623 $status->fatal( 'backend-fail-copy', $this->params['src'], $this->params['dst'] ); 00624 return $status; 00625 } 00626 // Check if destination file exists 00627 $status->merge( $this->precheckDestExistence( $predicates ) ); 00628 $this->params['dstExists'] = $this->destExists; // see FileBackendStore::setFileCache() 00629 if ( $status->isOK() ) { 00630 // Update file existence predicates 00631 $predicates['exists'][$this->params['dst']] = true; 00632 $predicates['sha1'][$this->params['dst']] = $this->sourceSha1; 00633 } 00634 return $status; // safe to call attempt() 00635 } 00636 00640 protected function doAttempt() { 00641 // Do nothing if the src/dst paths are the same 00642 if ( $this->params['src'] !== $this->params['dst'] ) { 00643 // Copy the file into the destination 00644 if ( !$this->destSameAsSource ) { 00645 return $this->backend->copyInternal( $this->setFlags( $this->params ) ); 00646 } 00647 } 00648 return Status::newGood(); 00649 } 00650 00654 public function storagePathsRead() { 00655 return array( $this->params['src'] ); 00656 } 00657 00661 public function storagePathsChanged() { 00662 return array( $this->params['dst'] ); 00663 } 00664 } 00665 00670 class MoveFileOp extends FileOp { 00674 protected function allowedParams() { 00675 return array( array( 'src', 'dst' ), 00676 array( 'overwrite', 'overwriteSame', 'ignoreMissingSource', 'disposition' ) ); 00677 } 00678 00683 protected function doPrecheck( array &$predicates ) { 00684 $status = Status::newGood(); 00685 // Check if the source file exists 00686 if ( !$this->fileExists( $this->params['src'], $predicates ) ) { 00687 if ( $this->getParam( 'ignoreMissingSource' ) ) { 00688 $this->doOperation = false; // no-op 00689 // Update file existence predicates (cache 404s) 00690 $predicates['exists'][$this->params['src']] = false; 00691 $predicates['sha1'][$this->params['src']] = false; 00692 return $status; // nothing to do 00693 } else { 00694 $status->fatal( 'backend-fail-notexists', $this->params['src'] ); 00695 return $status; 00696 } 00697 // Check if a file can be placed/changed at the destination 00698 } elseif ( !$this->backend->isPathUsableInternal( $this->params['dst'] ) ) { 00699 $status->fatal( 'backend-fail-usable', $this->params['dst'] ); 00700 $status->fatal( 'backend-fail-move', $this->params['src'], $this->params['dst'] ); 00701 return $status; 00702 } 00703 // Check if destination file exists 00704 $status->merge( $this->precheckDestExistence( $predicates ) ); 00705 $this->params['dstExists'] = $this->destExists; // see FileBackendStore::setFileCache() 00706 if ( $status->isOK() ) { 00707 // Update file existence predicates 00708 $predicates['exists'][$this->params['src']] = false; 00709 $predicates['sha1'][$this->params['src']] = false; 00710 $predicates['exists'][$this->params['dst']] = true; 00711 $predicates['sha1'][$this->params['dst']] = $this->sourceSha1; 00712 } 00713 return $status; // safe to call attempt() 00714 } 00715 00719 protected function doAttempt() { 00720 // Do nothing if the src/dst paths are the same 00721 if ( $this->params['src'] !== $this->params['dst'] ) { 00722 if ( !$this->destSameAsSource ) { 00723 // Move the file into the destination 00724 return $this->backend->moveInternal( $this->setFlags( $this->params ) ); 00725 } else { 00726 // Just delete source as the destination needs no changes 00727 $params = array( 'src' => $this->params['src'] ); 00728 return $this->backend->deleteInternal( $this->setFlags( $params ) ); 00729 } 00730 } 00731 return Status::newGood(); 00732 } 00733 00737 public function storagePathsRead() { 00738 return array( $this->params['src'] ); 00739 } 00740 00744 public function storagePathsChanged() { 00745 return array( $this->params['src'], $this->params['dst'] ); 00746 } 00747 } 00748 00753 class DeleteFileOp extends FileOp { 00757 protected function allowedParams() { 00758 return array( array( 'src' ), array( 'ignoreMissingSource' ) ); 00759 } 00760 00765 protected function doPrecheck( array &$predicates ) { 00766 $status = Status::newGood(); 00767 // Check if the source file exists 00768 if ( !$this->fileExists( $this->params['src'], $predicates ) ) { 00769 if ( $this->getParam( 'ignoreMissingSource' ) ) { 00770 $this->doOperation = false; // no-op 00771 // Update file existence predicates (cache 404s) 00772 $predicates['exists'][$this->params['src']] = false; 00773 $predicates['sha1'][$this->params['src']] = false; 00774 return $status; // nothing to do 00775 } else { 00776 $status->fatal( 'backend-fail-notexists', $this->params['src'] ); 00777 return $status; 00778 } 00779 // Check if a file can be placed/changed at the source 00780 } elseif ( !$this->backend->isPathUsableInternal( $this->params['src'] ) ) { 00781 $status->fatal( 'backend-fail-usable', $this->params['src'] ); 00782 $status->fatal( 'backend-fail-delete', $this->params['src'] ); 00783 return $status; 00784 } 00785 // Update file existence predicates 00786 $predicates['exists'][$this->params['src']] = false; 00787 $predicates['sha1'][$this->params['src']] = false; 00788 return $status; // safe to call attempt() 00789 } 00790 00794 protected function doAttempt() { 00795 // Delete the source file 00796 return $this->backend->deleteInternal( $this->setFlags( $this->params ) ); 00797 } 00798 00802 public function storagePathsChanged() { 00803 return array( $this->params['src'] ); 00804 } 00805 } 00806 00811 class DescribeFileOp extends FileOp { 00815 protected function allowedParams() { 00816 return array( array( 'src' ), array( 'disposition', 'headers' ) ); 00817 } 00818 00823 protected function doPrecheck( array &$predicates ) { 00824 $status = Status::newGood(); 00825 // Check if the source file exists 00826 if ( !$this->fileExists( $this->params['src'], $predicates ) ) { 00827 $status->fatal( 'backend-fail-notexists', $this->params['src'] ); 00828 return $status; 00829 // Check if a file can be placed/changed at the source 00830 } elseif ( !$this->backend->isPathUsableInternal( $this->params['src'] ) ) { 00831 $status->fatal( 'backend-fail-usable', $this->params['src'] ); 00832 $status->fatal( 'backend-fail-describe', $this->params['src'] ); 00833 return $status; 00834 } 00835 // Update file existence predicates 00836 $predicates['exists'][$this->params['src']] = 00837 $this->fileExists( $this->params['src'], $predicates ); 00838 $predicates['sha1'][$this->params['src']] = 00839 $this->fileSha1( $this->params['src'], $predicates ); 00840 return $status; // safe to call attempt() 00841 } 00842 00846 protected function doAttempt() { 00847 // Update the source file's metadata 00848 return $this->backend->describeInternal( $this->setFlags( $this->params ) ); 00849 } 00850 00854 public function storagePathsChanged() { 00855 return array( $this->params['src'] ); 00856 } 00857 } 00858 00862 class NullFileOp extends FileOp {}