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