3 * Remove old or broken uploads from temporary uploaded file storage,
4 * clean up associated database records
6 * Copyright © 2011, Wikimedia Foundation
8 * This program is free software; you can redistribute it and/or modify
9 * it under the terms of the GNU General Public License as published by
10 * the Free Software Foundation; either version 2 of the License, or
11 * (at your option) any later version.
13 * This program is distributed in the hope that it will be useful,
14 * but WITHOUT ANY WARRANTY; without even the implied warranty of
15 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16 * GNU General Public License for more details.
18 * You should have received a copy of the GNU General Public License along
19 * with this program; if not, write to the Free Software Foundation, Inc.,
20 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
21 * http://www.gnu.org/copyleft/gpl.html
24 * @author Ian Baker <ibaker@wikimedia.org>
25 * @ingroup Maintenance
28 use MediaWiki\MainConfigNames
;
29 use MediaWiki\Maintenance\Maintenance
;
31 // @codeCoverageIgnoreStart
32 require_once __DIR__
. '/Maintenance.php';
33 // @codeCoverageIgnoreEnd
36 * Maintenance script to remove old or broken uploads from temporary uploaded
37 * file storage and clean up associated database records.
39 * @ingroup Maintenance
41 class CleanupUploadStash
extends Maintenance
{
43 public function __construct() {
44 parent
::__construct();
45 $this->addDescription( 'Clean up abandoned files in temporary uploaded file stash' );
46 $this->setBatchSize( 50 );
49 public function execute() {
50 $repo = $this->getServiceContainer()->getRepoGroup()->getLocalRepo();
51 $tempRepo = $repo->getTempRepo();
53 $dbr = $repo->getReplicaDB();
55 // how far back should this look for files to delete?
56 $cutoff = time() - (int)$this->getConfig()->get( MainConfigNames
::UploadStashMaxAge
);
58 $this->output( "Getting list of files to clean up...\n" );
59 $res = $dbr->newSelectQueryBuilder()
61 ->from( 'uploadstash' )
62 ->where( $dbr->expr( 'us_timestamp', '<', $dbr->timestamp( $cutoff ) ) )
63 ->caller( __METHOD__
)
66 // Delete all registered stash files...
67 if ( $res->numRows() == 0 ) {
68 $this->output( "No stashed files to cleanup according to the DB.\n" );
70 // finish the read before starting writes.
72 foreach ( $res as $row ) {
73 $keys[] = $row->us_key
;
76 $this->output( 'Removing ' . count( $keys ) . " file(s)...\n" );
77 // this could be done some other, more direct/efficient way, but using
78 // UploadStash's own methods means it's less likely to fall accidentally
79 // out-of-date someday
80 $stash = new UploadStash( $repo );
83 foreach ( $keys as $key ) {
86 $stash->getFile( $key, true );
87 $stash->removeFileNoAuth( $key );
88 } catch ( UploadStashException
$ex ) {
89 $type = get_class( $ex );
90 $this->output( "Failed removing stashed upload with key: $key ($type)\n" );
92 if ( $i %
100 == 0 ) {
93 $this->waitForReplication();
94 $this->output( "$i\n" );
97 $this->output( "$i done\n" );
100 // Delete all the corresponding thumbnails...
101 $dir = $tempRepo->getZonePath( 'thumb' );
102 $iterator = $tempRepo->getBackend()->getFileList( [ 'dir' => $dir, 'adviseStat' => 1 ] );
103 if ( $iterator === null ) {
104 $this->fatalError( "Could not get file listing." );
106 $this->output( "Deleting old thumbnails...\n" );
109 foreach ( $iterator as $file ) {
110 if ( wfTimestamp( TS_UNIX
, $tempRepo->getFileTimestamp( "$dir/$file" ) ) < $cutoff ) {
111 $batch[] = [ 'op' => 'delete', 'src' => "$dir/$file" ];
112 if ( count( $batch ) >= $this->getBatchSize() ) {
113 $this->doOperations( $tempRepo, $batch );
114 $i +
= count( $batch );
116 $this->output( "$i\n" );
120 if ( count( $batch ) ) {
121 $this->doOperations( $tempRepo, $batch );
122 $i +
= count( $batch );
124 $this->output( "$i done\n" );
126 // Apparently lots of stash files are not registered in the DB...
127 $dir = $tempRepo->getZonePath( 'public' );
128 $iterator = $tempRepo->getBackend()->getFileList( [ 'dir' => $dir, 'adviseStat' => 1 ] );
129 if ( $iterator === null ) {
130 $this->fatalError( "Could not get file listing." );
132 $this->output( "Deleting orphaned temp files...\n" );
133 if ( strpos( $dir, '/local-temp' ) === false ) {
134 $this->output( "Temp repo might be misconfigured. It points to directory: '$dir' \n" );
139 foreach ( $iterator as $file ) {
140 if ( wfTimestamp( TS_UNIX
, $tempRepo->getFileTimestamp( "$dir/$file" ) ) < $cutoff ) {
141 $batch[] = [ 'op' => 'delete', 'src' => "$dir/$file" ];
142 if ( count( $batch ) >= $this->getBatchSize() ) {
143 $this->doOperations( $tempRepo, $batch );
144 $i +
= count( $batch );
146 $this->output( "$i\n" );
150 if ( count( $batch ) ) {
151 $this->doOperations( $tempRepo, $batch );
152 $i +
= count( $batch );
154 $this->output( "$i done\n" );
157 protected function doOperations( FileRepo
$tempRepo, array $ops ) {
158 $status = $tempRepo->getBackend()->doQuickOperations( $ops );
159 if ( !$status->isOK() ) {
160 $this->error( $status );
165 // @codeCoverageIgnoreStart
166 $maintClass = CleanupUploadStash
::class;
167 require_once RUN_MAINTENANCE_IF_MAIN
;
168 // @codeCoverageIgnoreEnd