MediaWiki master
purgeChangedFiles.php
Go to the documentation of this file.
1<?php
15
16// @codeCoverageIgnoreStart
17require_once __DIR__ . '/Maintenance.php';
18// @codeCoverageIgnoreEnd
19
31 private static $typeMappings = [
32 'created' => [
33 'upload' => [ 'upload' ],
34 'import' => [ 'upload', 'interwiki' ],
35 ],
36 'deleted' => [
37 'delete' => [ 'delete', 'revision' ],
38 'suppress' => [ 'delete', 'revision' ],
39 ],
40 'modified' => [
41 'upload' => [ 'overwrite', 'revert' ],
42 'move' => [ 'move', 'move_redir' ],
43 ],
44 ];
45
49 private $startTimestamp;
50
54 private $endTimestamp;
55
56 public function __construct() {
57 parent::__construct();
58 $this->addDescription( 'Scan the logging table and purge files and thumbnails.' );
59 $this->addOption( 'starttime', 'Starting timestamp', true, true );
60 $this->addOption( 'endtime', 'Ending timestamp', true, true );
61 $this->addOption( 'type', 'Comma-separated list of types of changes to send purges for (' .
62 implode( ',', array_keys( self::$typeMappings ) ) . ',all)', false, true );
63 $this->addOption( 'htcp-dest', 'HTCP announcement destination (IP:port)', false, true );
64 $this->addOption( 'dry-run', 'Do not send purge requests' );
65 $this->addOption( 'sleep-per-batch', 'Milliseconds to sleep between batches', false, true );
66 $this->addOption( 'verbose', 'Show more output', false, false, 'v' );
67 $this->setBatchSize( 100 );
68 }
69
70 public function execute() {
71 global $wgHTCPRouting;
72
73 if ( $this->hasOption( 'htcp-dest' ) ) {
74 $parts = explode( ':', $this->getOption( 'htcp-dest' ), 2 );
75 if ( count( $parts ) < 2 ) {
76 // Add default htcp port
77 $parts[] = '4827';
78 }
79
80 // Route all HTCP messages to provided host:port
82 '' => [ 'host' => $parts[0], 'port' => $parts[1] ],
83 ];
84 $this->verbose( "HTCP broadcasts to {$parts[0]}:{$parts[1]}\n" );
85 }
86
87 // Find out which actions we should be concerned with
88 $typeOpt = $this->getOption( 'type', 'all' );
89 if ( $typeOpt === 'all' ) {
90 // Convert 'all' to all registered types
91 $typeOpt = implode( ',', array_keys( self::$typeMappings ) );
92 }
93 $typeList = explode( ',', $typeOpt );
94 foreach ( $typeList as $type ) {
95 if ( !isset( self::$typeMappings[$type] ) ) {
96 $this->error( "\nERROR: Unknown type: {$type}\n" );
97 $this->maybeHelp( true );
98 }
99 }
100
101 // Validate the timestamps
102 $dbr = $this->getReplicaDB();
103 $this->startTimestamp = $dbr->timestamp( $this->getOption( 'starttime' ) );
104 $this->endTimestamp = $dbr->timestamp( $this->getOption( 'endtime' ) );
105
106 if ( $this->startTimestamp > $this->endTimestamp ) {
107 $this->error( "\nERROR: starttime after endtime\n" );
108 $this->maybeHelp( true );
109 }
110
111 // Turn on verbose when dry-run is enabled
112 if ( $this->hasOption( 'dry-run' ) ) {
113 $this->setOption( 'verbose', 1 );
114 }
115
116 $this->verbose( 'Purging files that were: ' . implode( ', ', $typeList ) . "\n" );
117 foreach ( $typeList as $type ) {
118 $this->verbose( "Checking for {$type} files...\n" );
119 $this->purgeFromLogType( $type );
120 if ( !$this->hasOption( 'dry-run' ) ) {
121 $this->verbose( "...{$type} files purged.\n\n" );
122 }
123 }
124 }
125
131 protected function purgeFromLogType( $type ) {
132 $repo = $this->getServiceContainer()->getRepoGroup()->getLocalRepo();
133 $dbr = $this->getReplicaDB();
134
135 foreach ( self::$typeMappings[$type] as $logType => $logActions ) {
136 $this->verbose( "Scanning for {$logType}/" . implode( ',', $logActions ) . "\n" );
137
138 $res = $dbr->newSelectQueryBuilder()
139 ->select( [ 'log_title', 'log_timestamp', 'log_params' ] )
140 ->from( 'logging' )
141 ->where( [
142 'log_namespace' => NS_FILE,
143 'log_type' => $logType,
144 'log_action' => $logActions,
145 $dbr->expr( 'log_timestamp', '>=', $this->startTimestamp ),
146 $dbr->expr( 'log_timestamp', '<=', $this->endTimestamp ),
147 ] )
148 ->caller( __METHOD__ )->fetchResultSet();
149
150 $bSize = 0;
151 foreach ( $res as $row ) {
152 $file = $repo->newFile( Title::makeTitle( NS_FILE, $row->log_title ) );
153
154 if ( $this->hasOption( 'dry-run' ) ) {
155 $this->verbose( "{$type}[{$row->log_timestamp}]: {$row->log_title}\n" );
156 continue;
157 }
158
159 // Purge current version and its thumbnails
160 $file->purgeCache();
161 // Purge the old versions and their thumbnails
162 foreach ( $file->getHistory() as $oldFile ) {
163 $oldFile->purgeCache();
164 }
165
166 if ( $logType === 'delete' ) {
167 // If there is an orphaned storage file... delete it
168 if ( !$file->exists() && $repo->fileExists( $file->getPath() ) ) {
169 $dpath = $this->getDeletedPath( $repo, $file );
170 if ( $repo->fileExists( $dpath ) ) {
171 // Check to avoid data loss
172 $repo->getBackend()->delete( [ 'src' => $file->getPath() ] );
173 $this->verbose( "Deleted orphan file: {$file->getPath()}.\n" );
174 } else {
175 $this->error( "File was not deleted: {$file->getPath()}.\n" );
176 }
177 }
178
179 // Purge items from fileachive table (rows are likely here)
180 $this->purgeFromArchiveTable( $repo, $file );
181 } elseif ( $logType === 'move' ) {
182 // Purge the target file as well
183 // (only handles hard-coded core logs, so no need to pass log type/action here)
184 $params = LogEntryBase::extractParams( $row->log_params, null );
185 if ( isset( $params['4::target'] ) ) {
186 $target = $params['4::target'];
187 $targetFile = $repo->newFile( Title::makeTitle( NS_FILE, $target ) );
188 $targetFile->purgeCache();
189 $this->verbose( "Purged file {$target}; move target @{$row->log_timestamp}.\n" );
190 }
191 }
192
193 $this->verbose( "Purged file {$row->log_title}; {$type} @{$row->log_timestamp}.\n" );
194
195 if ( $this->hasOption( 'sleep-per-batch' ) && ++$bSize > $this->getBatchSize() ) {
196 $bSize = 0;
197 // sleep-per-batch is milliseconds, usleep wants micro seconds.
198 usleep( 1000 * (int)$this->getOption( 'sleep-per-batch' ) );
199 }
200 }
201 }
202 }
203
204 protected function purgeFromArchiveTable( LocalRepo $repo, LocalFile $file ) {
205 $dbr = $repo->getReplicaDB();
206 $res = $dbr->newSelectQueryBuilder()
207 ->select( [ 'fa_archive_name' ] )
208 ->from( 'filearchive' )
209 ->where( [ 'fa_name' => $file->getName() ] )
210 ->caller( __METHOD__ )->fetchResultSet();
211
212 foreach ( $res as $row ) {
213 if ( $row->fa_archive_name === null ) {
214 // Was not an old version (current version names checked already)
215 continue;
216 }
217 $ofile = $repo->newFromArchiveName( $file->getTitle(), $row->fa_archive_name );
218 // If there is an orphaned storage file still there...delete it
219 if ( !$file->exists() && $repo->fileExists( $ofile->getPath() ) ) {
220 $dpath = $this->getDeletedPath( $repo, $ofile );
221 if ( $repo->fileExists( $dpath ) ) {
222 // Check to avoid data loss
223 $repo->getBackend()->delete( [ 'src' => $ofile->getPath() ] );
224 $this->output( "Deleted orphan file: {$ofile->getPath()}.\n" );
225 } else {
226 $this->error( "File was not deleted: {$ofile->getPath()}.\n" );
227 }
228 }
229 $file->purgeOldThumbnails( $row->fa_archive_name );
230 }
231 }
232
233 protected function getDeletedPath( LocalRepo $repo, LocalFile $file ): string {
234 $hash = $repo->getFileSha1( $file->getPath() );
235 $key = "{$hash}.{$file->getExtension()}";
236
237 return $repo->getDeletedHashPath( $key ) . $key;
238 }
239
245 protected function verbose( $msg ) {
246 if ( $this->hasOption( 'verbose' ) ) {
247 $this->output( $msg );
248 }
249 }
250}
251
252// @codeCoverageIgnoreStart
253$maintClass = PurgeChangedFiles::class;
254require_once RUN_MAINTENANCE_IF_MAIN;
255// @codeCoverageIgnoreEnd
const NS_FILE
Definition Defines.php:57
getDeletedHashPath( $key)
Get a relative path for a deletion archive key, e.g.
fileExists( $file)
Checks existence of a file.
getBackend()
Get the file backend instance.
Definition FileRepo.php:253
getTitle()
Return the associated title object.
Definition File.php:378
getName()
Return the name of this file.
Definition File.php:348
Local file in the wiki's own database.
Definition LocalFile.php:80
purgeOldThumbnails( $archiveName)
Delete cached transformed files for an archived version only.
Local repository that stores files in the local filesystem and registers them in the wiki's own datab...
Definition LocalRepo.php:44
newFromArchiveName( $title, $archiveName)
getReplicaDB()
Get a connection to the replica DB.
Extends the LogEntry Interface with some basic functionality.
Abstract maintenance class for quickly writing and churning out maintenance scripts with minimal effo...
getBatchSize()
Returns batch size.
output( $out, $channel=null)
Throw some output to the user.
error( $err)
Throw an error to the user.
addOption( $name, $description, $required=false, $withArg=false, $shortName=false, $multiOccurrence=false)
Add a parameter to the script.
hasOption( $name)
Checks to see if a particular option was set.
setOption(string $name, $value)
Programmatically set the value of the given option.
getOption( $name, $default=null)
Get an option, or return the default.
getReplicaDB(string|false $virtualDomain=false)
getServiceContainer()
Returns the main service container.
maybeHelp( $force=false)
Maybe show the help.
addDescription( $text)
Set the description text.
Represents a title within MediaWiki.
Definition Title.php:69
Maintenance script that scans the deletion log and purges affected files within a timeframe.
execute()
Do the actual work.
__construct()
Default constructor.
purgeFromLogType( $type)
Purge cache and thumbnails for changes of the given type.
getDeletedPath(LocalRepo $repo, LocalFile $file)
purgeFromArchiveTable(LocalRepo $repo, LocalFile $file)
verbose( $msg)
Send an output message iff the 'verbose' option has been provided.
$wgHTCPRouting
Config variable stub for the HTCPRouting setting, for use by phpdoc and IDEs.