3 * Local file in the wiki's own database.
5 * This program is free software; you can redistribute it and/or modify
6 * it under the terms of the GNU General Public License as published by
7 * the Free Software Foundation; either version 2 of the License, or
8 * (at your option) any later version.
10 * This program is distributed in the hope that it will be useful,
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 * GNU General Public License for more details.
15 * You should have received a copy of the GNU General Public License along
16 * with this program; if not, write to the Free Software Foundation, Inc.,
17 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
18 * http://www.gnu.org/copyleft/gpl.html
21 * @ingroup FileAbstraction
24 use MediaWiki\MediaWikiServices
;
25 use Wikimedia\Rdbms\IDatabase
;
28 * Helper class for file movement
29 * @ingroup FileAbstraction
31 class LocalFileMoveBatch
{
51 * @param Title $target
53 function __construct( File
$file, Title
$target ) {
55 $this->target
= $target;
56 $this->oldHash
= $this->file
->repo
->getHashPath( $this->file
->getName() );
57 $this->newHash
= $this->file
->repo
->getHashPath( $this->target
->getDBkey() );
58 $this->oldName
= $this->file
->getName();
59 $this->newName
= $this->file
->repo
->getNameFromTitle( $this->target
);
60 $this->oldRel
= $this->oldHash
. $this->oldName
;
61 $this->newRel
= $this->newHash
. $this->newName
;
62 $this->db
= $file->getRepo()->getMasterDB();
66 * Add the current image to the batch
68 public function addCurrent() {
69 $this->cur
= [ $this->oldRel
, $this->newRel
];
73 * Add the old versions of the image to the batch
74 * @return string[] List of archive names from old versions
76 public function addOlds() {
77 $archiveBase = 'archive';
82 $result = $this->db
->select( 'oldimage',
83 [ 'oi_archive_name', 'oi_deleted' ],
84 [ 'oi_name' => $this->oldName
],
86 [ 'LOCK IN SHARE MODE' ] // ignore snapshot
89 foreach ( $result as $row ) {
90 $archiveNames[] = $row->oi_archive_name
;
91 $oldName = $row->oi_archive_name
;
92 $bits = explode( '!', $oldName, 2 );
94 if ( count( $bits ) != 2 ) {
95 wfDebug( "Old file name missing !: '$oldName' \n" );
99 list( $timestamp, $filename ) = $bits;
101 if ( $this->oldName
!= $filename ) {
102 wfDebug( "Old file name doesn't match: '$oldName' \n" );
108 // Do we want to add those to oldCount?
109 if ( $row->oi_deleted
& File
::DELETED_FILE
) {
114 "{$archiveBase}/{$this->oldHash}{$oldName}",
115 "{$archiveBase}/{$this->newHash}{$timestamp}!{$this->newName}"
119 return $archiveNames;
126 public function execute() {
127 $repo = $this->file
->repo
;
128 $status = $repo->newGood();
129 $destFile = MediaWikiServices
::getInstance()->getRepoGroup()->getLocalRepo()
130 ->newFile( $this->target
);
133 $destFile->lock(); // quickly fail if destination is not available
135 $triplets = $this->getMoveTriplets();
136 $checkStatus = $this->removeNonexistentFiles( $triplets );
137 if ( !$checkStatus->isGood() ) {
139 $this->file
->unlock();
140 $status->merge( $checkStatus ); // couldn't talk to file backend
143 $triplets = $checkStatus->value
;
145 // Verify the file versions metadata in the DB.
146 $statusDb = $this->verifyDBUpdates();
147 if ( !$statusDb->isGood() ) {
149 $this->file
->unlock();
150 $statusDb->setOK( false );
155 if ( !$repo->hasSha1Storage() ) {
156 // Copy the files into their new location.
157 // If a prior process fataled copying or cleaning up files we tolerate any
158 // of the existing files if they are identical to the ones being stored.
159 $statusMove = $repo->storeBatch( $triplets, FileRepo
::OVERWRITE_SAME
);
160 wfDebugLog( 'imagemove', "Moved files for {$this->file->getName()}: " .
161 "{$statusMove->successCount} successes, {$statusMove->failCount} failures" );
162 if ( !$statusMove->isGood() ) {
163 // Delete any files copied over (while the destination is still locked)
164 $this->cleanupTarget( $triplets );
166 $this->file
->unlock();
167 wfDebugLog( 'imagemove', "Error in moving files: "
168 . $statusMove->getWikiText( false, false, 'en' ) );
169 $statusMove->setOK( false );
173 $status->merge( $statusMove );
176 // Rename the file versions metadata in the DB.
177 $this->doDBUpdates();
179 wfDebugLog( 'imagemove', "Renamed {$this->file->getName()} in database: " .
180 "{$statusDb->successCount} successes, {$statusDb->failCount} failures" );
183 $this->file
->unlock();
185 // Everything went ok, remove the source files
186 $this->cleanupSource( $triplets );
188 $status->merge( $statusDb );
194 * Verify the database updates and return a new Status indicating how
195 * many rows would be updated.
199 protected function verifyDBUpdates() {
200 $repo = $this->file
->repo
;
201 $status = $repo->newGood();
204 $hasCurrent = $dbw->lockForUpdate(
206 [ 'img_name' => $this->oldName
],
209 $oldRowCount = $dbw->lockForUpdate(
211 [ 'oi_name' => $this->oldName
],
216 $status->successCount++
;
218 $status->failCount++
;
220 $status->successCount +
= $oldRowCount;
221 // T36934: oldCount is based on files that actually exist.
222 // There may be more DB rows than such files, in which case $affected
223 // can be greater than $total. We use max() to avoid negatives here.
224 $status->failCount +
= max( 0, $this->oldCount
- $oldRowCount );
225 if ( $status->failCount
) {
226 $status->error( 'imageinvalidfilename' );
233 * Do the database updates and return a new Status indicating how
234 * many rows where updated.
236 protected function doDBUpdates() {
239 // Update current image
242 [ 'img_name' => $this->newName
],
243 [ 'img_name' => $this->oldName
],
251 'oi_name' => $this->newName
,
252 'oi_archive_name = ' . $dbw->strreplace( 'oi_archive_name',
253 $dbw->addQuotes( $this->oldName
), $dbw->addQuotes( $this->newName
) ),
255 [ 'oi_name' => $this->oldName
],
261 * Generate triplets for FileRepo::storeBatch().
264 protected function getMoveTriplets() {
265 $moves = array_merge( [ $this->cur
], $this->olds
);
266 $triplets = []; // The format is: (srcUrl, destZone, destUrl)
268 foreach ( $moves as $move ) {
269 // $move: (oldRelativePath, newRelativePath)
270 $srcUrl = $this->file
->repo
->getVirtualUrl() . '/public/' . rawurlencode( $move[0] );
271 $triplets[] = [ $srcUrl, 'public', $move[1] ];
274 "Generated move triplet for {$this->file->getName()}: {$srcUrl} :: public :: {$move[1]}"
282 * Removes non-existent files from move batch.
283 * @param array $triplets
286 protected function removeNonexistentFiles( $triplets ) {
289 foreach ( $triplets as $file ) {
290 $files[$file[0]] = $file[0];
293 $result = $this->file
->repo
->fileExistsBatch( $files );
294 if ( in_array( null, $result, true ) ) {
295 return Status
::newFatal( 'backend-fail-internal',
296 $this->file
->repo
->getBackend()->getName() );
299 $filteredTriplets = [];
300 foreach ( $triplets as $file ) {
301 if ( $result[$file[0]] ) {
302 $filteredTriplets[] = $file;
304 wfDebugLog( 'imagemove', "File {$file[0]} does not exist" );
308 return Status
::newGood( $filteredTriplets );
312 * Cleanup a partially moved array of triplets by deleting the target
313 * files. Called if something went wrong half way.
314 * @param array[] $triplets
316 protected function cleanupTarget( $triplets ) {
317 // Create dest pairs from the triplets
319 foreach ( $triplets as $triplet ) {
320 // $triplet: (old source virtual URL, dst zone, dest rel)
321 $pairs[] = [ $triplet[1], $triplet[2] ];
324 $this->file
->repo
->cleanupBatch( $pairs );
328 * Cleanup a fully moved array of triplets by deleting the source files.
329 * Called at the end of the move process if everything else went ok.
330 * @param array[] $triplets
332 protected function cleanupSource( $triplets ) {
333 // Create source file names from the triplets
335 foreach ( $triplets as $triplet ) {
336 $files[] = $triplet[0];
339 $this->file
->repo
->cleanupBatch( $files );