Split LocalFile.php to have one class in one file
[lhc/web/wiklou.git] / includes / filerepo / file / LocalFileMoveBatch.php
1 <?php
2 /**
3 * Local file in the wiki's own database.
4 *
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.
9 *
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.
14 *
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
19 *
20 * @file
21 * @ingroup FileAbstraction
22 */
23
24 use Wikimedia\Rdbms\IDatabase;
25
26 /**
27 * Helper class for file movement
28 * @ingroup FileAbstraction
29 */
30 class LocalFileMoveBatch {
31 /** @var LocalFile */
32 protected $file;
33
34 /** @var Title */
35 protected $target;
36
37 protected $cur;
38
39 protected $olds;
40
41 protected $oldCount;
42
43 protected $archive;
44
45 /** @var IDatabase */
46 protected $db;
47
48 /**
49 * @param File $file
50 * @param Title $target
51 */
52 function __construct( File $file, Title $target ) {
53 $this->file = $file;
54 $this->target = $target;
55 $this->oldHash = $this->file->repo->getHashPath( $this->file->getName() );
56 $this->newHash = $this->file->repo->getHashPath( $this->target->getDBkey() );
57 $this->oldName = $this->file->getName();
58 $this->newName = $this->file->repo->getNameFromTitle( $this->target );
59 $this->oldRel = $this->oldHash . $this->oldName;
60 $this->newRel = $this->newHash . $this->newName;
61 $this->db = $file->getRepo()->getMasterDB();
62 }
63
64 /**
65 * Add the current image to the batch
66 */
67 public function addCurrent() {
68 $this->cur = [ $this->oldRel, $this->newRel ];
69 }
70
71 /**
72 * Add the old versions of the image to the batch
73 * @return string[] List of archive names from old versions
74 */
75 public function addOlds() {
76 $archiveBase = 'archive';
77 $this->olds = [];
78 $this->oldCount = 0;
79 $archiveNames = [];
80
81 $result = $this->db->select( 'oldimage',
82 [ 'oi_archive_name', 'oi_deleted' ],
83 [ 'oi_name' => $this->oldName ],
84 __METHOD__,
85 [ 'LOCK IN SHARE MODE' ] // ignore snapshot
86 );
87
88 foreach ( $result as $row ) {
89 $archiveNames[] = $row->oi_archive_name;
90 $oldName = $row->oi_archive_name;
91 $bits = explode( '!', $oldName, 2 );
92
93 if ( count( $bits ) != 2 ) {
94 wfDebug( "Old file name missing !: '$oldName' \n" );
95 continue;
96 }
97
98 list( $timestamp, $filename ) = $bits;
99
100 if ( $this->oldName != $filename ) {
101 wfDebug( "Old file name doesn't match: '$oldName' \n" );
102 continue;
103 }
104
105 $this->oldCount++;
106
107 // Do we want to add those to oldCount?
108 if ( $row->oi_deleted & File::DELETED_FILE ) {
109 continue;
110 }
111
112 $this->olds[] = [
113 "{$archiveBase}/{$this->oldHash}{$oldName}",
114 "{$archiveBase}/{$this->newHash}{$timestamp}!{$this->newName}"
115 ];
116 }
117
118 return $archiveNames;
119 }
120
121 /**
122 * Perform the move.
123 * @return Status
124 */
125 public function execute() {
126 $repo = $this->file->repo;
127 $status = $repo->newGood();
128 $destFile = wfLocalFile( $this->target );
129
130 $this->file->lock();
131 $destFile->lock(); // quickly fail if destination is not available
132
133 $triplets = $this->getMoveTriplets();
134 $checkStatus = $this->removeNonexistentFiles( $triplets );
135 if ( !$checkStatus->isGood() ) {
136 $destFile->unlock();
137 $this->file->unlock();
138 $status->merge( $checkStatus ); // couldn't talk to file backend
139 return $status;
140 }
141 $triplets = $checkStatus->value;
142
143 // Verify the file versions metadata in the DB.
144 $statusDb = $this->verifyDBUpdates();
145 if ( !$statusDb->isGood() ) {
146 $destFile->unlock();
147 $this->file->unlock();
148 $statusDb->setOK( false );
149
150 return $statusDb;
151 }
152
153 if ( !$repo->hasSha1Storage() ) {
154 // Copy the files into their new location.
155 // If a prior process fataled copying or cleaning up files we tolerate any
156 // of the existing files if they are identical to the ones being stored.
157 $statusMove = $repo->storeBatch( $triplets, FileRepo::OVERWRITE_SAME );
158 wfDebugLog( 'imagemove', "Moved files for {$this->file->getName()}: " .
159 "{$statusMove->successCount} successes, {$statusMove->failCount} failures" );
160 if ( !$statusMove->isGood() ) {
161 // Delete any files copied over (while the destination is still locked)
162 $this->cleanupTarget( $triplets );
163 $destFile->unlock();
164 $this->file->unlock();
165 wfDebugLog( 'imagemove', "Error in moving files: "
166 . $statusMove->getWikiText( false, false, 'en' ) );
167 $statusMove->setOK( false );
168
169 return $statusMove;
170 }
171 $status->merge( $statusMove );
172 }
173
174 // Rename the file versions metadata in the DB.
175 $this->doDBUpdates();
176
177 wfDebugLog( 'imagemove', "Renamed {$this->file->getName()} in database: " .
178 "{$statusDb->successCount} successes, {$statusDb->failCount} failures" );
179
180 $destFile->unlock();
181 $this->file->unlock();
182
183 // Everything went ok, remove the source files
184 $this->cleanupSource( $triplets );
185
186 $status->merge( $statusDb );
187
188 return $status;
189 }
190
191 /**
192 * Verify the database updates and return a new Status indicating how
193 * many rows would be updated.
194 *
195 * @return Status
196 */
197 protected function verifyDBUpdates() {
198 $repo = $this->file->repo;
199 $status = $repo->newGood();
200 $dbw = $this->db;
201
202 $hasCurrent = $dbw->lockForUpdate(
203 'image',
204 [ 'img_name' => $this->oldName ],
205 __METHOD__
206 );
207 $oldRowCount = $dbw->lockForUpdate(
208 'oldimage',
209 [ 'oi_name' => $this->oldName ],
210 __METHOD__
211 );
212
213 if ( $hasCurrent ) {
214 $status->successCount++;
215 } else {
216 $status->failCount++;
217 }
218 $status->successCount += $oldRowCount;
219 // T36934: oldCount is based on files that actually exist.
220 // There may be more DB rows than such files, in which case $affected
221 // can be greater than $total. We use max() to avoid negatives here.
222 $status->failCount += max( 0, $this->oldCount - $oldRowCount );
223 if ( $status->failCount ) {
224 $status->error( 'imageinvalidfilename' );
225 }
226
227 return $status;
228 }
229
230 /**
231 * Do the database updates and return a new Status indicating how
232 * many rows where updated.
233 */
234 protected function doDBUpdates() {
235 $dbw = $this->db;
236
237 // Update current image
238 $dbw->update(
239 'image',
240 [ 'img_name' => $this->newName ],
241 [ 'img_name' => $this->oldName ],
242 __METHOD__
243 );
244
245 // Update old images
246 $dbw->update(
247 'oldimage',
248 [
249 'oi_name' => $this->newName,
250 'oi_archive_name = ' . $dbw->strreplace( 'oi_archive_name',
251 $dbw->addQuotes( $this->oldName ), $dbw->addQuotes( $this->newName ) ),
252 ],
253 [ 'oi_name' => $this->oldName ],
254 __METHOD__
255 );
256 }
257
258 /**
259 * Generate triplets for FileRepo::storeBatch().
260 * @return array[]
261 */
262 protected function getMoveTriplets() {
263 $moves = array_merge( [ $this->cur ], $this->olds );
264 $triplets = []; // The format is: (srcUrl, destZone, destUrl)
265
266 foreach ( $moves as $move ) {
267 // $move: (oldRelativePath, newRelativePath)
268 $srcUrl = $this->file->repo->getVirtualUrl() . '/public/' . rawurlencode( $move[0] );
269 $triplets[] = [ $srcUrl, 'public', $move[1] ];
270 wfDebugLog(
271 'imagemove',
272 "Generated move triplet for {$this->file->getName()}: {$srcUrl} :: public :: {$move[1]}"
273 );
274 }
275
276 return $triplets;
277 }
278
279 /**
280 * Removes non-existent files from move batch.
281 * @param array $triplets
282 * @return Status
283 */
284 protected function removeNonexistentFiles( $triplets ) {
285 $files = [];
286
287 foreach ( $triplets as $file ) {
288 $files[$file[0]] = $file[0];
289 }
290
291 $result = $this->file->repo->fileExistsBatch( $files );
292 if ( in_array( null, $result, true ) ) {
293 return Status::newFatal( 'backend-fail-internal',
294 $this->file->repo->getBackend()->getName() );
295 }
296
297 $filteredTriplets = [];
298 foreach ( $triplets as $file ) {
299 if ( $result[$file[0]] ) {
300 $filteredTriplets[] = $file;
301 } else {
302 wfDebugLog( 'imagemove', "File {$file[0]} does not exist" );
303 }
304 }
305
306 return Status::newGood( $filteredTriplets );
307 }
308
309 /**
310 * Cleanup a partially moved array of triplets by deleting the target
311 * files. Called if something went wrong half way.
312 * @param array[] $triplets
313 */
314 protected function cleanupTarget( $triplets ) {
315 // Create dest pairs from the triplets
316 $pairs = [];
317 foreach ( $triplets as $triplet ) {
318 // $triplet: (old source virtual URL, dst zone, dest rel)
319 $pairs[] = [ $triplet[1], $triplet[2] ];
320 }
321
322 $this->file->repo->cleanupBatch( $pairs );
323 }
324
325 /**
326 * Cleanup a fully moved array of triplets by deleting the source files.
327 * Called at the end of the move process if everything else went ok.
328 * @param array[] $triplets
329 */
330 protected function cleanupSource( $triplets ) {
331 // Create source file names from the triplets
332 $files = [];
333 foreach ( $triplets as $triplet ) {
334 $files[] = $triplet[0];
335 }
336
337 $this->file->repo->cleanupBatch( $files );
338 }
339 }