24require_once __DIR__ .
'/Maintenance.php';
34 parent::__construct();
35 $this->
addDescription(
'Sync one file backend with another using the journal' );
36 $this->
addOption(
'src',
'Name of backend to sync from',
true,
true );
37 $this->
addOption(
'dst',
'Name of destination backend to sync',
false,
true );
38 $this->
addOption(
'start',
'Starting journal ID',
false,
true );
39 $this->
addOption(
'end',
'Ending journal ID',
false,
true );
40 $this->
addOption(
'posdir',
'Directory to read/record journal positions',
false,
true );
41 $this->
addOption(
'posdump',
'Just dump current journal position into the position dir.' );
42 $this->
addOption(
'postime',
'For position dumps, get the ID at this time',
false,
true );
43 $this->
addOption(
'backoff',
'Stop at entries younger than this age (sec).',
false,
true );
44 $this->
addOption(
'verbose',
'Verbose mode',
false,
false,
'v' );
52 if ( $posDir !=
'' ) {
53 $posFile =
"$posDir/" . rawurlencode( $src->getDomainId() );
64 $id = (int)$src->getJournal()->getPositionAtTime( $this->
getOption(
'postime' ) );
65 $this->
output(
"Requested journal position is $id.\n" );
67 $id = (int)$src->getJournal()->getCurrentPosition();
68 $this->
output(
"Current journal position is $id.\n" );
70 if ( file_put_contents( $posFile, $id, LOCK_EX ) !==
false ) {
71 $this->
output(
"Saved journal position file.\n" );
73 $this->
output(
"Could not save journal position file.\n" );
88 if ( !$start && $posFile && is_dir( $posDir ) ) {
89 $start = is_file( $posFile )
90 ? (int)trim( file_get_contents( $posFile ) )
93 $startFromPosFile =
true;
95 $startFromPosFile =
false;
99 $time = time() - $this->
getOption(
'backoff', 0 );
100 $end = (int)$src->getJournal()->getPositionAtTime( $time );
105 $this->
output(
"Synchronizing backend '{$dst->getName()}' to '{$src->getName()}'...\n" );
106 $this->
output(
"Starting journal position is $start.\n" );
107 if ( is_finite( $end ) ) {
108 $this->
output(
"Ending journal position is $end.\n" );
112 $callback =
function ( $pos ) use ( $startFromPosFile, $posFile, $start ) {
113 if ( $startFromPosFile && $pos >= $start ) {
114 file_put_contents( $posFile, $pos, LOCK_EX );
119 $lastOKPos = $this->
syncBackends( $src, $dst, $start, $end, $callback );
122 if ( $startFromPosFile && $lastOKPos >= $start ) {
123 if ( file_put_contents( $posFile, $lastOKPos, LOCK_EX ) !==
false ) {
124 $this->
output(
"Updated journal position file.\n" );
126 $this->
output(
"Could not update journal position file.\n" );
130 if ( $lastOKPos ===
false ) {
132 $this->
output(
"No journal entries found.\n" );
134 $this->
output(
"No new journal entries found.\n" );
137 $this->
output(
"Stopped synchronization at journal position $lastOKPos.\n" );
162 if ( $start > $end ) {
163 $this->
fatalError(
"Error: given starting ID greater than ending ID." );
168 $limit = min( $this->
getBatchSize(), $end - $start + 1 );
169 $this->
output(
"Doing id $start to " . ( $start + $limit - 1 ) .
"...\n" );
171 $entries = $src->
getJournal()->getChangeEntries( $start, $limit, $next );
173 if ( $first && !count( $entries ) ) {
180 foreach ( $entries as $entry ) {
181 if ( $entry[
'op'] !==
'null' ) {
182 $pathsInBatch[$entry[
'path']] = 1;
184 $lastPosInBatch = $entry[
'id'];
187 $status = $this->
syncFileBatch( array_keys( $pathsInBatch ), $src, $dst );
188 if ( $status->isOK() ) {
189 $lastOKPos = max( $lastOKPos, $lastPosInBatch );
190 $callback( $lastOKPos );
192 $this->
error( print_r( $status->getErrorsArray(),
true ) );
197 $this->
output(
"End of journal entries.\n" );
199 }
while ( $start && $start <= $end );
213 $status = Status::newGood();
214 if ( !count( $paths ) ) {
226 if ( !$status->isOK() ) {
235 foreach ( $sPaths as $i => $sPath ) {
236 $dPath = $dPaths[$i];
237 $sExists = $src->
fileExists( [
'src' => $sPath,
'latest' => 1 ] );
238 if ( $sExists ===
true ) {
239 if ( $this->
filesAreSame( $src, $dst, $sPath, $dPath ) ) {
245 $this->
error(
"Unable to sync '$dPath': could not get local copy." );
246 $status->fatal(
'backend-fail-internal', $src->
getName() );
250 $fsFiles[] = $fsFile;
252 $status->merge( $dst->
prepare( [
253 'dir' => dirname( $dPath ),
'bypassReadOnly' => 1 ] ) );
254 if ( !$status->isOK() ) {
257 $ops[] = [
'op' =>
'store',
258 'src' => $fsFile->getPath(),
'dst' => $dPath,
'overwrite' => 1 ];
259 } elseif ( $sExists ===
false ) {
260 $ops[] = [
'op' =>
'delete',
'src' => $dPath,
'ignoreMissingSource' => 1 ];
262 $this->
error(
"Unable to sync '$dPath': could not stat file." );
263 $status->fatal(
'backend-fail-internal', $src->
getName() );
269 $t_start = microtime(
true );
271 if ( !$status->isOK() ) {
275 $elapsed_ms = floor( ( microtime(
true ) - $t_start ) * 1000 );
276 if ( $status->isOK() && $this->getOption(
'verbose' ) ) {
277 $this->
output(
"Synchronized these file(s) [{$elapsed_ms}ms]:\n" .
278 implode(
"\n", $dPaths ) .
"\n" );
293 '!^mwstore://([^/]+)!',
294 StringUtils::escapeRegexReplacement(
"mwstore://" . $backend->
getName() ),
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 exists.
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.
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