MediaWiki  master
LocalFileMoveBatch.php
Go to the documentation of this file.
1 <?php
26 use Psr\Log\LoggerInterface;
28 use Wikimedia\ScopedCallback;
29 
36  protected $file;
37 
39  protected $target;
40 
41  protected $cur;
42 
43  protected $olds;
44 
45  protected $oldCount;
46 
47  protected $archive;
48 
50  protected $db;
51 
53  protected $oldHash;
54 
56  protected $newHash;
57 
59  protected $oldName;
60 
62  protected $newName;
63 
65  protected $oldRel;
66 
68  protected $newRel;
69 
71  private $logger;
72 
74  private $haveSourceLock = false;
75 
77  private $haveTargetLock = false;
78 
80  private $targetFile;
81 
86  public function __construct( LocalFile $file, Title $target ) {
87  $this->file = $file;
88  $this->target = $target;
89  $this->oldHash = $this->file->repo->getHashPath( $this->file->getName() );
90  $this->newHash = $this->file->repo->getHashPath( $this->target->getDBkey() );
91  $this->oldName = $this->file->getName();
92  $this->newName = $this->file->repo->getNameFromTitle( $this->target );
93  $this->oldRel = $this->oldHash . $this->oldName;
94  $this->newRel = $this->newHash . $this->newName;
95  $this->db = $file->getRepo()->getPrimaryDB();
96 
97  $this->logger = LoggerFactory::getInstance( 'imagemove' );
98  }
99 
105  public function addCurrent() {
106  $status = $this->acquireSourceLock();
107  if ( $status->isOK() ) {
108  $this->cur = [ $this->oldRel, $this->newRel ];
109  }
110  return $status;
111  }
112 
117  public function addOlds() {
118  $archiveBase = 'archive';
119  $this->olds = [];
120  $this->oldCount = 0;
121  $archiveNames = [];
122 
123  $result = $this->db->select( 'oldimage',
124  [ 'oi_archive_name', 'oi_deleted' ],
125  [ 'oi_name' => $this->oldName ],
126  __METHOD__,
127  [ 'FOR UPDATE' ] // ignore snapshot
128  );
129 
130  foreach ( $result as $row ) {
131  $archiveNames[] = $row->oi_archive_name;
132  $oldName = $row->oi_archive_name;
133  $bits = explode( '!', $oldName, 2 );
134 
135  if ( count( $bits ) != 2 ) {
136  $this->logger->debug(
137  'Old file name missing !: {oldName}',
138  [ 'oldName' => $oldName ]
139  );
140  continue;
141  }
142 
143  list( $timestamp, $filename ) = $bits;
144 
145  if ( $this->oldName != $filename ) {
146  $this->logger->debug(
147  'Old file name does not match: {oldName}',
148  [ 'oldName' => $oldName ]
149  );
150  continue;
151  }
152 
153  $this->oldCount++;
154 
155  // Do we want to add those to oldCount?
156  if ( $row->oi_deleted & File::DELETED_FILE ) {
157  continue;
158  }
159 
160  $this->olds[] = [
161  "{$archiveBase}/{$this->oldHash}{$oldName}",
162  "{$archiveBase}/{$this->newHash}{$timestamp}!{$this->newName}"
163  ];
164  }
165 
166  return $archiveNames;
167  }
168 
174  protected function acquireSourceLock() {
175  if ( $this->haveSourceLock ) {
176  return Status::newGood();
177  }
178  $status = $this->file->acquireFileLock();
179  if ( $status->isOK() ) {
180  $this->haveSourceLock = true;
181  }
182  return $status;
183  }
184 
190  protected function acquireTargetLock() {
191  if ( $this->haveTargetLock ) {
192  return Status::newGood();
193  }
194  $status = $this->getTargetFile()->acquireFileLock();
195  if ( $status->isOK() ) {
196  $this->haveTargetLock = true;
197  }
198  return $status;
199  }
200 
204  protected function releaseLocks() {
205  if ( $this->haveSourceLock ) {
206  $this->file->releaseFileLock();
207  $this->haveSourceLock = false;
208  }
209  if ( $this->haveTargetLock ) {
210  $this->getTargetFile()->releaseFileLock();
211  $this->haveTargetLock = false;
212  }
213  }
214 
220  protected function getTargetFile() {
221  if ( $this->targetFile === null ) {
222  $this->targetFile = MediaWikiServices::getInstance()->getRepoGroup()->getLocalRepo()
223  ->newFile( $this->target );
224  }
225  return $this->targetFile;
226  }
227 
232  public function execute() {
233  $repo = $this->file->repo;
234  $status = $repo->newGood();
235 
236  $status->merge( $this->acquireSourceLock() );
237  if ( !$status->isOK() ) {
238  return $status;
239  }
240  $status->merge( $this->acquireTargetLock() );
241  if ( !$status->isOK() ) {
242  $this->releaseLocks();
243  return $status;
244  }
245  $unlockScope = new ScopedCallback( function () {
246  $this->releaseLocks();
247  } );
248 
249  $triplets = $this->getMoveTriplets();
250  $checkStatus = $this->removeNonexistentFiles( $triplets );
251  if ( !$checkStatus->isGood() ) {
252  $status->merge( $checkStatus ); // couldn't talk to file backend
253  return $status;
254  }
255  $triplets = $checkStatus->value;
256 
257  // Verify the file versions metadata in the DB.
258  $statusDb = $this->verifyDBUpdates();
259  if ( !$statusDb->isGood() ) {
260  $statusDb->setOK( false );
261 
262  return $statusDb;
263  }
264 
265  if ( !$repo->hasSha1Storage() ) {
266  // Copy the files into their new location.
267  // If a prior process fataled copying or cleaning up files we tolerate any
268  // of the existing files if they are identical to the ones being stored.
269  $statusMove = $repo->storeBatch( $triplets, FileRepo::OVERWRITE_SAME );
270 
271  $this->logger->debug(
272  'Moved files for {fileName}: {successCount} successes, {failCount} failures',
273  [
274  'fileName' => $this->file->getName(),
275  'successCount' => $statusMove->successCount,
276  'failCount' => $statusMove->failCount,
277  ]
278  );
279 
280  if ( !$statusMove->isGood() ) {
281  // Delete any files copied over (while the destination is still locked)
282  $this->cleanupTarget( $triplets );
283 
284  $this->logger->debug(
285  'Error in moving files: {error}',
286  [ 'error' => $statusMove->getWikiText( false, false, 'en' ) ]
287  );
288 
289  $statusMove->setOK( false );
290 
291  return $statusMove;
292  }
293  $status->merge( $statusMove );
294  }
295 
296  // Rename the file versions metadata in the DB.
297  $this->doDBUpdates();
298 
299  $this->logger->debug(
300  'Renamed {fileName} in database: {successCount} successes, {failCount} failures',
301  [
302  'fileName' => $this->file->getName(),
303  'successCount' => $statusDb->successCount,
304  'failCount' => $statusDb->failCount,
305  ]
306  );
307 
308  // Everything went ok, remove the source files
309  $this->cleanupSource( $triplets );
310 
311  // Defer lock release until the transaction is committed.
312  if ( $this->db->trxLevel() ) {
313  $unlockScope->cancel();
314  $this->db->onTransactionResolution( function () {
315  $this->releaseLocks();
316  } );
317  } else {
318  ScopedCallback::consume( $unlockScope );
319  }
320 
321  $status->merge( $statusDb );
322 
323  return $status;
324  }
325 
332  protected function verifyDBUpdates() {
333  $repo = $this->file->repo;
334  $status = $repo->newGood();
335  $dbw = $this->db;
336 
337  $hasCurrent = $dbw->lockForUpdate(
338  'image',
339  [ 'img_name' => $this->oldName ],
340  __METHOD__
341  );
342  $oldRowCount = $dbw->lockForUpdate(
343  'oldimage',
344  [ 'oi_name' => $this->oldName ],
345  __METHOD__
346  );
347 
348  if ( $hasCurrent ) {
349  $status->successCount++;
350  } else {
351  $status->failCount++;
352  }
353  $status->successCount += $oldRowCount;
354  // T36934: oldCount is based on files that actually exist.
355  // There may be more DB rows than such files, in which case $affected
356  // can be greater than $total. We use max() to avoid negatives here.
357  $status->failCount += max( 0, $this->oldCount - $oldRowCount );
358  if ( $status->failCount ) {
359  $status->error( 'imageinvalidfilename' );
360  }
361 
362  return $status;
363  }
364 
369  protected function doDBUpdates() {
370  $dbw = $this->db;
371 
372  // Update current image
373  $dbw->update(
374  'image',
375  [ 'img_name' => $this->newName ],
376  [ 'img_name' => $this->oldName ],
377  __METHOD__
378  );
379 
380  // Update old images
381  $dbw->update(
382  'oldimage',
383  [
384  'oi_name' => $this->newName,
385  'oi_archive_name = ' . $dbw->strreplace( 'oi_archive_name',
386  $dbw->addQuotes( $this->oldName ), $dbw->addQuotes( $this->newName ) ),
387  ],
388  [ 'oi_name' => $this->oldName ],
389  __METHOD__
390  );
391  }
392 
397  protected function getMoveTriplets() {
398  $moves = array_merge( [ $this->cur ], $this->olds );
399  $triplets = []; // The format is: (srcUrl, destZone, destUrl)
400 
401  foreach ( $moves as $move ) {
402  // $move: (oldRelativePath, newRelativePath)
403  $srcUrl = $this->file->repo->getVirtualUrl() . '/public/' . rawurlencode( $move[0] );
404  $triplets[] = [ $srcUrl, 'public', $move[1] ];
405 
406  $this->logger->debug(
407  'Generated move triplet for {fileName}: {srcUrl} :: public :: {move1}',
408  [
409  'fileName' => $this->file->getName(),
410  'srcUrl' => $srcUrl,
411  'move1' => $move[1],
412  ]
413  );
414  }
415 
416  return $triplets;
417  }
418 
424  protected function removeNonexistentFiles( $triplets ) {
425  $files = [];
426 
427  foreach ( $triplets as $file ) {
428  $files[$file[0]] = $file[0];
429  }
430 
431  $result = $this->file->repo->fileExistsBatch( $files );
432  if ( in_array( null, $result, true ) ) {
433  return Status::newFatal( 'backend-fail-internal',
434  $this->file->repo->getBackend()->getName() );
435  }
436 
437  $filteredTriplets = [];
438  foreach ( $triplets as $file ) {
439  if ( $result[$file[0]] ) {
440  $filteredTriplets[] = $file;
441  } else {
442  $this->logger->debug(
443  'File {file} does not exist',
444  [ 'file' => $file[0] ]
445  );
446  }
447  }
448 
449  return Status::newGood( $filteredTriplets );
450  }
451 
457  protected function cleanupTarget( $triplets ) {
458  // Create dest pairs from the triplets
459  $pairs = [];
460  foreach ( $triplets as $triplet ) {
461  // $triplet: (old source virtual URL, dst zone, dest rel)
462  $pairs[] = [ $triplet[1], $triplet[2] ];
463  }
464 
465  $this->file->repo->cleanupBatch( $pairs );
466  }
467 
473  protected function cleanupSource( $triplets ) {
474  // Create source file names from the triplets
475  $files = [];
476  foreach ( $triplets as $triplet ) {
477  $files[] = $triplet[0];
478  }
479 
480  $this->file->repo->cleanupBatch( $files );
481  }
482 }
LocalFileMoveBatch\$target
Title $target
Definition: LocalFileMoveBatch.php:39
StatusValue\newFatal
static newFatal( $message,... $parameters)
Factory function for fatal errors.
Definition: StatusValue.php:70
LocalFileMoveBatch\$oldRel
string $oldRel
Definition: LocalFileMoveBatch.php:65
LocalFileMoveBatch\$file
LocalFile $file
Definition: LocalFileMoveBatch.php:36
FileRepo\OVERWRITE_SAME
const OVERWRITE_SAME
Definition: FileRepo.php:48
LocalFile\getRepo
getRepo()
Definition: LocalFile.php:330
MediaWiki\MediaWikiServices
MediaWikiServices is the service locator for the application scope of MediaWiki.
Definition: MediaWikiServices.php:203
LocalFileMoveBatch\cleanupSource
cleanupSource( $triplets)
Cleanup a fully moved array of triplets by deleting the source files.
Definition: LocalFileMoveBatch.php:473
LocalFileMoveBatch\verifyDBUpdates
verifyDBUpdates()
Verify the database updates and return a new Status indicating how many rows would be updated.
Definition: LocalFileMoveBatch.php:332
LocalFileMoveBatch\$haveSourceLock
bool $haveSourceLock
Definition: LocalFileMoveBatch.php:74
LocalFileMoveBatch\$oldHash
string $oldHash
Definition: LocalFileMoveBatch.php:53
LocalFileMoveBatch\$newName
string $newName
Definition: LocalFileMoveBatch.php:62
Wikimedia\Rdbms\IDatabase
Basic database interface for live and lazy-loaded relation database handles.
Definition: IDatabase.php:38
LocalFileMoveBatch\$oldCount
$oldCount
Definition: LocalFileMoveBatch.php:45
LocalFileMoveBatch\addOlds
addOlds()
Add the old versions of the image to the batch.
Definition: LocalFileMoveBatch.php:117
Wikimedia\Rdbms\IDatabase\update
update( $table, $set, $conds, $fname=__METHOD__, $options=[])
Update all rows in a table that match a given condition.
LocalFileMoveBatch\$haveTargetLock
bool $haveTargetLock
Definition: LocalFileMoveBatch.php:77
MediaWiki\Logger\LoggerFactory
PSR-3 logger instance factory.
Definition: LoggerFactory.php:45
Wikimedia\Rdbms\IDatabase\lockForUpdate
lockForUpdate( $table, $conds='', $fname=__METHOD__, $options=[], $join_conds=[])
Lock all rows meeting the given conditions/options FOR UPDATE.
LocalFileMoveBatch\$db
IDatabase $db
Definition: LocalFileMoveBatch.php:50
LocalFileMoveBatch\$logger
LoggerInterface $logger
Definition: LocalFileMoveBatch.php:71
LocalFile
Class to represent a local file in the wiki's own database.
Definition: LocalFile.php:63
LocalFileMoveBatch\$archive
$archive
Definition: LocalFileMoveBatch.php:47
LocalFileMoveBatch\$oldName
string $oldName
Definition: LocalFileMoveBatch.php:59
LocalFileMoveBatch\acquireSourceLock
acquireSourceLock()
Acquire the source file lock, if it has not been acquired already.
Definition: LocalFileMoveBatch.php:174
StatusValue\newGood
static newGood( $value=null)
Factory function for good results.
Definition: StatusValue.php:82
LocalFileMoveBatch\getMoveTriplets
getMoveTriplets()
Generate triplets for FileRepo::storeBatch().
Definition: LocalFileMoveBatch.php:397
LocalFileMoveBatch\acquireTargetLock
acquireTargetLock()
Acquire the target file lock, if it has not been acquired already.
Definition: LocalFileMoveBatch.php:190
Title
Represents a title within MediaWiki.
Definition: Title.php:47
LocalFileMoveBatch\$newHash
string $newHash
Definition: LocalFileMoveBatch.php:56
LocalFileMoveBatch\execute
execute()
Perform the move.
Definition: LocalFileMoveBatch.php:232
LocalFileMoveBatch\removeNonexistentFiles
removeNonexistentFiles( $triplets)
Removes non-existent files from move batch.
Definition: LocalFileMoveBatch.php:424
LocalFileMoveBatch\$newRel
string $newRel
Definition: LocalFileMoveBatch.php:68
LocalFileMoveBatch
Helper class for file movement.
Definition: LocalFileMoveBatch.php:34
LocalFileMoveBatch\getTargetFile
getTargetFile()
Get the target file.
Definition: LocalFileMoveBatch.php:220
File\DELETED_FILE
const DELETED_FILE
Definition: File.php:71
LocalFileMoveBatch\addCurrent
addCurrent()
Add the current image to the batch.
Definition: LocalFileMoveBatch.php:105
LocalFileMoveBatch\doDBUpdates
doDBUpdates()
Do the database updates and return a new Status indicating how many rows where updated.
Definition: LocalFileMoveBatch.php:369
LocalFileMoveBatch\__construct
__construct(LocalFile $file, Title $target)
Definition: LocalFileMoveBatch.php:86
LocalFileMoveBatch\releaseLocks
releaseLocks()
Release both file locks.
Definition: LocalFileMoveBatch.php:204
LocalFileMoveBatch\$olds
$olds
Definition: LocalFileMoveBatch.php:43
LocalFileMoveBatch\$cur
$cur
Definition: LocalFileMoveBatch.php:41
LocalFileMoveBatch\cleanupTarget
cleanupTarget( $triplets)
Cleanup a partially moved array of triplets by deleting the target files.
Definition: LocalFileMoveBatch.php:457
LocalFileMoveBatch\$targetFile
LocalFile null $targetFile
Definition: LocalFileMoveBatch.php:80