26require_once __DIR__ .
'/Maintenance.php';
36 parent::__construct();
37 $this->
addDescription(
'Sync one file backend with another using the journal' );
38 $this->
addOption(
'src',
'Name of backend to sync from',
true,
true );
39 $this->
addOption(
'dst',
'Name of destination backend to sync',
false,
true );
40 $this->
addOption(
'start',
'Starting journal ID',
false,
true );
41 $this->
addOption(
'end',
'Ending journal ID',
false,
true );
42 $this->
addOption(
'posdir',
'Directory to read/record journal positions',
false,
true );
43 $this->
addOption(
'posdump',
'Just dump current journal position into the position dir.' );
44 $this->
addOption(
'postime',
'For position dumps, get the ID at this time',
false,
true );
45 $this->
addOption(
'backoff',
'Stop at entries younger than this age (sec).',
false,
true );
46 $this->
addOption(
'verbose',
'Verbose mode',
false,
false,
'v' );
51 $backendGroup = MediaWikiServices::getInstance()->getFileBackendGroup();
52 $src = $backendGroup->get( $this->
getOption(
'src' ) );
55 if ( $posDir !=
'' ) {
56 $posFile =
"$posDir/" . rawurlencode( $src->getDomainId() );
67 $id = (int)$src->getJournal()->getPositionAtTime( $this->
getOption(
'postime' ) );
68 $this->
output(
"Requested journal position is $id.\n" );
70 $id = (int)$src->getJournal()->getCurrentPosition();
71 $this->
output(
"Current journal position is $id.\n" );
73 if ( file_put_contents( $posFile, $id, LOCK_EX ) !==
false ) {
74 $this->
output(
"Saved journal position file.\n" );
76 $this->
output(
"Could not save journal position file.\n" );
88 $dst = $backendGroup->get( $this->
getOption(
'dst' ) );
91 if ( !$start && $posFile && is_dir( $posDir ) ) {
92 $start = is_file( $posFile )
93 ? (int)trim( file_get_contents( $posFile ) )
96 $startFromPosFile =
true;
98 $startFromPosFile =
false;
102 $time = time() - $this->
getOption(
'backoff', 0 );
103 $end = (int)$src->getJournal()->getPositionAtTime( $time );
108 $this->
output(
"Synchronizing backend '{$dst->getName()}' to '{$src->getName()}'...\n" );
109 $this->
output(
"Starting journal position is $start.\n" );
110 if ( is_finite( $end ) ) {
111 $this->
output(
"Ending journal position is $end.\n" );
115 $callback =
function ( $pos ) use ( $startFromPosFile, $posFile, $start ) {
116 if ( $startFromPosFile && $pos >= $start ) {
117 file_put_contents( $posFile, $pos, LOCK_EX );
122 $lastOKPos = $this->
syncBackends( $src, $dst, $start, $end, $callback );
125 if ( $startFromPosFile && $lastOKPos >= $start ) {
126 if ( file_put_contents( $posFile, $lastOKPos, LOCK_EX ) !==
false ) {
127 $this->
output(
"Updated journal position file.\n" );
129 $this->
output(
"Could not update journal position file.\n" );
133 if ( $lastOKPos ===
false ) {
135 $this->
output(
"No journal entries found.\n" );
137 $this->
output(
"No new journal entries found.\n" );
140 $this->
output(
"Stopped synchronization at journal position $lastOKPos.\n" );
165 if ( $start > $end ) {
166 $this->
fatalError(
"Error: given starting ID greater than ending ID." );
171 $limit = min( $this->
getBatchSize(), $end - $start + 1 );
172 $this->
output(
"Doing id $start to " . ( $start + $limit - 1 ) .
"...\n" );
174 $entries = $src->
getJournal()->getChangeEntries( $start, $limit, $next );
176 if ( $first && !count( $entries ) ) {
183 foreach ( $entries as $entry ) {
184 if ( $entry[
'op'] !==
'null' ) {
185 $pathsInBatch[$entry[
'path']] = 1;
187 $lastPosInBatch = $entry[
'id'];
190 $status = $this->
syncFileBatch( array_keys( $pathsInBatch ), $src, $dst );
191 if ( $status->isOK() ) {
192 $lastOKPos = max( $lastOKPos, $lastPosInBatch );
193 $callback( $lastOKPos );
195 $this->
error( print_r( $status->getErrorsArray(),
true ) );
200 $this->
output(
"End of journal entries.\n" );
202 }
while ( $start && $start <= $end );
216 $status = Status::newGood();
217 if ( !count( $paths ) ) {
229 if ( !$status->isOK() ) {
238 foreach ( $sPaths as $i => $sPath ) {
239 $dPath = $dPaths[$i];
240 $sExists = $src->
fileExists( [
'src' => $sPath,
'latest' => 1 ] );
241 if ( $sExists ===
true ) {
242 if ( $this->
filesAreSame( $src, $dst, $sPath, $dPath ) ) {
248 $this->
error(
"Unable to sync '$dPath': could not get local copy." );
249 $status->fatal(
'backend-fail-internal', $src->
getName() );
253 $fsFiles[] = $fsFile;
255 $status->merge( $dst->
prepare( [
256 'dir' => dirname( $dPath ),
'bypassReadOnly' => 1 ] ) );
257 if ( !$status->isOK() ) {
260 $ops[] = [
'op' =>
'store',
261 'src' => $fsFile->getPath(),
'dst' => $dPath,
'overwrite' => 1 ];
262 } elseif ( $sExists ===
false ) {
263 $ops[] = [
'op' =>
'delete',
'src' => $dPath,
'ignoreMissingSource' => 1 ];
265 $this->
error(
"Unable to sync '$dPath': could not stat file." );
266 $status->fatal(
'backend-fail-internal', $src->
getName() );
272 $t_start = microtime(
true );
274 if ( !$status->isOK() ) {
278 $elapsed_ms = floor( ( microtime(
true ) - $t_start ) * 1000 );
279 if ( $status->isOK() && $this->getOption(
'verbose' ) ) {
280 $this->
output(
"Synchronized these file(s) [{$elapsed_ms}ms]:\n" .
281 implode(
"\n", $dPaths ) .
"\n" );
296 '!^mwstore://([^/]+)!',
const RUN_MAINTENANCE_IF_MAIN
Base class for all file backend classes (including multi-write backends).
preloadFileStat(array $params)
Preload file stat information (concurrently if possible) into in-process cache.
getFileSha1Base36(array $params)
Get a SHA-1 hash of the content of the file at a storage path in the backend.
fileExists(array $params)
Check if a file exists at a storage path in the backend.
getScopedFileLocks(array $paths, $type, StatusValue $status, $timeout=0)
Lock the files at the given storage paths in the backend.
getFileSize(array $params)
Get the size (bytes) of a file at a storage path in the backend.
prepare(array $params)
Prepare a storage directory for usage.
doQuickOperations(array $ops, array $opts=[])
Perform a set of independent file operations on some files.
getLocalReference(array $params)
Returns a file system file, identical in content to the file at a storage path.
getName()
Get the unique backend name.
getJournal()
Get the file journal object for this backend.
Abstract maintenance class for quickly writing and churning out maintenance scripts with minimal effo...
error( $err, $die=0)
Throw an error to the user.
output( $out, $channel=null)
Throw some output to the user.
hasOption( $name)
Checks to see if a particular option was set.
getBatchSize()
Returns batch size.
addDescription( $text)
Set the description text.
addOption( $name, $description, $required=false, $withArg=false, $shortName=false, $multiOccurrence=false)
Add a parameter to the script.
getOption( $name, $default=null)
Get an option, or return the default.
setBatchSize( $s=0)
Set the batch size.
fatalError( $msg, $exitCode=1)
Output a message and terminate the current script.
static escapeRegexReplacement( $string)
Escape a string to make it suitable for inclusion in a preg_replace() replacement parameter.
Maintenance script that syncs one file backend to another based on the journal of later.
syncBackends(FileBackend $src, FileBackend $dst, $start, $end, Closure $callback)
Sync $dst backend to $src backend based on the $src logs given after $start.
__construct()
Default constructor.
syncFileBatch(array $paths, FileBackend $src, FileBackend $dst)
Sync particular files of backend $src to the corresponding $dst backend files.
filesAreSame(FileBackend $src, FileBackend $dst, $sPath, $dPath)
execute()
Do the actual work.
replaceNamePaths( $paths, FileBackend $backend)
Substitute the backend name of storage paths with that of a given one.
while(( $__line=Maintenance::readconsole()) !==false) print