MediaWiki REL1_35
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
71 public function __construct( LocalFile $file, Title $target ) {
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'" );
114 continue;
115 }
116
117 list( $timestamp, $filename ) = $bits;
118
119 if ( $this->oldName != $filename ) {
120 wfDebug( "Old file name doesn't match: '$oldName'" );
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] ];
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}
wfDebug( $text, $dest='all', array $context=[])
Sends a line to the debug log if enabled or, optionally, to a comment in output.
wfDebugLog( $logGroup, $text, $dest='all', array $context=[])
Send a line to a supplementary debug log file, if configured, or main debug log if not.
const OVERWRITE_SAME
Definition FileRepo.php:44
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)
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:59
MediaWikiServices is the service locator for the application scope of MediaWiki.
Represents a title within MediaWiki.
Definition Title.php:42
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.