MediaWiki  master
LocalFileMoveBatch.php
Go to the documentation of this file.
1 <?php
26 
33  protected $file;
34 
36  protected $target;
37 
38  protected $cur;
39 
40  protected $olds;
41 
42  protected $oldCount;
43 
44  protected $archive;
45 
47  protected $db;
48 
50  protected $oldHash;
51 
53  protected $newHash;
54 
56  protected $oldName;
57 
59  protected $newName;
60 
62  protected $oldRel;
63 
65  protected $newRel;
66 
72  $this->file = $file;
73  $this->target = $target;
74  $this->oldHash = $this->file->repo->getHashPath( $this->file->getName() );
75  $this->newHash = $this->file->repo->getHashPath( $this->target->getDBkey() );
76  $this->oldName = $this->file->getName();
77  $this->newName = $this->file->repo->getNameFromTitle( $this->target );
78  $this->oldRel = $this->oldHash . $this->oldName;
79  $this->newRel = $this->newHash . $this->newName;
80  $this->db = $file->getRepo()->getMasterDB();
81  }
82 
86  public function addCurrent() {
87  $this->cur = [ $this->oldRel, $this->newRel ];
88  }
89 
94  public function addOlds() {
95  $archiveBase = 'archive';
96  $this->olds = [];
97  $this->oldCount = 0;
98  $archiveNames = [];
99 
100  $result = $this->db->select( 'oldimage',
101  [ 'oi_archive_name', 'oi_deleted' ],
102  [ 'oi_name' => $this->oldName ],
103  __METHOD__,
104  [ 'LOCK IN SHARE MODE' ] // ignore snapshot
105  );
106 
107  foreach ( $result as $row ) {
108  $archiveNames[] = $row->oi_archive_name;
109  $oldName = $row->oi_archive_name;
110  $bits = explode( '!', $oldName, 2 );
111 
112  if ( count( $bits ) != 2 ) {
113  wfDebug( "Old file name missing !: '$oldName' \n" );
114  continue;
115  }
116 
117  list( $timestamp, $filename ) = $bits;
118 
119  if ( $this->oldName != $filename ) {
120  wfDebug( "Old file name doesn't match: '$oldName' \n" );
121  continue;
122  }
123 
124  $this->oldCount++;
125 
126  // Do we want to add those to oldCount?
127  if ( $row->oi_deleted & File::DELETED_FILE ) {
128  continue;
129  }
130 
131  $this->olds[] = [
132  "{$archiveBase}/{$this->oldHash}{$oldName}",
133  "{$archiveBase}/{$this->newHash}{$timestamp}!{$this->newName}"
134  ];
135  }
136 
137  return $archiveNames;
138  }
139 
144  public function execute() {
145  $repo = $this->file->repo;
146  $status = $repo->newGood();
147  $destFile = MediaWikiServices::getInstance()->getRepoGroup()->getLocalRepo()
148  ->newFile( $this->target );
149 
150  $this->file->lock();
151  $destFile->lock(); // quickly fail if destination is not available
152 
153  $triplets = $this->getMoveTriplets();
154  $checkStatus = $this->removeNonexistentFiles( $triplets );
155  if ( !$checkStatus->isGood() ) {
156  $destFile->unlock();
157  $this->file->unlock();
158  $status->merge( $checkStatus ); // couldn't talk to file backend
159  return $status;
160  }
161  $triplets = $checkStatus->value;
162 
163  // Verify the file versions metadata in the DB.
164  $statusDb = $this->verifyDBUpdates();
165  if ( !$statusDb->isGood() ) {
166  $destFile->unlock();
167  $this->file->unlock();
168  $statusDb->setOK( false );
169 
170  return $statusDb;
171  }
172 
173  if ( !$repo->hasSha1Storage() ) {
174  // Copy the files into their new location.
175  // If a prior process fataled copying or cleaning up files we tolerate any
176  // of the existing files if they are identical to the ones being stored.
177  $statusMove = $repo->storeBatch( $triplets, FileRepo::OVERWRITE_SAME );
178  wfDebugLog( 'imagemove', "Moved files for {$this->file->getName()}: " .
179  "{$statusMove->successCount} successes, {$statusMove->failCount} failures" );
180  if ( !$statusMove->isGood() ) {
181  // Delete any files copied over (while the destination is still locked)
182  $this->cleanupTarget( $triplets );
183  $destFile->unlock();
184  $this->file->unlock();
185  wfDebugLog( 'imagemove', "Error in moving files: "
186  . $statusMove->getWikiText( false, false, 'en' ) );
187  $statusMove->setOK( false );
188 
189  return $statusMove;
190  }
191  $status->merge( $statusMove );
192  }
193 
194  // Rename the file versions metadata in the DB.
195  $this->doDBUpdates();
196 
197  wfDebugLog( 'imagemove', "Renamed {$this->file->getName()} in database: " .
198  "{$statusDb->successCount} successes, {$statusDb->failCount} failures" );
199 
200  $destFile->unlock();
201  $this->file->unlock();
202 
203  // Everything went ok, remove the source files
204  $this->cleanupSource( $triplets );
205 
206  $status->merge( $statusDb );
207 
208  return $status;
209  }
210 
217  protected function verifyDBUpdates() {
218  $repo = $this->file->repo;
219  $status = $repo->newGood();
220  $dbw = $this->db;
221 
222  $hasCurrent = $dbw->lockForUpdate(
223  'image',
224  [ 'img_name' => $this->oldName ],
225  __METHOD__
226  );
227  $oldRowCount = $dbw->lockForUpdate(
228  'oldimage',
229  [ 'oi_name' => $this->oldName ],
230  __METHOD__
231  );
232 
233  if ( $hasCurrent ) {
234  $status->successCount++;
235  } else {
236  $status->failCount++;
237  }
238  $status->successCount += $oldRowCount;
239  // T36934: oldCount is based on files that actually exist.
240  // There may be more DB rows than such files, in which case $affected
241  // can be greater than $total. We use max() to avoid negatives here.
242  $status->failCount += max( 0, $this->oldCount - $oldRowCount );
243  if ( $status->failCount ) {
244  $status->error( 'imageinvalidfilename' );
245  }
246 
247  return $status;
248  }
249 
254  protected function doDBUpdates() {
255  $dbw = $this->db;
256 
257  // Update current image
258  $dbw->update(
259  'image',
260  [ 'img_name' => $this->newName ],
261  [ 'img_name' => $this->oldName ],
262  __METHOD__
263  );
264 
265  // Update old images
266  $dbw->update(
267  'oldimage',
268  [
269  'oi_name' => $this->newName,
270  'oi_archive_name = ' . $dbw->strreplace( 'oi_archive_name',
271  $dbw->addQuotes( $this->oldName ), $dbw->addQuotes( $this->newName ) ),
272  ],
273  [ 'oi_name' => $this->oldName ],
274  __METHOD__
275  );
276  }
277 
282  protected function getMoveTriplets() {
283  $moves = array_merge( [ $this->cur ], $this->olds );
284  $triplets = []; // The format is: (srcUrl, destZone, destUrl)
285 
286  foreach ( $moves as $move ) {
287  // $move: (oldRelativePath, newRelativePath)
288  $srcUrl = $this->file->repo->getVirtualUrl() . '/public/' . rawurlencode( $move[0] );
289  $triplets[] = [ $srcUrl, 'public', $move[1] ];
290  wfDebugLog(
291  'imagemove',
292  "Generated move triplet for {$this->file->getName()}: {$srcUrl} :: public :: {$move[1]}"
293  );
294  }
295 
296  return $triplets;
297  }
298 
304  protected function removeNonexistentFiles( $triplets ) {
305  $files = [];
306 
307  foreach ( $triplets as $file ) {
308  $files[$file[0]] = $file[0];
309  }
310 
311  $result = $this->file->repo->fileExistsBatch( $files );
312  if ( in_array( null, $result, true ) ) {
313  return Status::newFatal( 'backend-fail-internal',
314  $this->file->repo->getBackend()->getName() );
315  }
316 
317  $filteredTriplets = [];
318  foreach ( $triplets as $file ) {
319  if ( $result[$file[0]] ) {
320  $filteredTriplets[] = $file;
321  } else {
322  wfDebugLog( 'imagemove', "File {$file[0]} does not exist" );
323  }
324  }
325 
326  return Status::newGood( $filteredTriplets );
327  }
328 
334  protected function cleanupTarget( $triplets ) {
335  // Create dest pairs from the triplets
336  $pairs = [];
337  foreach ( $triplets as $triplet ) {
338  // $triplet: (old source virtual URL, dst zone, dest rel)
339  $pairs[] = [ $triplet[1], $triplet[2] ];
340  }
341 
342  $this->file->repo->cleanupBatch( $pairs );
343  }
344 
350  protected function cleanupSource( $triplets ) {
351  // Create source file names from the triplets
352  $files = [];
353  foreach ( $triplets as $triplet ) {
354  $files[] = $triplet[0];
355  }
356 
357  $this->file->repo->cleanupBatch( $files );
358  }
359 }
static newFatal( $message,... $parameters)
Factory function for fatal errors.
Definition: StatusValue.php:69
addCurrent()
Add the current image to the batch.
verifyDBUpdates()
Verify the database updates and return a new Status indicating how many rows would be updated...
getRepo()
Returns the repository.
Definition: File.php:1879
cleanupSource( $triplets)
Cleanup a fully moved array of triplets by deleting the source files.
doDBUpdates()
Do the database updates and return a new Status indicating how many rows where updated.
const DELETED_FILE
Definition: File.php:63
__construct(File $file, Title $target)
cleanupTarget( $triplets)
Cleanup a partially moved array of triplets by deleting the target files.
static newGood( $value=null)
Factory function for good results.
Definition: StatusValue.php:81
wfDebug( $text, $dest='all', array $context=[])
Sends a line to the debug log if enabled or, optionally, to a comment in output.
Helper class for file movement.
execute()
Perform the move.
removeNonexistentFiles( $triplets)
Removes non-existent files from move batch.
const OVERWRITE_SAME
Definition: FileRepo.php:42
wfDebugLog( $logGroup, $text, $dest='all', array $context=[])
Send a line to a supplementary debug log file, if configured, or main debug log if not...
addOlds()
Add the old versions of the image to the batch.
getMoveTriplets()
Generate triplets for FileRepo::storeBatch().
lockForUpdate( $table, $conds='', $fname=__METHOD__, $options=[], $join_conds=[])
Lock all rows meeting the given conditions/options FOR UPDATE.
update( $table, $values, $conds, $fname=__METHOD__, $options=[])
UPDATE wrapper.
Implements some public methods and some protected utility functions which are required by multiple ch...
Definition: File.php:61