41 parent::__construct();
43 $this->
addDescription(
'Script to update image metadata records' );
48 'Reload metadata from file even if the metadata looks ok',
55 'Only fix really broken records, leave old but still compatible records alone.'
59 'Fix records with an out of date serialization format.'
63 'Enable splitting out large metadata items to the text table. Implies --convert-to-json.'
67 'Output extra information about each upgraded/non-upgraded file.',
72 $this->
addOption(
'start',
'Name of file to start with',
false,
true );
73 $this->
addOption(
'end',
'Name of file to end with',
false,
true );
77 'Only refresh files with this media type, e.g. BITMAP, UNKNOWN etc.',
83 "Only refresh files with this MIME type. Can accept wild-card 'image/*'. "
84 .
"Potentially inefficient unless 'mediatype' is also specified",
90 '(Inefficient!) Only refresh files where the img_metadata field '
91 .
'contains this string. Can be used if its known a specific '
92 .
'property was being extracted incorrectly.',
98 'Time to sleep between each batch (in seconds). Default: 0',
102 $this->
addOption(
'oldimage',
'Run and refresh on oldimage table.' );
105 'File with file titles to refresh, one per line (newline delimited). '
106 .
'If provided, other filtering options (--mime, --mediatype, --metadata-contains, '
107 .
'--start, --end) will be ignored.',
115 $brokenOnly = $this->
hasOption(
'broken-only' );
116 $verbose = $this->
hasOption(
'verbose' );
118 $sleep = (int)$this->
getOption(
'sleep', 0 );
119 $reserialize = $this->
hasOption(
'convert-to-json' );
120 $oldimage = $this->
hasOption(
'oldimage' );
121 $listFile = $this->
getOption(
'listfile',
false );
124 if ( $batchSize <= 0 ) {
125 $this->
fatalError(
"Batch size is too low...", 12 );
129 $repo = $this->newLocalRepo( $force, $brokenOnly, $reserialize, $split );
131 if ( $listFile !==
false ) {
133 $this->processFilesByTitles( $listFile, $repo, $oldimage, $force, $verbose, $sleep );
136 $this->processFilesByDatabase( $repo, $oldimage, $force, $verbose, $sleep );
150 private function processFilesByTitles(
158 if ( !file_exists( $listFile ) ) {
159 $this->fatalError(
"List file does not exist: $listFile" );
162 $file = fopen( $listFile,
'r' );
164 $this->
fatalError(
"Unable to read list file: $listFile" );
168 $requestedNames = [];
172 while ( !feof( $file ) ) {
173 $line = trim( fgets( $file ) );
176 if ( $line ===
'' ) {
181 $titleValue = $this->titleParser->parseTitle( $line,
NS_FILE );
182 if ( $titleValue->getNamespace() !==
NS_FILE ) {
183 $invalidTitles[] = [
'line' => $lineNum,
'title' => $line ];
186 $requestedNames[$titleValue->getDBkey()] = $line;
188 $invalidTitles[] = [
'line' => $lineNum,
'title' => $line ];
195 foreach ( $invalidTitles as $invalid ) {
196 $this->
output(
"Invalid file title on line {$invalid['line']}: '{$invalid['title']}'\n" );
199 if ( count( $requestedNames ) === 0 ) {
200 $this->
output(
"No valid titles found in list file.\n" );
204 $this->
output(
"Found " . count( $requestedNames ) .
" valid title(s) to process.\n" );
208 $nameField = $oldimage ?
'oi_name' :
'img_name';
213 $nameBatches = array_chunk( array_keys( $requestedNames ), $batchSize );
215 foreach ( $nameBatches as $nameBatch ) {
217 $queryBuilder = FileSelectQueryBuilder::newForOldFile( $dbw );
219 $queryBuilder = FileSelectQueryBuilder::newForFile( $dbw );
222 ->
where( [ $nameField => $nameBatch ] )
223 ->caller( __METHOD__ );
227 [ $up, $left, $found ] = $this->processBatch( $res, $repo, $nameField, $force, $verbose );
230 $foundNames += $found;
239 $notFound = array_diff_key( $requestedNames, $foundNames );
240 if ( count( $notFound ) > 0 ) {
241 $this->
output(
"\nThe following " . count( $notFound ) .
" file(s) were not found in the database:\n" );
242 foreach ( $notFound as $dbKey => $originalTitle ) {
243 $this->
output(
" - $originalTitle\n" );
247 $this->outputResult( $upgraded, $leftAlone, $force );
259 private function processFilesByDatabase(
266 $start = $this->getOption(
'start', false );
267 $dbw = $this->getPrimaryDB();
269 $fieldPrefix =
'oi_';
270 $queryBuilderTemplate = FileSelectQueryBuilder::newForOldFile( $dbw );
272 $fieldPrefix =
'img_';
273 $queryBuilderTemplate = FileSelectQueryBuilder::newForFile( $dbw );
276 $batchSize = (int)$this->getBatchSize();
277 $nameField = $fieldPrefix .
'name';
281 $this->setConditions( $dbw, $queryBuilderTemplate, $fieldPrefix );
282 $queryBuilderTemplate
283 ->orderBy( $nameField, SelectQueryBuilder::SORT_ASC )
284 ->limit( $batchSize );
286 $batchCondition = [];
287 if ( $start !==
false ) {
288 $batchCondition[] = $dbw->
expr( $nameField,
'>=', $start );
292 $queryBuilder = clone $queryBuilderTemplate;
293 $res = $queryBuilder->
andWhere( $batchCondition )
294 ->caller( __METHOD__ )->fetchResultSet();
296 [ $up, $left, $found ] = $this->processBatch( $res, $repo, $nameField, $force, $verbose );
301 $lastName = array_key_last( $found );
302 $batchCondition = [ $dbw->
expr( $nameField,
'>', $lastName ) ];
305 $this->waitForReplication();
309 }
while ( $res->
numRows() === $batchSize );
311 $this->outputResult( $upgraded, $leftAlone, $force );
324 private function processBatch(
337 $firstName = $firstRow->$nameField;
339 $this->output(
"Processing next {$res->numRows()} row(s) starting with $firstName.\n" );
342 foreach ( $res as $row ) {
343 $name = $row->$nameField;
344 $foundNames[$name] =
true;
348 $file->maybeUpgradeRow();
349 if ( $file->getUpgraded() ) {
350 $this->output(
"Refreshed File:$name.\n" );
356 $this->output(
"Forcibly refreshed File:$name.\n" );
358 } elseif ( $verbose ) {
359 $this->output(
"Skipping File:$name.\n" );
363 }
catch ( Exception $e ) {
364 $this->output(
"$name failed. {$e->getMessage()}\n" );
368 return [ $upgraded, $leftAlone, $foundNames ];
378 private function outputResult(
int $upgraded,
int $leftAlone,
bool $force ): void {
379 $total = $upgraded + $leftAlone;
381 $this->output(
"\nFinished refreshing file metadata for $total files. "
382 .
"$upgraded needed to be refreshed, $leftAlone did not need to "
383 .
"be but were refreshed anyways.\n" );
385 $this->output(
"\nFinished refreshing file metadata for $total files. "
386 .
"$upgraded were refreshed, $leftAlone were already up to date.\n" );
395 private function setConditions(
400 $end = $this->getOption(
'end', false );
401 $mime = $this->getOption(
'mime',
false );
402 $mediatype = $this->getOption(
'mediatype',
false );
403 $like = $this->getOption(
'metadata-contains',
false );
405 if ( $end !==
false ) {
406 $queryBuilder->
andWhere( $dbw->
expr( $fieldPrefix .
'name',
'<=', $end ) );
408 if ( $mime !==
false ) {
409 [ $major, $minor ] = File::splitMime( $mime );
410 $queryBuilder->
andWhere( [ $fieldPrefix .
'major_mime' => $major ] );
411 if ( $minor !==
'*' ) {
412 $queryBuilder->
andWhere( [ $fieldPrefix .
'minor_mime' => $minor ] );
415 if ( $mediatype !==
false ) {
416 $queryBuilder->
andWhere( [ $fieldPrefix .
'media_type' => $mediatype ] );
420 $dbw->
expr( $fieldPrefix .
'metadata', IExpression::LIKE,
434 private function newLocalRepo(
bool $force,
bool $brokenOnly,
bool $reserialize,
bool $split ):
LocalRepo {
435 if ( $brokenOnly && $force ) {
436 $this->fatalError(
'Cannot use --broken-only and --force together. ', 2 );
438 $reserialize = $reserialize || $split;
439 if ( $brokenOnly && $reserialize ) {
440 $this->fatalError(
'Cannot use --broken-only with --convert-to-json or --split. ',
445 'updateCompatibleMetadata' => !$brokenOnly,
447 if ( $reserialize ) {
448 $overrides[
'reserializeMetadata'] =
true;
449 $overrides[
'useJsonMetadata'] =
true;
452 $overrides[
'useSplitMetadata'] =
true;
455 return $this->getServiceContainer()->getRepoGroup()
456 ->newCustomLocalRepo( $overrides );