MediaWiki REL1_37
LocalFileMoveBatch.php
Go to the documentation of this file.
1<?php
26use 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}
const OVERWRITE_SAME
Definition FileRepo.php:48
Helper class for file movement.
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.
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)
LoggerInterface $logger
addCurrent()
Add the current image to the batch.
cleanupSource( $triplets)
Cleanup a fully moved array of triplets by deleting the source files.
Class to represent a local file in the wiki's own database.
Definition LocalFile.php:63
PSR-3 logger instance factory.
MediaWikiServices is the service locator for the application scope of MediaWiki.
Represents a title within MediaWiki.
Definition Title.php:48
Basic database interface for live and lazy-loaded relation database handles.
Definition IDatabase.php:38
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.