r104127 MediaWiki - Code Review archive

Repository:MediaWiki
Revision:r104126‎ | r104127 | r104128 >
Date:01:59, 24 November 2011
Author:aaron
Status:deferred
Tags:
Comment:
* Moved FileOp class to its own file
* Added FileBackend::create() function to create a file from a string and prepare() to setup a directory (mostly for FS)
* Made FSFileBackend gives errors when wfMkdirParents() fails
* Committed very unfinished FileRepo changes
Modified paths:
  • /branches/FileBackend/phase3/includes/filerepo/FileRepo.php (modified) (history)
  • /branches/FileBackend/phase3/includes/filerepo/backend/FSFileBackend.php (modified) (history)
  • /branches/FileBackend/phase3/includes/filerepo/backend/FileBackend.php (modified) (history)
  • /branches/FileBackend/phase3/includes/filerepo/backend/FileBackendMultiWrite.php (modified) (history)
  • /branches/FileBackend/phase3/includes/filerepo/backend/FileLockManager.php (modified) (history)
  • /branches/FileBackend/phase3/includes/filerepo/backend/FileOp.php (added) (history)

Diff [purge]

Index: branches/FileBackend/phase3/includes/filerepo/backend/FileBackendMultiWrite.php
@@ -132,6 +132,11 @@
133133 return $this->doOperation( array( $op ) );
134134 }
135135
 136+ function create( array $params ) {
 137+ $op = array( 'operation' => 'create' ) + $params;
 138+ return $this->doOperation( array( $op ) );
 139+ }
 140+
