MediaWiki  master
LocalFileMoveBatch.php
Go to the documentation of this file.
1 <?php
26 use Psr\Log\LoggerInterface;
28 
35  protected $file;
36 
38  protected $target;
39 
40  protected $cur;
41 
42  protected $olds;
43 
44  protected $oldCount;
45 
46  protected $archive;
47 
49  protected $db;
50 
52  protected $oldHash;
53 
55  protected $newHash;
56 
58  protected $oldName;
59 
61  protected $newName;
62 
64  protected $oldRel;
65 
67  protected $newRel;
68 
70  private $logger;
71 
76  public function __construct( LocalFile $file, Title $target ) {
77  $this->file = $file;
78  $this->target = $target;
79  $this->oldHash = $this->file->repo->getHashPath( $this->file->getName() );
80  $this->newHash = $this->file->repo->getHashPath( $this->target->getDBkey() );
81  $this->oldName = $this->file->getName();
82  $this->newName = $this->file->repo->getNameFromTitle( $this->target );
83  $this->oldRel = $this->oldHash . $this->oldName;
84  $this->newRel = $this->newHash . $this->newName;
85  $this->db = $file->getRepo()->getPrimaryDB();
86 
87  $this->logger = LoggerFactory::getInstance( 'imagemove' );
88  }
89 
93  public function addCurrent() {
94  $this->cur = [ $this->oldRel, $this->newRel ];
95  }
96 
101  public function addOlds() {
102  $archiveBase = 'archive';
103  $this->olds = [];
104  $this->oldCount = 0;
105  $archiveNames = [];
106 
107  $result = $this->db->select( 'oldimage',
108  [ 'oi_archive_name', 'oi_deleted' ],
109  [ 'oi_name' => $this->oldName ],
110  __METHOD__,
111  [ 'LOCK IN SHARE MODE' ] // ignore snapshot
112  );
113 
114  foreach ( $result as $row ) {
115  $archiveNames[] = $row->oi_archive_name;
116  $oldName = $row->oi_archive_name;
117  $bits = explode( '!', $oldName, 2 );
118 
119  if ( count( $bits ) != 2 ) {
120  $this->logger->debug(
121  'Old file name missing !: {oldName}',
122  [ 'oldName' => $oldName ]
123  );
124  continue;
125  }
126 
127  list( $timestamp, $filename ) = $bits;
128 
129  if ( $this->oldName != $filename ) {
130  $this->logger->debug(
131  'Old file name does not match: {oldName}',
132  [ 'oldName' => $oldName ]
133  );
134  continue;
135  }
136 
137  $this->oldCount++;
138 
139  // Do we want to add those to oldCount?
140  if ( $row->oi_deleted & File::DELETED_FILE ) {
141  continue;
142  }
143 
144  $this->olds[] = [
145  "{$archiveBase}/{$this->oldHash}{$oldName}",
146  "{$archiveBase}/{$this->newHash}{$timestamp}!{$this->newName}"
147  ];
148  }
149 
150  return $archiveNames;
151  }
152 
157  public function execute() {
158  $repo = $this->file->repo;
159  $status = $repo->newGood();
160  $destFile = MediaWikiServices::getInstance()->getRepoGroup()->getLocalRepo()
161  ->newFile( $this->target );
162 
163  $this->file->lock();
164  $destFile->lock(); // quickly fail if destination is not available
165 
166  $triplets = $this->getMoveTriplets();
167  $checkStatus = $this->removeNonexistentFiles( $triplets );
168  if ( !$checkStatus->isGood() ) {
169  $destFile->unlock();
170  $this->file->unlock();
171  $status->merge( $checkStatus ); // couldn't talk to file backend
172  return $status;
173  }
174  $triplets = $checkStatus->value;
175 
176  // Verify the file versions metadata in the DB.
177  $statusDb = $this->verifyDBUpdates();
178  if ( !$statusDb->isGood() ) {
179  $destFile->unlock();
180  $this->file->unlock();
181  $statusDb->setOK( false );
182 
183  return $statusDb;
184  }
185 
186  if ( !$repo->hasSha1Storage() ) {
187  // Copy the files into their new location.
188  // If a prior process fataled copying or cleaning up files we tolerate any
189  // of the existing files if they are identical to the ones being stored.
190  $statusMove = $repo->storeBatch( $triplets, FileRepo::OVERWRITE_SAME );
191 
192  $this->logger->debug(
193  'Moved files for {fileName}: {successCount} successes, {failCount} failures',
194  [
195  'fileName' => $this->file->getName(),
196  'successCount' => $statusMove->successCount,
197  'failCount' => $statusMove->failCount,
198  ]
199  );
200 
201  if ( !$statusMove->isGood() ) {
202  // Delete any files copied over (while the destination is still locked)
203  $this->cleanupTarget( $triplets );
204  $destFile->unlock();
205  $this->file->unlock();
206 
207  $this->logger->debug(
208  'Error in moving files: {error}',
209  [ 'error' => $statusMove->getWikiText( false, false, 'en' ) ]
210  );
211 
212  $statusMove->setOK( false );
213 
214  return $statusMove;
215  }
216  $status->merge( $statusMove );
217  }
218 
219  // Rename the file versions metadata in the DB.
220  $this->doDBUpdates();
221 
222  $this->logger->debug(
223  'Renamed {fileName} in database: {successCount} successes, {failCount} failures',
224  [
225  'fileName' => $this->file->getName(),
226  'successCount' => $statusDb->successCount,
227  'failCount' => $statusDb->failCount,
228  ]
229  );
230 
231  $destFile->unlock();
232  $this->file->unlock();
233 
234  // Everything went ok, remove the source files
235  $this->cleanupSource( $triplets );
236 
237  $status->merge( $statusDb );
238 
239  return $status;
240  }
241 
248  protected function verifyDBUpdates() {
249  $repo = $this->file->repo;
250  $status = $repo->newGood();
251  $dbw = $this->db;
252 
253  $hasCurrent = $dbw->lockForUpdate(
254  'image',
255  [ 'img_name' => $this->oldName ],
256  __METHOD__
257  );
258  $oldRowCount = $dbw->lockForUpdate(
259  'oldimage',
260  [ 'oi_name' => $this->oldName ],
261  __METHOD__
262  );
263 
264  if ( $hasCurrent ) {
265  $status->successCount++;
266  } else {
267  $status->failCount++;
268  }
269  $status->successCount += $oldRowCount;
270  // T36934: oldCount is based on files that actually exist.
271  // There may be more DB rows than such files, in which case $affected
272  // can be greater than $total. We use max() to avoid negatives here.
273  $status->failCount += max( 0, $this->oldCount - $oldRowCount );
274  if ( $status->failCount ) {
275  $status->error( 'imageinvalidfilename' );
276  }
277 
278  return $status;
279  }
280 
285  protected function doDBUpdates() {
286  $dbw = $this->db;
287 
288  // Update current image
289  $dbw->update(
290  'image',
291  [ 'img_name' => $this->newName ],
292  [ 'img_name' => $this->oldName ],
293  __METHOD__
294  );
295 
296  // Update old images
297  $dbw->update(
298  'oldimage',
299  [
300  'oi_name' => $this->newName,
301  'oi_archive_name = ' . $dbw->strreplace( 'oi_archive_name',
302  $dbw->addQuotes( $this->oldName ), $dbw->addQuotes( $this->newName ) ),
303  ],
304  [ 'oi_name' => $this->oldName ],
305  __METHOD__
306  );
307  }
308 
313  protected function getMoveTriplets() {
314  $moves = array_merge( [ $this->cur ], $this->olds );
315  $triplets = []; // The format is: (srcUrl, destZone, destUrl)
316 
317  foreach ( $moves as $move ) {
318  // $move: (oldRelativePath, newRelativePath)
319  $srcUrl = $this->file->repo->getVirtualUrl() . '/public/' . rawurlencode( $move[0] );
320  $triplets[] = [ $srcUrl, 'public', $move[1] ];
321 
322  $this->logger->debug(
323  'Generated move triplet for {fileName}: {srcUrl} :: public :: {move1}',
324  [
325  'fileName' => $this->file->getName(),
326  'srcUrl' => $srcUrl,
327  'move1' => $move[1],
328  ]
329  );
330  }
331 
332  return $triplets;
333  }
334 
340  protected function removeNonexistentFiles( $triplets ) {
341  $files = [];
342 
343  foreach ( $triplets as $file ) {
344  $files[$file[0]] = $file[0];
345  }
346 
347  $result = $this->file->repo->fileExistsBatch( $files );
348  if ( in_array( null, $result, true ) ) {
349  return Status::newFatal( 'backend-fail-internal',
350  $this->file->repo->getBackend()->getName() );
351  }
352 
353  $filteredTriplets = [];
354  foreach ( $triplets as $file ) {
355  if ( $result[$file[0]] ) {
356  $filteredTriplets[] = $file;
357  } else {
358  $this->logger->debug(
359  'File {file} does not exist',
360  [ 'file' => $file[0] ]
361  );
362  }
363  }
364 
365  return Status::newGood( $filteredTriplets );
366  }
367 
373  protected function cleanupTarget( $triplets ) {
374  // Create dest pairs from the triplets
375  $pairs = [];
376  foreach ( $triplets as $triplet ) {
377  // $triplet: (old source virtual URL, dst zone, dest rel)
378  $pairs[] = [ $triplet[1], $triplet[2] ];
379  }
380 
381  $this->file->repo->cleanupBatch( $pairs );
382  }
383 
389  protected function cleanupSource( $triplets ) {
390  // Create source file names from the triplets
391  $files = [];
392  foreach ( $triplets as $triplet ) {
393  $files[] = $triplet[0];
394  }
395 
396  $this->file->repo->cleanupBatch( $files );
397  }
398 }
LocalFileMoveBatch\$target
Title $target
Definition: LocalFileMoveBatch.php:38
StatusValue\newFatal
static newFatal( $message,... $parameters)
Factory function for fatal errors.
Definition: StatusValue.php:70
LocalFileMoveBatch\$oldRel
string $oldRel
Definition: LocalFileMoveBatch.php:64
LocalFileMoveBatch\$file
LocalFile $file
Definition: LocalFileMoveBatch.php:35
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:195
LocalFileMoveBatch\cleanupSource
cleanupSource( $triplets)
Cleanup a fully moved array of triplets by deleting the source files.
Definition: LocalFileMoveBatch.php:389
LocalFileMoveBatch\verifyDBUpdates
verifyDBUpdates()
Verify the database updates and return a new Status indicating how many rows would be updated.
Definition: LocalFileMoveBatch.php:248
LocalFileMoveBatch\$oldHash
string $oldHash
Definition: LocalFileMoveBatch.php:52
LocalFileMoveBatch\$newName
string $newName
Definition: LocalFileMoveBatch.php:61
Wikimedia\Rdbms\IDatabase
Basic database interface for live and lazy-loaded relation database handles.
Definition: IDatabase.php:38
LocalFileMoveBatch\$oldCount
$oldCount
Definition: LocalFileMoveBatch.php:44
LocalFileMoveBatch\addOlds
addOlds()
Add the old versions of the image to the batch.
Definition: LocalFileMoveBatch.php:101
Wikimedia\Rdbms\IDatabase\update
update( $table, $set, $conds, $fname=__METHOD__, $options=[])
Update all rows in a table that match a given condition.
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:49
LocalFileMoveBatch\$logger
LoggerInterface $logger
Definition: LocalFileMoveBatch.php:70
LocalFile
Class to represent a local file in the wiki's own database.
Definition: LocalFile.php:63
LocalFileMoveBatch\$archive
$archive
Definition: LocalFileMoveBatch.php:46
LocalFileMoveBatch\$oldName
string $oldName
Definition: LocalFileMoveBatch.php:58
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:313
Title
Represents a title within MediaWiki.
Definition: Title.php:47
LocalFileMoveBatch\$newHash
string $newHash
Definition: LocalFileMoveBatch.php:55
LocalFileMoveBatch\execute
execute()
Perform the move.
Definition: LocalFileMoveBatch.php:157
LocalFileMoveBatch\removeNonexistentFiles
removeNonexistentFiles( $triplets)
Removes non-existent files from move batch.
Definition: LocalFileMoveBatch.php:340
LocalFileMoveBatch\$newRel
string $newRel
Definition: LocalFileMoveBatch.php:67
LocalFileMoveBatch
Helper class for file movement.
Definition: LocalFileMoveBatch.php:33
File\DELETED_FILE
const DELETED_FILE
Definition: File.php:70
LocalFileMoveBatch\addCurrent
addCurrent()
Add the current image to the batch.
Definition: LocalFileMoveBatch.php:93
LocalFileMoveBatch\doDBUpdates
doDBUpdates()
Do the database updates and return a new Status indicating how many rows where updated.
Definition: LocalFileMoveBatch.php:285
LocalFileMoveBatch\__construct
__construct(LocalFile $file, Title $target)
Definition: LocalFileMoveBatch.php:76
LocalFileMoveBatch\$olds
$olds
Definition: LocalFileMoveBatch.php:42
LocalFileMoveBatch\$cur
$cur
Definition: LocalFileMoveBatch.php:40
LocalFileMoveBatch\cleanupTarget
cleanupTarget( $triplets)
Cleanup a partially moved array of triplets by deleting the target files.
Definition: LocalFileMoveBatch.php:373