136141 function fileExists( array $params ) {
137142 foreach ( $this->backends as $backend ) {
138143 if ( $backend->fileExists( $params ) ) {
Index: branches/FileBackend/phase3/includes/filerepo/backend/FileOp.php
@@ -0,0 +1,438 @@
 2+<?php
 3+/**
 4+ * @file
 5+ * @ingroup FileRepo
 6+ */
 7+
 8+/**
 9+ * Helper class for representing operations with transaction support.
 10+ * FileBackend::doOperations() will require these classes for supported operations.
 11+ *
 12+ * Access use of large fields should be avoided as we want to be able to support
 13+ * potentially many FileOp classes in large arrays in memory.
 14+ */
 15+abstract class FileOp {
 16+ /** $var Array */
 17+ protected $params;
 18+ /** $var FileBackend */
 19+ protected $backend;
 20+
 21+ protected $state;
 22+ protected $failedAttempt;
 23+
 24+ const STATE_NEW = 1;
 25+ const STATE_ATTEMPTED = 2;
 26+ const STATE_DONE = 3;
 27+
 28+ /**
 29+ * Build a new file operation transaction
 30+ *
 31+ * @params $backend FileBackend
 32+ * @params $params Array
 33+ */
 34+ final public function __construct( FileBackend $backend, array $params ) {
 35+ $this->backend = $backend;
 36+ $this->params = $params;
 37+ $this->state = self::STATE_NEW;
 38+ $this->failedAttempt = false;
 39+ $this->initialize();
 40+ }
 41+
 42+ /**
 43+ * Attempt the operation; this must be reversible
 44+ *
 45+ * @return Status
 46+ */
 47+ final public function attempt() {
 48+ if ( $this->state !== self::STATE_NEW ) {
 49+ throw new MWException( "Cannot attempt operation called twice." );
 50+ }
 51+ $this->state = self::STATE_ATTEMPTED;
 52+ $status = $this->doAttempt();
 53+ if ( !$status->isOK() ) {
 54+ $this->failedAttempt = true;
 55+ }
 56+ return $status;
 57+ }
 58+
 59+ /**
 60+ * Revert the operation; affected files are restored
 61+ *
 62+ * @return Status
 63+ */
 64+ final public function revert() {
 65+ if ( $this->state !== self::STATE_ATTEMPTED ) {
 66+ throw new MWException( "Cannot rollback an unstarted or finished operation." );
 67+ }
 68+ $this->state = self::STATE_DONE;
 69+ if ( $this->failedAttempt ) {
 70+ $status = Status::newGood(); // nothing to revert
 71+ } else {
 72+ $status = $this->doRevert();
 73+ }
 74+ return $status;
 75+ }
 76+
 77+ /**
 78+ * Finish the operation; this may be irreversible
 79+ *
 80+ * @return Status
 81+ */
 82+ final public function finish() {
 83+ if ( $this->state !== self::STATE_ATTEMPTED ) {
 84+ throw new MWException( "Cannot cleanup an unstarted or finished operation." );
 85+ }
 86+ $this->state = self::STATE_DONE;
 87+ if ( $this->failedAttempt ) {
 88+ $status = Status::newGood(); // nothing to revert
 89+ } else {
 90+ $status = $this->doFinish();
 91+ }
 92+ return $status;
 93+ }
 94+
 95+ /**
 96+ * Get a list of storage paths to lock for this operation
 97+ *
 98+ * @return Array
 99+ */
 100+ public function storagePathsToLock() {
 101+ return array();
 102+ }
 103+
 104+ /**
 105+ * @return void
 106+ */
 107+ protected function initialize() {}
 108+
 109+ /**
 110+ * @return Status
 111+ */
 112+ abstract protected function doAttempt();
 113+
 114+ /**
 115+ * @return Status
 116+ */
 117+ abstract protected function doRevert();
 118+
 119+ /**
 120+ * @return Status
 121+ */
 122+ abstract protected function doFinish();
 123+}
 124+
 125+/**
 126+ * Store a file into the backend from a file on disk.
 127+ * Parameters must match FileBackend::store(), which include:
 128+ * source : source path on disk
 129+ * dest : destination storage path
 130+ * overwriteDest : do nothing and pass if an identical file exists at destination
 131+ * overwriteSame : override any existing file at destination
 132+ */
 133+class FileStoreOp extends FileOp {
 134+ /** @var TempLocalFile|null */
 135+ protected $tmpDestFile; // temp copy of existing destination file
 136+
 137+ function doAttempt() {
 138+ // Create a backup copy of any file that exists at destination
 139+ $status = $this->backupDest();
 140+ if ( !$status->isOK() ) {
 141+ return $status;
 142+ }
 143+ // Store the file at the destination
 144+ $status = $this->backend->store( $this->params );
 145+ return $status;
 146+ }
 147+
 148+ function doRevert() {
 149+ // Remove the file saved to the destination
 150+ $params = array( 'source' => $this->params['dest'] );
 151+ $status = $this->backend->delete( $params );
 152+ if ( !$status->isOK() ) {
 153+ return $status; // also can't restore any dest file
 154+ }
 155+ // Restore any file that was at the destination
 156+ $status = $this->restoreDest();
 157+ return $status;
 158+ }
 159+
 160+ function doFinish() {
 161+ return Status::newGood();
 162+ }
 163+
 164+ function storagePathsToLock() {
 165+ return array( $this->params['dest'] );
 166+ }
 167+
 168+ /**
 169+ * Backup any file at destination to a temporary file.
 170+ * Don't bother backing it up unless we might overwrite the file.
 171+ *
 172+ * @return Status
 173+ */
 174+ protected function backupDest() {
 175+ $status = Status::newGood();
 176+ // Check if a file already exists at the destination...
 177+ if ( $this->backend->fileExists( $this->params['dest'] ) ) {
 178+ if ( $this->params['overwriteDest'] ) {
 179+ // Create a temporary backup copy...
 180+ $this->tmpDestFile = $this->getLocalCopy( $this->params['dest'] );
 181+ if ( !$this->tmpDestFile ) {
 182+ $status->fatal( 'backend-fail-restore', $this->params['dest'] );
 183+ return $status;
 184+ }
 185+ }
 186+ }
 187+ return $status;
 188+ }
 189+
 190+ /**
 191+ * Restore any temporary destination backup file
 192+ *
 193+ * @return Status
 194+ */
 195+ protected function restoreDest() {
 196+ $status = Status::newGood();
 197+ // Restore any file that was at the destination
 198+ if ( $this->tmpDestFile ) {
 199+ $params = array(
 200+ 'source' => $this->tmpDestFile->getPath(),
 201+ 'dest' => $this->params['dest']
 202+ );
 203+ $status = $this->backend->store( $params );
 204+ if ( !$status->isOK() ) {
 205+ return $status;
 206+ }
 207+ }
 208+ return $status;
 209+ }
 210+}
 211+
 212+/**
 213+ * Create a file in the backend with the given content.
 214+ * Parameters must match FileBackend::create(), which include:
 215+ * content : a string of raw file contents
 216+ * dest : destination storage path
 217+ * overwriteDest : do nothing and pass if an identical file exists at destination
 218+ * overwriteSame : override any existing file at destination
 219+ */
 220+class FileCreateOp extends FileStoreOp {
 221+ function doAttempt() {
 222+ // Create a backup copy of any file that exists at destination
 223+ $status = $this->backupDest();
 224+ if ( !$status->isOK() ) {
 225+ return $status;
 226+ }
 227+ // Create the file at the destination
 228+ $status = $this->backend->create( $this->params );
 229+ return $status;
 230+ }
 231+
 232+ function doRevert() {
 233+ // Remove the file saved to the destination
 234+ $params = array( 'source' => $this->params['dest'] );
 235+ $status = $this->backend->delete( $params );
 236+ if ( !$status->isOK() ) {
 237+ return $status; // also can't restore any dest file
 238+ }
 239+ // Restore any file that was at the destination
 240+ $status = $this->restoreDest();
 241+ return $status;
 242+ }
 243+
 244+ function doFinish() {
 245+ return Status::newGood();
 246+ }
 247+
 248+ function storagePathsToLock() {
 249+ return array( $this->params['dest'] );
 250+ }
 251+}
 252+
 253+/**
 254+ * Copy a file from one storage path to another in the backend.
 255+ * Parameters must match FileBackend::copy(), which include:
 256+ * source : source storage path
 257+ * dest : destination storage path
 258+ * overwriteDest : do nothing and pass if an identical file exists at destination
 259+ * overwriteSame : override any existing file at destination
 260+ */
 261+class FileCopyOp extends FileStoreOp {
 262+ function doAttempt() {
 263+ // Create a backup copy of any file that exists at destination
 264+ $status = $this->backupDest();
 265+ if ( !$status->isOK() ) {
 266+ return $status;
 267+ }
 268+ // Copy the file into the destination
 269+ $status = $this->backend->copy( $this->params );
 270+ return $status;
 271+ }
 272+
 273+ function doRevert() {
 274+ // Remove the file saved to the destination
 275+ $params = array( 'source' => $this->params['dest'] );
 276+ $status = $this->backend->delete( $params );
 277+ if ( !$status->isOK() ) {
 278+ return $status; // also can't restore any dest file
 279+ }
 280+ // Restore any file that was at the destination
 281+ $status = $this->restoreDest();
 282+ return $status;
 283+ }
 284+
 285+ function doFinish() {
 286+ return Status::newGood();
 287+ }
 288+
 289+ function storagePathsToLock() {
 290+ return array( $this->params['source'], $this->params['dest'] );
 291+ }
 292+}
 293+
 294+/**
 295+ * Move a file from one storage path to another in the backend.
 296+ * Parameters must match FileBackend::move(), which include:
 297+ * source : source storage path
 298+ * dest : destination storage path
 299+ * overwriteDest : do nothing and pass if an identical file exists at destination
 300+ * overwriteSame : override any existing file at destination
 301+ */
 302+class FileMoveOp extends FileStoreOp {
 303+ protected $usingMove = false; // using backend move() function?
 304+
 305+ function initialize() {
 306+ // Use faster, native, move() if applicable
 307+ $this->usingMove = $this->backend->canMove( $this->params );
 308+ }
 309+
 310+ function doAttempt() {
 311+ // Create a backup copy of any file that exists at destination
 312+ $status = $this->backupDest();
 313+ if ( !$status->isOK() ) {
 314+ return $status;
 315+ }
 316+ // Native moves: move the file into the destination
 317+ if ( $this->usingMove ) {
 318+ $status = $this->backend->move( $this->params );
 319+ // Non-native moves: copy the file into the destination
 320+ } else {
 321+ $status = $this->backend->copy( $this->params );
 322+ }
 323+ return $status;
 324+ }
 325+
 326+ function doRevert() {
 327+ // Native moves: move the file back to the source
 328+ if ( $this->usingMove ) {
 329+ $params = array(
 330+ 'source' => $this->params['dest'],
 331+ 'dest' => $this->params['source']
 332+ );
 333+ $status = $this->backend->move( $params );
 334+ if ( !$status->isOK() ) {
 335+ return $status; // also can't restore any dest file
 336+ }
 337+ // Non-native moves: remove the file saved to the destination
 338+ } else {
 339+ $params = array( 'source' => $this->params['dest'] );
 340+ $status = $this->backend->delete( $params );
 341+ if ( !$status->isOK() ) {
 342+ return $status; // also can't restore any dest file
 343+ }
 344+ }
 345+ // Restore any file that was at the destination
 346+ $status = $this->restoreDest();
 347+ return $status;
 348+ }
 349+
 350+ function doFinish() {
 351+ // Native moves: nothing is at the source anymore
 352+ if ( $this->usingMove ) {
 353+ $status = Status::newGood();
 354+ // Non-native moves: delete the source file
 355+ } else {
 356+ $params = array( 'source' => $this->params['source'] );
 357+ $status = $this->backend->delete( $params );
 358+ }
 359+ return $status;
 360+ }
 361+
 362+ function storagePathsToLock() {
 363+ return array( $this->params['source'], $this->params['dest'] );
 364+ }
 365+}
 366+
 367+/**
 368+ * Combines files from severals storage paths into a new file in the backend.
 369+ * Parameters must match FileBackend::concatenate(), which include:
 370+ * sources : ordered source storage paths (e.g. chunk1,chunk2,...)
 371+ * dest : destination storage path
 372+ * overwriteDest : do nothing and pass if an identical file exists at destination
 373+ * overwriteSame : override any existing file at destination
 374+ */
 375+class FileConcatenateOp extends FileStoreOp {
 376+ function doAttempt() {
 377+ // Create a backup copy of any file that exists at destination
 378+ $status = $this->backupDest();
 379+ if ( !$status->isOK() ) {
 380+ return $status;
 381+ }
 382+ // Concatenate the file at the destination
 383+ $status = $this->backend->concatenate( $this->params );
 384+ return $status;
 385+ }
 386+
 387+ function doRevert() {
 388+ // Remove the file saved to the destination
 389+ $params = array( 'source' => $this->params['dest'] );
 390+ $status = $this->backend->delete( $params );
 391+ if ( !$status->isOK() ) {
 392+ return $status; // also can't restore any dest file
 393+ }
 394+ // Restore any file that was at the destination
 395+ $status = $this->restoreDest();
 396+ return $status;
 397+ }
 398+
 399+ function doFinish() {
 400+ return Status::newGood();
 401+ }
 402+
 403+ function storagePathsToLock() {
 404+ return array_merge( $this->params['sources'], $this->params['dest'] );
 405+ }
 406+}
 407+
 408+/**
 409+ * Delete a file at the storage path.
 410+ * Parameters must match FileBackend::delete(), which include:
 411+ * source : source storage path
 412+ * ignoreMissingSource : don't return an error if the file does not exist
 413+ */
 414+class FileDeleteOp extends FileOp {
 415+ function doAttempt() {
 416+ $status = Status::newGood();
 417+ if ( !$this->params['ignoreMissingSource'] ) {
 418+ if ( !$this->backend->fileExists( $this->params['source'] ) ) {
 419+ $status->fatal( 'backend-fail-notexists', $this->params['source'] );
 420+ return $status;
 421+ }
 422+ }
 423+ return $status;
 424+ }
 425+
 426+ function doRevert() {
 427+ return Status::newGood();
 428+ }
 429+
 430+ function doFinish() {
 431+ // Delete the source file
 432+ $status = $this->fileBackend->delete( $this->params );
 433+ return $status;
 434+ }
 435+
 436+ function storagePathsToLock() {
 437+ return array( $this->params['source'] );
 438+ }
 439+}
Property changes on: branches/FileBackend/phase3/includes/filerepo/backend/FileOp.php
___________________________________________________________________
Added: svn:eol-style
1440 + native
Index: branches/FileBackend/phase3/includes/filerepo/backend/FSFileBackend.php
@@ -55,7 +55,10 @@
5656 return $status;
5757 }
5858 } else {
59 - wfMkdirParents( $dest );
 59+ if ( !wfMkdirParents( $dest ) ) {
 60+ $status->fatal( 'directorycreateerror', $param['dest'] );
 61+ return $status;
 62+ }
6063 }
6164
6265 wfSuppressWarnings();
@@ -113,7 +116,10 @@
114117 return $status;
115118 }
116119 } else {
117 - wfMkdirParents( $dest );
 120+ if ( !wfMkdirParents( $dest ) ) {
 121+ $status->fatal( 'directorycreateerror', $param['dest'] );
 122+ return $status;
 123+ }
118124 }
119125
120126 wfSuppressWarnings();
@@ -234,7 +240,10 @@
235241 }
236242 } else {
237243 // Make sure destination directory exists
238 - wfMkdirParents( $dest );
 244+ if ( !wfMkdirParents( $dest ) ) {
 245+ $status->fatal( 'directorycreateerror', $param['dest'] );
 246+ return $status;
 247+ }
239248 }
240249
241250 // Rename the temporary file to the destination path
@@ -251,6 +260,51 @@
252261 return $status;
253262 }
254263
 264+ function create( array $params ) {
 265+ $status = Status::newGood();
 266+
 267+ list( $c, $dest ) = $this->resolveVirtualPath( $params['dest'] );
 268+ if ( $dest === null ) {
 269+ $status->fatal( 'backend-fail-invalidpath', $params['dest'] );
 270+ return $status;
 271+ }
 272+
 273+ if ( file_exists( $dest ) ) {
 274+ if ( isset( $params['overwriteDest'] ) ) {
 275+ $ok = unlink( $dest );
 276+ if ( !$ok ) {
 277+ $status->fatal( 'backend-fail-delete', $param['dest'] );
 278+ return $status;
 279+ }
 280+ } elseif ( isset( $params['overwriteSame'] ) ) {
 281+ if ( !$this->fileAndDataAreSame( $dest, $params['content'] ) ) {
 282+ $status->fatal( 'backend-fail-notsame-raw', $params['dest'] );
 283+ }
 284+ return $status; // do nothing; either OK or bad status
 285+ } else {
 286+ $status->fatal( 'backend-fail-alreadyexists', $params['dest'] );
 287+ return $status;
 288+ }
 289+ } else {
 290+ if ( !wfMkdirParents( $dest ) ) {
 291+ $status->fatal( 'directorycreateerror', $param['dest'] );
 292+ return $status;
 293+ }
 294+ }
 295+
 296+ wfSuppressWarnings();
 297+ $ok = file_put_contents( $dest, $params['content'] );
 298+ wfRestoreWarnings();
 299+ if ( !$ok ) {
 300+ $status->fatal( 'backend-fail-create', $params['dest'] );
 301+ return $status;
 302+ }
 303+
 304+ $this->chmod( $dest );
 305+
 306+ return $status;
 307+ }
 308+
255309 function fileExists( array $params ) {
256310 list( $c, $source ) = $this->resolveVirtualPath( $params['source'] );
257311 if ( $source === null ) {
@@ -335,6 +389,20 @@
336390 }
337391
338392 /**
 393+ * Check if a file has identical contents as a string
 394+ *
 395+ * @param $path string Absolute filesystem path
 396+ * @param $content string Raw file data
 397+ * @return bool
 398+ */
 399+ protected function fileAndDataAreSame( $path, $content ) {
 400+ return ( // check size first since it's faster
 401+ filesize( $path ) === strlen( $content ) &&
 402+ sha1_file( $path ) === sha1( $content )
 403+ );
 404+ }
 405+
 406+ /**
339407 * Chmod a file, suppressing the warnings
340408 *
341409 * @param $path string Absolute file system path
Index: branches/FileBackend/phase3/includes/filerepo/backend/FileLockManager.php
@@ -235,10 +235,10 @@
236236 /**
237237 * Construct a new instance from configuration.
238238 * $config paramaters include:
239 - * 'serverMap' : Array of no more than 16 consecutive integer keys,
240 - * starting from 0, with a list of servers as values.
241 - * The first server in each list is the main server and
242 - * the others are fallback servers.
 239+ * 'serverMap' : Array of no more than 16 consecutive integer keys,
 240+ * starting from 0, with a list of servers as values.
 241+ * The first server in each list is the main server and
 242+ * the others are fallback servers.
243243 *
244244 * @param Array $config
245245 */
Index: branches/FileBackend/phase3/includes/filerepo/backend/FileBackend.php
@@ -70,6 +70,18 @@
7171 abstract public function doOperations( array $ops );
7272
7373 /**
 74+ * Prepare a storage path for usage. This will create containers
 75+ * that don't yet exists or, on FS backends, create parent directories.
 76+ * Do not call this function from places outside FileBackend and FileOp.
 77+ * $params include:
 78+ * directory : destination storage path
 79+ *
 80+ * @param Array $params
 81+ * @return Status
 82+ */
 83+ abstract public function prepare( array $params );
 84+
 85+ /**
7486 * Store a file into the backend from a file on disk.
7587 * Do not call this function from places outside FileBackend and FileOp.
7688 * $params include:
@@ -139,6 +151,20 @@
140152 abstract public function concatenate( array $params );
141153
142154 /**
 155+ * Create a file in the backend with the given contents.
 156+ * Do not call this function from places outside FileBackend and FileOp.
 157+ * $params include:
 158+ * contents : the raw file contents
 159+ * dest : destination storage path
 160+ * overwriteDest : do nothing and pass if an identical file exists at destination
 161+ * overwriteSame : override any existing file at destination
 162+ *
 163+ * @param Array $params
 164+ * @return Status
 165+ */
 166+ abstract public function create( array $params );
 167+
 168+ /**
143169 * Whether this backend implements move() and is applies to a potential
144170 * move from one storage path to another. No backends hits are required.
145171 * For example, moving objects accross containers may not be supported.
@@ -256,7 +282,8 @@
257283 'copy' => 'FileCopyOp',
258284 'move' => 'FileMoveOp',
259285 'delete' => 'FileDeleteOp',
260 - 'concatenate' => 'FileConcatenateOp'
 286+ 'concatenate' => 'FileConcatenateOp',
 287+ 'create' => 'FileCreateOp'
261288 );
262289 }
263290
@@ -374,395 +401,3 @@
375402 return $relStoragePath;
376403 }
377404 }
378 -
379 -/**
380 - * Helper class for representing operations with transaction support.
381 - * FileBackend::doOperations() will require these classes for supported operations.
382 - *
383 - * Access use of large fields should be avoided as we want to be able to support
384 - * potentially many FileOp classes in large arrays in memory.
385 - */
386 -abstract class FileOp {
387 - /** $var Array */
388 - protected $params;
389 - /** $var FileBackend */
390 - protected $backend;
391 -
392 - protected $state;
393 - protected $failedAttempt;
394 -
395 - const STATE_NEW = 1;
396 - const STATE_ATTEMPTED = 2;
397 - const STATE_DONE = 3;
398 -
399 - /**
400 - * Build a new file operation transaction
401 - *
402 - * @params $backend FileBackend
403 - * @params $params Array
404 - */
405 - final public function __construct( FileBackend $backend, array $params ) {
406 - $this->backend = $backend;
407 - $this->params = $params;
408 - $this->state = self::STATE_NEW;
409 - $this->failedAttempt = false;
410 - $this->initialize();
411 - }
412 -
413 - /**
414 - * Attempt the operation; this must be reversible
415 - *
416 - * @return Status
417 - */
418 - final public function attempt() {
419 - if ( $this->state !== self::STATE_NEW ) {
420 - throw new MWException( "Cannot attempt operation called twice." );
421 - }
422 - $this->state = self::STATE_ATTEMPTED;
423 - $status = $this->doAttempt();
424 - if ( !$status->isOK() ) {
425 - $this->failedAttempt = true;
426 - }
427 - return $status;
428 - }
429 -
430 - /**
431 - * Revert the operation; affected files are restored
432 - *
433 - * @return Status
434 - */
435 - final public function revert() {
436 - if ( $this->state !== self::STATE_ATTEMPTED ) {
437 - throw new MWException( "Cannot rollback an unstarted or finished operation." );
438 - }
439 - $this->state = self::STATE_DONE;
440 - if ( $this->failedAttempt ) {
441 - $status = Status::newGood(); // nothing to revert
442 - } else {
443 - $status = $this->doRevert();
444 - }
445 - return $status;
446 - }
447 -
448 - /**
449 - * Finish the operation; this may be irreversible
450 - *
451 - * @return Status
452 - */
453 - final public function finish() {
454 - if ( $this->state !== self::STATE_ATTEMPTED ) {
455 - throw new MWException( "Cannot cleanup an unstarted or finished operation." );
456 - }
457 - $this->state = self::STATE_DONE;
458 - if ( $this->failedAttempt ) {
459 - $status = Status::newGood(); // nothing to revert
460 - } else {
461 - $status = $this->doFinish();
462 - }
463 - return $status;
464 - }
465 -
466 - /**
467 - * Get a list of storage paths to lock for this operation
468 - *
469 - * @return Array
470 - */
471 - public function storagePathsToLock() {
472 - return array();
473 - }
474 -
475 - /**
476 - * @return void
477 - */
478 - protected function initialize() {}
479 -
480 - /**
481 - * @return Status
482 - */
483 - abstract protected function doAttempt();
484 -
485 - /**
486 - * @return Status
487 - */
488 - abstract protected function doRevert();
489 -
490 - /**
491 - * @return Status
492 - */
493 - abstract protected function doFinish();
494 -}
495 -
496 -/**
497 - * Store a file into the backend from a file on disk.
498 - * Parameters must match FileBackend::store(), which include:
499 - * source : source path on disk
500 - * dest : destination storage path
501 - * overwriteDest : do nothing and pass if an identical file exists at destination
502 - * overwriteSame : override any existing file at destination
503 - */
504 -class FileStoreOp extends FileOp {
505 - /** @var TempLocalFile|null */
506 - protected $tmpDestFile; // temp copy of existing destination file
507 -
508 - function doAttempt() {
509 - // Create a backup copy of any file that exists at destination
510 - $status = $this->backupDest();
511 - if ( !$status->isOK() ) {
512 - return $status;
513 - }
514 - // Store the file at the destination
515 - $status = $this->backend->store( $this->params );
516 - return $status;
517 - }
518 -
519 - function doRevert() {
520 - // Remove the file saved to the destination
521 - $params = array( 'source' => $this->params['dest'] );
522 - $status = $this->backend->delete( $params );
523 - if ( !$status->isOK() ) {
524 - return $status; // also can't restore any dest file
525 - }
526 - // Restore any file that was at the destination
527 - $status = $this->restoreDest();
528 - return $status;
529 - }
530 -
531 - function doFinish() {
532 - return Status::newGood();
533 - }
534 -
535 - function storagePathsToLock() {
536 - return array( $this->params['dest'] );
537 - }
538 -
539 - /**
540 - * Backup any file at destination to a temporary file.
541 - * Don't bother backing it up unless we might overwrite the file.
542 - *
543 - * @return Status
544 - */
545 - protected function backupDest() {
546 - $status = Status::newGood();
547 - // Check if a file already exists at the destination...
548 - if ( $this->backend->fileExists( $this->params['dest'] ) ) {
549 - if ( $this->params['overwriteDest'] ) {
550 - // Create a temporary backup copy...
551 - $this->tmpDestFile = $this->getLocalCopy( $this->params['dest'] );
552 - if ( !$this->tmpDestFile ) {
553 - $status->fatal( 'backend-fail-restore', $this->params['dest'] );
554 - return $status;
555 - }
556 - }
557 - }
558 - return $status;
559 - }
560 -
561 - /**
562 - * Restore any temporary destination backup file
563 - *
564 - * @return Status
565 - */
566 - protected function restoreDest() {
567 - $status = Status::newGood();
568 - // Restore any file that was at the destination
569 - if ( $this->tmpDestFile ) {
570 - $params = array(
571 - 'source' => $this->tmpDestFile->getPath(),
572 - 'dest' => $this->params['dest']
573 - );
574 - $status = $this->backend->store( $params );
575 - if ( !$status->isOK() ) {
576 - return $status;
577 - }
578 - }
579 - return $status;
580 - }
581 -}
582 -
583 -/**
584 - * Copy a file from one storage path to another in the backend.
585 - * Parameters must match FileBackend::copy(), which include:
586 - * source : source storage path
587 - * dest : destination storage path
588 - * overwriteDest : do nothing and pass if an identical file exists at destination
589 - * overwriteSame : override any existing file at destination
590 - */
591 -class FileCopyOp extends FileStoreOp {
592 - function doAttempt() {
593 - // Create a backup copy of any file that exists at destination
594 - $status = $this->backupDest();
595 - if ( !$status->isOK() ) {
596 - return $status;
597 - }
598 - // Copy the file into the destination
599 - $status = $this->backend->copy( $this->params );
600 - return $status;
601 - }
602 -
603 - function doRevert() {
604 - // Remove the file saved to the destination
605 - $params = array( 'source' => $this->params['dest'] );
606 - $status = $this->backend->delete( $params );
607 - if ( !$status->isOK() ) {
608 - return $status; // also can't restore any dest file
609 - }
610 - // Restore any file that was at the destination
611 - $status = $this->restoreDest();
612 - return $status;
613 - }
614 -
615 - function doFinish() {
616 - return Status::newGood();
617 - }
618 -
619 - function storagePathsToLock() {
620 - return array( $this->params['source'], $this->params['dest'] );
621 - }
622 -}
623 -
624 -/**
625 - * Move a file from one storage path to another in the backend.
626 - * Parameters must match FileBackend::move(), which include:
627 - * source : source storage path
628 - * dest : destination storage path
629 - * overwriteDest : do nothing and pass if an identical file exists at destination
630 - * overwriteSame : override any existing file at destination
631 - */
632 -class FileMoveOp extends FileStoreOp {
633 - protected $usingMove = false; // using backend move() function?
634 -
635 - function initialize() {
636 - // Use faster, native, move() if applicable
637 - $this->usingMove = $this->backend->canMove( $this->params );
638 - }
639 -
640 - function doAttempt() {
641 - // Create a backup copy of any file that exists at destination
642 - $status = $this->backupDest();
643 - if ( !$status->isOK() ) {
644 - return $status;
645 - }
646 - // Native moves: move the file into the destination
647 - if ( $this->usingMove ) {
648 - $status = $this->backend->move( $this->params );
649 - // Non-native moves: copy the file into the destination
650 - } else {
651 - $status = $this->backend->copy( $this->params );
652 - }
653 - return $status;
654 - }
655 -
656 - function doRevert() {
657 - // Native moves: move the file back to the source
658 - if ( $this->usingMove ) {
659 - $params = array(
660 - 'source' => $this->params['dest'],
661 - 'dest' => $this->params['source']
662 - );
663 - $status = $this->backend->move( $params );
664 - if ( !$status->isOK() ) {
665 - return $status; // also can't restore any dest file
666 - }
667 - // Non-native moves: remove the file saved to the destination
668 - } else {
669 - $params = array( 'source' => $this->params['dest'] );
670 - $status = $this->backend->delete( $params );
671 - if ( !$status->isOK() ) {
672 - return $status; // also can't restore any dest file
673 - }
674 - }
675 - // Restore any file that was at the destination
676 - $status = $this->restoreDest();
677 - return $status;
678 - }
679 -
680 - function doFinish() {
681 - // Native moves: nothing is at the source anymore
682 - if ( $this->usingMove ) {
683 - $status = Status::newGood();
684 - // Non-native moves: delete the source file
685 - } else {
686 - $params = array( 'source' => $this->params['source'] );
687 - $status = $this->backend->delete( $params );
688 - }
689 - return $status;
690 - }
691 -
692 - function storagePathsToLock() {
693 - return array( $this->params['source'], $this->params['dest'] );
694 - }
695 -}
696 -
697 -/**
698 - * Combines files from severals storage paths into a new file in the backend.
699 - * Parameters must match FileBackend::concatenate(), which include:
700 - * sources : ordered source storage paths (e.g. chunk1,chunk2,...)
701 - * dest : destination storage path
702 - * overwriteDest : do nothing and pass if an identical file exists at destination
703 - * overwriteSame : override any existing file at destination
704 - */
705 -class FileConcatenateOp extends FileStoreOp {
706 - function doAttempt() {
707 - // Create a backup copy of any file that exists at destination
708 - $status = $this->backupDest();
709 - if ( !$status->isOK() ) {
710 - return $status;
711 - }
712 - // Concatenate the file at the destination
713 - $status = $this->backend->concatenate( $this->params );
714 - return $status;
715 - }
716 -
717 - function doRevert() {
718 - // Remove the file saved to the destination
719 - $params = array( 'source' => $this->params['dest'] );
720 - $status = $this->backend->delete( $params );
721 - if ( !$status->isOK() ) {
722 - return $status; // also can't restore any dest file
723 - }
724 - // Restore any file that was at the destination
725 - $status = $this->restoreDest();
726 - return $status;
727 - }
728 -
729 - function doFinish() {
730 - return Status::newGood();
731 - }
732 -
733 - function storagePathsToLock() {
734 - return array_merge( $this->params['sources'], $this->params['dest'] );
735 - }
736 -}
737 -
738 -/**
739 - * Delete a file at the storage path.
740 - * Parameters must match FileBackend::delete(), which include:
741 - * source : source storage path
742 - * ignoreMissingSource : don't return an error if the file does not exist
743 - */
744 -class FileDeleteOp extends FileOp {
745 - function doAttempt() {
746 - $status = Status::newGood();
747 - if ( !$this->params['ignoreMissingSource'] ) {
748 - if ( !$this->backend->fileExists( $this->params['source'] ) ) {
749 - $status->fatal( 'backend-fail-notexists', $this->params['source'] );
750 - return $status;
751 - }
752 - }
753 - return $status;
754 - }
755 -
756 - function doRevert() {
757 - return Status::newGood();
758 - }
759 -
760 - function doFinish() {
761 - // Delete the source file
762 - $status = $this->fileBackend->delete( $this->params );
763 - return $status;
764 - }
765 -
766 - function storagePathsToLock() {
767 - return array( $this->params['source'] );
768 - }
769 -}
Index: branches/FileBackend/phase3/includes/filerepo/FileRepo.php
@@ -12,13 +12,19 @@
1313 *
1414 * @ingroup FileRepo
1515 */
16 -abstract class FileRepo {
 16+class FileRepo {
1717 const FILES_ONLY = 1;
1818 const DELETE_SOURCE = 1;
1919 const OVERWRITE = 2;
2020 const OVERWRITE_SAME = 4;
2121 const SKIP_VALIDATION = 8;
2222
 23+ /** @var FileBackend */
 24+ protected $backend;
 25+ /** @var Array Map of zones to config */
 26+ protected $zones;
 27+
 28+ protected $wikiKey; // unique wiki identifier
2329 var $thumbScriptUrl, $transformVia404;
2430 var $descBaseUrl, $scriptDirUrl, $scriptExtension, $articleUrl;
2531 var $fetchDescription, $initialCapital;
@@ -35,22 +41,136 @@
3642 function __construct( $info ) {
3743 // Required settings
3844 $this->name = $info['name'];
 45+ $this->url = $info['url'];
3946
40 - // Optional settings
41 - $this->initialCapital = MWNamespace::isCapitalized( NS_FILE );
42 - foreach ( array( 'descBaseUrl', 'scriptDirUrl', 'articleUrl', 'fetchDescription',
43 - 'thumbScriptUrl', 'initialCapital', 'pathDisclosureProtection',
44 - 'descriptionCacheExpiry', 'hashLevels', 'url', 'thumbUrl', 'scriptExtension' )
45 - as $var )
46 - {
 47+ // Optional settings that can have no value
 48+ $optionalSettings = array(
 49+ 'descBaseUrl', 'scriptDirUrl', 'articleUrl', 'fetchDescription',
 50+ 'thumbScriptUrl', 'pathDisclosureProtection', 'descriptionCacheExpiry',
 51+ 'scriptExtension'
 52+ );
 53+ foreach ( $optionalSettings as $var ) {
4754 if ( isset( $info[$var] ) ) {
4855 $this->$var = $info[$var];
4956 }
5057 }
 58+
 59+ // Optional settings that have a default
 60+ $this->initialCapital = isset( $info['initialCapital'] )
 61+ ? $info['initialCapital']
 62+ : MWNamespace::isCapitalized( NS_FILE );
 63+ $this->wikiKey = isset( $info['wikiKey'] )
 64+ ? $info['wikiKey']
 65+ : wfWikiID();
 66+ $this->thumbUrl = isset( $info['thumbUrl'] )
 67+ ? $info['thumbUrl']
 68+ : "{$this->url}/thumb";
 69+ $this->hashLevels = isset( $info['hashLevels'] )
 70+ ? $info['hashLevels']
 71+ : 2;
 72+ $this->deletedHashLevels = isset( $info['deletedHashLevels'] )
 73+ ? $info['deletedHashLevels']
 74+ : $this->hashLevels;
5175 $this->transformVia404 = !empty( $info['transformVia404'] );
 76+
 77+ // New backend config style
 78+ if ( isset( $info['backend'] ) ) {
 79+ $this->backend = $info['backend'];
 80+ $this->zones = $info['zones'];
 81+ $prefix = strlen( $this->wikiKey ) ? "{$this->wikiKey}-" : "";
 82+ // Give defaults for basic zones...
 83+ foreach ( array( 'public', 'thumb', 'archive', 'temp' ) as $zone ) {
 84+ if ( !isset( $this->zones[$zone] ) ) {
 85+ $this->zones[$zone] = array(
 86+ 'container' => "{$prefix}{$zone}",
 87+ 'directory' => '' // container root
 88+ );
 89+ }
 90+ }
 91+ // Old fashioned backend (FS) config
 92+ } else {
 93+ $this->initLegacyConfig( $info );
 94+ }
5295 }
5396
5497 /**
 98+ * Handle backend and zone settings for old config style
 99+ *
 100+ * @param $info Array
 101+ * @return void
 102+ */
 103+ protected function initLegacyConfig( $info ) {
 104+ // Local vars that used to be FSRepo members...
 105+ $directory = $info['directory'];
 106+ $deletedDir = isset( $info['deletedDir'] )
 107+ ? $info['deletedDir']
 108+ : false;
 109+ if ( isset( $info['thumbDir'] ) ) {
 110+ $thumbDir = $info['thumbDir'];
 111+ } else {
 112+ $thumbDir = "{$directory}/thumb";
 113+ }
 114+ $fileMode = isset( $info['fileMode'] )
 115+ ? $info['fileMode']
 116+ : 0644;
 117+
 118+ // Get the FS backend from configuration...
 119+ $prefix = strlen( $this->wikiKey ) ? "{$this->wikiKey}-" : "";
 120+ $config = array(
 121+ 'name' => "{$this->name}-backend",
 122+ 'lockManager' => new FSFileLockManager(
 123+ array( 'lockDir' => "{$directory}/locks" )
 124+ ),
 125+ 'containerPaths' => array(
 126+ "{$prefix}public" => "{$directory}",
 127+ "{$prefix}temp" => "{$directory}/temp",
 128+ "{$prefix}deleted" => $deletedDir,
 129+ "{$prefix}thumb" => $thumbDir
 130+ ),
 131+ 'fileMode' => $fileMode,
 132+ );
 133+ $this->backend = new FSFileBackend( $config );
 134+ }
 135+
 136+ /**
 137+ * Prepare all the zones for usage
 138+ *
 139+ * @param $doZone string Only do a particular zone
 140+ * @return Status
 141+ */
 142+ protected function initZones( $doZone = '' ) {
 143+ $status = Status::newGood();
 144+ foreach( $this->zones as $zone => $info ) {
 145+ if ( $doZone && $zone !== $doZone ) {
 146+ continue;
 147+ }
 148+ $params = array( 'directory' => $this->getZonePath( $zone ) );
 149+ $status->merge( $this->backend->prepare( $params ) );
 150+ }
 151+ return $status;
 152+ }
 153+
 154+ /**
 155+ * Take all available measures to prevent web accessibility of new deleted
 156+ * directories, in case the user has not configured offline storage
 157+ * @return void
 158+ */
 159+ protected function initDeletedDir( $dir ) {
 160+ if ( !( $this->backend instanceof FSFileBackend ) ) {
 161+ return; // this is not an apache web dir
 162+ }
 163+ // Add a .htaccess file to the root of the deleted zone
 164+ $root = $this->getZonePath( 'deleted' );
 165+ if ( !$this->backend->fileExists( array( 'source' => "$root/.htaccess" ) ) ) {
 166+ $params = array( 'content' => "Deny from all\n", 'dest' => "$root/.htaccess" );
 167+ $this->backend->create( $params );
 168+ }
 169+ // Seed new directories with a blank index.html, to prevent crawling
 170+ $params = array( 'content' => "$dir/index.html", 'dest' => '' );
 171+ $this->backend->create( $params );
 172+ }
 173+
 174+ /**
55175 * Determine if a string is an mwrepo:// URL
56176 *
57177 * @param $url string
@@ -62,6 +182,97 @@
63183 }
64184
65185 /**
 186+ * Get a URL referring to this repository, with the private mwrepo protocol.
 187+ * The suffix, if supplied, is considered to be unencoded, and will be
 188+ * URL-encoded before being returned.
 189+ *
 190+ * @param $suffix string
 191+ *
 192+ * @return string
 193+ */
 194+ function getVirtualUrl( $suffix = false ) {
 195+ $path = 'mwrepo://' . $this->name;
 196+ if ( $suffix !== false ) {
 197+ $path .= '/' . rawurlencode( $suffix );
 198+ }
 199+ return $path;
 200+ }
 201+
 202+ /**
 203+ * Get the URL corresponding to one of the four basic zones
 204+ * @param $zone String: one of: public, deleted, temp, thumb
 205+ * @return String or false
 206+ */
 207+ function getZoneUrl( $zone ) {
 208+ switch ( $zone ) {
 209+ case 'public':
 210+ return $this->url;
 211+ case 'temp':
 212+ return "{$this->url}/temp";
 213+ case 'deleted':
 214+ return false; // no public URL
 215+ case 'thumb':
 216+ return $this->thumbUrl;
 217+ default:
 218+ return false;
 219+ }
 220+ }
 221+
 222+ /**
 223+ * Get the backend storage path corresponding to a virtual URL
 224+ *
 225+ * @param $url string
 226+ *
 227+ * @return string
 228+ */
 229+ function resolveVirtualUrl( $url ) {
 230+ if ( substr( $url, 0, 9 ) != 'mwrepo://' ) {
 231+ throw new MWException( __METHOD__.': unknown protocol' );
 232+ }
 233+
 234+ $bits = explode( '/', substr( $url, 9 ), 3 );
 235+ if ( count( $bits ) != 3 ) {
 236+ throw new MWException( __METHOD__.": invalid mwrepo URL: $url" );
 237+ }
 238+ list( $repo, $zone, $rel ) = $bits;
 239+ if ( $repo !== $this->name ) {
 240+ throw new MWException( __METHOD__.": fetching from a foreign repo is not supported" );
 241+ }
 242+ $base = $this->getZonePath( $zone );
 243+ if ( !$base ) {
 244+ throw new MWException( __METHOD__.": invalid zone: $zone" );
 245+ }
 246+ return $base . '/' . rawurldecode( $rel );
 247+ }
 248+
 249+ /**
 250+ * The the storage container and base path of a zone
 251+ *
 252+ * @param $zone string
 253+ * @return Array (container, base path) or (null, null)
 254+ */
 255+ function getZoneLocation( $zone ) {
 256+ if ( !isset( $this->zones[$zone] ) ) {
 257+ return array( null, null ); // bogus
 258+ }
 259+ return array( $this->zones[$zone]['container'], $this->zones[$zone]['directory'] );
 260+ }
 261+
 262+ /**
 263+ * Get the storage path corresponding to one of the zones
 264+ *
 265+ * @param $zone string
 266+ * @return string|null
 267+ */
 268+ function getZonePath( $zone ) {
 269+ list( $container, $base ) = $this->getZoneLocation( $zone );
 270+ if ( $container === null || $base === null ) {
 271+ return null;
 272+ }
 273+ return "mwstore://{$backend}/{$container}/{$base}";
 274+ }
 275+
 276+ /**
66277 * Create a new File object from the local repository
67278 *
68279 * @param $title Mixed: Title object or string
@@ -217,22 +428,29 @@
218429 }
219430
220431 /**
221 - * Get the URL of thumb.php
 432+ * Get the public root URL of the repository
 433+ * @return string
222434 */
223 - function getThumbScriptUrl() {
224 - return $this->thumbScriptUrl;
 435+ function getRootUrl() {
 436+ return $this->url;
225437 }
226438
227439 /**
228 - * Get the URL corresponding to one of the four basic zones
229 - * @param $zone String: one of: public, deleted, temp, thumb
230 - * @return String or false
 440+ * Returns true if the repository uses a multi-level directory structure
 441+ * @return string
231442 */
232 - function getZoneUrl( $zone ) {
233 - return false;
 443+ function isHashed() {
 444+ return (bool)$this->hashLevels;
234445 }
235446
236447 /**
 448+ * Get the URL of thumb.php
 449+ */
 450+ function getThumbScriptUrl() {
 451+ return $this->thumbScriptUrl;
 452+ }
 453+
 454+ /**
237455 * Returns true if the repository can transform files via a 404 handler
238456 *
239457 * @return bool
@@ -404,11 +622,131 @@
405623 /**
406624 * Store a batch of files
407625 *
408 - * @param $triplets Array: (src,zone,dest) triplets as per store()
 626+ * @param $triplets Array: (src,zone,dest rel) triplets as per store()
409627 * @param $flags Integer: flags as per store
 628+ * @return Status
410629 */
411 - abstract function storeBatch( $triplets, $flags = 0 );
 630+ function storeBatch( $triplets, $flags = 0 ) {
 631+ $backend = $this->backend; // convenience
412632
 633+ wfDebug( __METHOD__ . ': Storing ' . count( $triplets ) .
 634+ " triplets; flags: {$flags}\n" );
 635+
 636+ $status = $this->newGood();
 637+ // Try creating directories
 638+ $status->merge( $this->initZones() );
 639+ if ( !$status->isOK() ) {
 640+ return $status;
 641+ }
 642+
 643+ // Validate each triplet
 644+ $status = $this->newGood();
 645+ foreach ( $triplets as $i => $triplet ) {
 646+ list( $srcPath, $dstZone, $dstRel ) = $triplet;
 647+
 648+ // Resolve destination path
 649+ $root = $this->getZonePath( $dstZone );
 650+ if ( !$root ) {
 651+ throw new MWException( "Invalid zone: $dstZone" );
 652+ }
 653+ if ( !$this->validateFilename( $dstRel ) ) {
 654+ throw new MWException( 'Validation error in $dstRel' );
 655+ }
 656+ $dstPath = "$root/$dstRel";
 657+ $dstDir = dirname( $dstPath );
 658+
 659+ // Create destination directories for this triplet
 660+ $status->merge( $backend->prepare( array( 'directory' => $dstDir ) ) );
 661+ if ( !$status->isOK() ) {
 662+ return $status;
 663+ }
 664+
 665+ if ( $dstZone == 'deleted' ) {
 666+ $this->initDeletedDir( $dstDir );
 667+ }
 668+
 669+ // Resolve source
 670+ if ( self::isVirtualUrl( $srcPath ) ) {
 671+ $srcPath = $triplets[$i][0] = $this->resolveVirtualUrl( $srcPath );
 672+ }
 673+ if ( !$backend->fileExists( array( 'source' => $srcPath ) ) ) {
 674+ // Make a list of files that don't exist for return to the caller
 675+ $status->fatal( 'filenotfound', $srcPath );
 676+ continue;
 677+ }
 678+
 679+ // Check overwriting
 680+ if ( !( $flags & self::OVERWRITE ) &&
 681+ $backend->fileExists( array( 'source' => $dstPath ) ) )
 682+ {
 683+ if ( $flags & self::OVERWRITE_SAME ) {
 684+ $hashSource = sha1_file( $srcPath );
 685+ $hashDest = sha1_file( $dstPath );
 686+ if ( $hashSource != $hashDest ) {
 687+ $status->fatal( 'fileexistserror', $dstPath );
 688+ $status->failCount++;
 689+ }
 690+ } else {
 691+ $status->fatal( 'fileexistserror', $dstPath );
 692+ $status->failCount++;
 693+ }
 694+ }
 695+ }
 696+
 697+ // Windows does not support moving over existing files, so explicitly delete them
 698+ $deleteDest = wfIsWindows() && ( $flags & self::OVERWRITE );
 699+
 700+ // Abort now on failure
 701+ if ( !$status->ok ) {
 702+ return $status;
 703+ }
 704+
 705+ // Execute the store operation for each triplet
 706+ foreach ( $triplets as $i => $triplet ) {
 707+ list( $srcPath, $dstZone, $dstRel ) = $triplet;
 708+ $root = $this->getZonePath( $dstZone );
 709+ $dstPath = "$root/$dstRel";
 710+ $good = true;
 711+
 712+ if ( $flags & self::DELETE_SOURCE ) {
 713+ if ( $deleteDest ) {
 714+ unlink( $dstPath );
 715+ }
 716+ if ( !rename( $srcPath, $dstPath ) ) {
 717+ $status->error( 'filerenameerror', $srcPath, $dstPath );
 718+ $good = false;
 719+ }
 720+ } else {
 721+ if ( !copy( $srcPath, $dstPath ) ) {
 722+ $status->error( 'filecopyerror', $srcPath, $dstPath );
 723+ $good = false;
 724+ }
 725+ if ( !( $flags & self::SKIP_VALIDATION ) ) {
 726+ wfSuppressWarnings();
 727+ $hashSource = sha1_file( $srcPath );
 728+ $hashDest = sha1_file( $dstPath );
 729+ wfRestoreWarnings();
 730+
 731+ if ( $hashDest === false || $hashSource !== $hashDest ) {
 732+ wfDebug( __METHOD__ . ': File copy validation failed: ' .
 733+ "$srcPath ($hashSource) to $dstPath ($hashDest)\n" );
 734+
 735+ $status->error( 'filecopyerror', $srcPath, $dstPath );
 736+ $good = false;
 737+ }
 738+ }
 739+ }
 740+ if ( $good ) {
 741+ $this->chmod( $dstPath );
 742+ $status->successCount++;
 743+ } else {
 744+ $status->failCount++;
 745+ }
 746+ $status->success[$i] = $good;
 747+ }
 748+ return $status;
 749+ }
 750+
413751 /**
414752 * Pick a random name in the temp zone and store a file to it.
415753 * Returns a FileRepoStatus object with the URL in the value.

Status & tagging log