X-Git-Url: http://www.git.stargrave.org/?a=blobdiff_plain;f=lib%2FPublicInbox%2FLeiMirror.pm;h=bfbfd8434a2f5d51c5051df64ecb269235c4de3b;hb=b70592ae75f1251c2b116c8bb58a671534c650b4;hp=cc5ea1d2a5c0bbbe576ce624e66dbf85e8be1169;hpb=b3b025134c71d00fa26435aec1d2cb8c196103ac;p=public-inbox.git diff --git a/lib/PublicInbox/LeiMirror.pm b/lib/PublicInbox/LeiMirror.pm index cc5ea1d2..bfbfd843 100644 --- a/lib/PublicInbox/LeiMirror.pm +++ b/lib/PublicInbox/LeiMirror.pm @@ -19,6 +19,7 @@ use PublicInbox::Inbox; use PublicInbox::LeiCurl; use PublicInbox::OnDestroy; use Digest::SHA qw(sha256_hex sha1_hex); +use POSIX qw(strftime); our $LIVE; # pid => callback our $FGRP_TODO; # objstore -> [ fgrp mirror objects ] @@ -51,7 +52,7 @@ sub try_scrape { my $uri = URI->new($self->{src}); my $lei = $self->{lei}; my $curl = $self->{curl} //= PublicInbox::LeiCurl->new($lei) or return; - my $cmd = $curl->for_uri($lei, $uri, qw(-f --compressed)); + my $cmd = $curl->for_uri($lei, $uri, '--compressed'); my $opt = { 0 => $lei->{0}, 2 => $lei->{2} }; my $fh = popen_rd($cmd, undef, $opt); my $html = do { local $/; <$fh> } // die "read(curl $uri): $!"; @@ -151,7 +152,7 @@ sub _get_txt_start { # non-fatal my $f = (split(m!/!, $endpoint))[-1]; my $ft = File::Temp->new(TEMPLATE => "$f-XXXX", TMPDIR => 1); my $opt = { 0 => $lei->{0}, 1 => $lei->{1}, 2 => $lei->{2} }; - my $cmd = $self->{curl}->for_uri($lei, $uri, qw(-f --compressed -R -o), + my $cmd = $self->{curl}->for_uri($lei, $uri, qw(--compressed -R -o), $ft->filename); do_reap($self); $lei->qerr("# @$cmd"); @@ -180,13 +181,16 @@ sub _write_inbox_config { my $buf = delete($self->{'txt._/text/config/raw'}) // return; my $dst = $self->{cur_dst} // $self->{dst}; my $f = "$dst/inbox.config.example"; - open my $fh, '>', $f or die "open($f): $!"; - print $fh $buf or die "print: $!"; - chmod(0444 & ~umask, $fh) or die "chmod($f): $!"; my $mtime = delete $self->{'mtime._/text/config/raw'}; - $fh->flush or die "flush($f): $!"; - if (defined $mtime) { - utime($mtime, $mtime, $fh) or die "utime($f): $!"; + if (sysopen(my $fh, $f, O_CREAT|O_EXCL|O_WRONLY)) { + print $fh $buf or die "print: $!"; + chmod(0444 & ~umask, $fh) or die "chmod($f): $!"; + $fh->flush or die "flush($f): $!"; + if (defined $mtime) { + utime($mtime, $mtime, $fh) or die "utime($f): $!"; + } + } elsif (!$!{EEXIST}) { + die "open($f): $!"; } my $cfg = PublicInbox::Config->git_config_dump($f, $self->{lei}->{2}); my $ibx = $self->{ibx} = {}; @@ -200,22 +204,14 @@ sub _write_inbox_config { sub set_description ($) { my ($self) = @_; my $dst = $self->{cur_dst} // $self->{dst}; - my $f = "$dst/description"; - open my $fh, '+>>', $f or die "open($f): $!"; - seek($fh, 0, SEEK_SET) or die "seek($f): $!"; - my $d = do { local $/; <$fh> } // die "read($f): $!"; - chomp(my $orig = $d); + chomp(my $orig = PublicInbox::Git::try_cat("$dst/description")); + my $d = $orig; while (defined($d) && ($d =~ m!^\(\$INBOX_DIR/description missing\)! || $d =~ /^Unnamed repository/ || $d !~ /\S/)) { $d = delete($self->{'txt.description'}); } $d //= 'mirror of '.($self->{cur_src} // $self->{src}); - chomp $d; - return if $d eq $orig; - seek($fh, 0, SEEK_SET) or die "seek($f): $!"; - truncate($fh, 0) or die "truncate($f): $!"; - print $fh $d, "\n" or die "print($f): $!"; - close $fh or die "close($f): $!"; + atomic_write($dst, 'description', $d."\n") if $d ne $orig; } sub index_cloned_inbox { @@ -383,6 +379,7 @@ sub fgrp_fetch_all { push(@fetch, "-j$j") if $j; while (my ($osdir, $fgrpv) = each %$todo) { my $f = "$osdir/config"; + return if !keep_going($self); # clobber group from previous run atomically my $cmd = ['git', "--git-dir=$osdir", qw(config -f), @@ -431,20 +428,9 @@ sub forkgroup_prep { my $key = $self->{-key} // die 'BUG: no -key'; my $rn = substr(sha256_hex($key), 0, 16); if (!-d $self->{cur_dst} && !$self->{dry_run}) { - my $alt = File::Spec->rel2abs("$dir/objects"); PublicInbox::Import::init_bare($self->{cur_dst}); - my $o = "$self->{cur_dst}/objects"; - my $f = "$o/info/alternates"; - my $l = File::Spec->abs2rel($alt, File::Spec->rel2abs($o)); - open my $fh, '+>>', $f or die "open($f): $!"; - seek($fh, SEEK_SET, 0) or die "seek($f): $!"; - chomp(my @cur = <$fh>); - if (!grep(/\A\Q$l\E\z/, @cur)) { - say $fh $l or die "say($f): $!"; - } - close $fh or die "close($f): $!"; - $f = "$self->{cur_dst}/config"; - open $fh, '+>>', $f or die "open:($f): $!"; + my $f = "$self->{cur_dst}/config"; + open my $fh, '+>>', $f or die "open:($f): $!"; print $fh <{dry_run}) { + my $alt = File::Spec->rel2abs("$dir/objects"); + my $o = "$self->{cur_dst}/objects"; + my $f = "$o/info/alternates"; + my $l = File::Spec->abs2rel($alt, File::Spec->rel2abs($o)); + open my $fh, '+>>', $f or die "open($f): $!"; + seek($fh, SEEK_SET, 0) or die "seek($f): $!"; + chomp(my @cur = <$fh>); + if (!grep(/\A\Q$l\E\z/, @cur)) { + say $fh $l or die "say($f): $!"; + } + close $fh or die "close($f): $!"; + } bless { %$self, -osdir => $dir, -remote => $rn, -uri => $uri }, __PACKAGE__; } sub fp_done { - my ($self, $cb, @arg) = @_; + my ($self, $cmd, $cb, @arg) = @_; + if ($?) { + $self->{lei}->err("@$cmd failed (\$?=$?) (non-fatal)"); + $? = 0; # don't let it influence normal exit + } return if !keep_going($self); my $fh = delete $self->{-show_ref} // die 'BUG: no show-ref output'; seek($fh, SEEK_SET, 0) or die "seek(show_ref): $!"; @@ -488,8 +491,10 @@ sub cmp_fp_do { my $opt = { 2 => $self->{lei}->{2} }; open($opt->{1}, '+>', undef) or die "open(tmp): $!"; $self->{-show_ref} = $opt->{1}; - my $done = PublicInbox::OnDestroy->new($$, \&fp_done, $self, $cb, @arg); - start_cmd($self, $cmd, $opt, $done); + do_reap($self); + $self->{lei}->qerr("# @$cmd"); + $LIVE->{spawn($cmd, undef, $opt)} = [ \&fp_done, $self, $cmd, + $cb, @arg ]; } sub resume_fetch { @@ -533,6 +538,8 @@ sub clone_v1 { my $lei = $self->{lei}; my $curl = $self->{curl} //= PublicInbox::LeiCurl->new($lei) or return; my $uri = URI->new($self->{cur_src} // $self->{src}); + my $path = $uri->path; + $path =~ s!/*\z!! and $uri->path($path); defined($lei->{opt}->{epoch}) and die "$uri is a v1 inbox, --epoch is not supported\n"; $self->{-torsocks} //= $curl->torsocks($lei, $uri) or return; @@ -666,6 +673,30 @@ sub up_fp_done { push @{$self->{chg}->{fp_mismatch}}, $self->{-key}; } +sub atomic_write ($$$) { + my ($dn, $bn, $raw) = @_; + my $ft = File::Temp->new(DIR => $dn, TEMPLATE => "$bn-XXXX"); + print $ft $raw or die "print($ft): $!"; + $ft->flush or die "flush($ft): $!"; + ft_rename($ft, "$dn/$bn", 0666); +} + +sub run_next_puh { + my ($self) = @_; + my $puh = shift @{$self->{-puh_todo}} // return; + my $fini = PublicInbox::OnDestroy->new($$, \&run_next_puh, $self); + my $cmd = [ @$puh, ($self->{cur_dst} // $self->{dst}) ]; + my $opt = +{ map { $_ => $self->{lei}->{$_} } (0..2) }; + start_cmd($self, $cmd, undef, $opt, $fini); +} + +sub run_post_update_hooks { + my ($self) = @_; + my $puh = $self->{-puh} // return; + @{$self->{-puh_todo}} = @$puh; + run_next_puh($self); +} + # modifies the to-be-written manifest entry, and sets values from it, too sub update_ent { my ($self) = @_; @@ -681,7 +712,6 @@ sub update_ent { my $done = PublicInbox::OnDestroy->new($$, \&up_fp_done, $self); start_cmd($self, $cmd, $opt, $done); } - $new = $self->{-ent}->{head}; $cur = $self->{-local_manifest}->{$key}->{head} // "\0"; if (defined($new) && $new ne $cur) { @@ -699,6 +729,7 @@ sub update_ent { } if (my $symlinks = $self->{-ent}->{symlinks}) { my $top = File::Spec->rel2abs($self->{dst}); + push @{$self->{-new_symlinks}}, @$symlinks; for my $p (@$symlinks) { my $ln = "$top/$p"; $ln =~ tr!/!/!s; @@ -717,6 +748,14 @@ sub update_ent { symlink($tgt, $ln) or die "symlink($tgt, $ln): $!"; } } + if (defined(my $t = $self->{-ent}->{modified})) { + my ($dn, $bn) = ("$dst/info/web", 'last-modified'); + my $orig = PublicInbox::Git::try_cat("$dn/$bn"); + $t = strftime('%F %T', gmtime($t))." +0000\n"; + File::Path::mkpath($dn); + atomic_write($dn, $bn, $t) if $orig ne $t; + } + $new = $self->{-ent}->{owner} // return; $cur = $self->{-local_manifest}->{$key}->{owner} // "\0"; return if $cur eq $new; @@ -749,6 +788,7 @@ sub v1_done { # called via OnDestroy } eval { set_description($self) }; warn $@ if $@; + run_post_update_hooks($self); return if ($self->{-is_epoch} || $self->{lei}->{opt}->{'inbox-config'} ne 'always'); write_makefile($dst, 1); @@ -943,9 +983,13 @@ sub clone_all { # resolve multi-level references while ($m && defined($nxt = $m->{$x}->{reference})) { exists($todo->{$nxt}) or last; - die < 1000; -E: dependency loop detected (`$x' => `$nxt') + if (++$nr > 1000) { + $m->{$x}->{reference} = undef; + $m->{$nxt}->{reference} = undef; + warn < `$nxt'), breaking EOM + } $x = $nxt; } my $y = delete $todo->{$x} // next; # already done @@ -977,6 +1021,36 @@ sub dump_manifest ($$) { utime($mtime, $mtime, "$ft") or die "utime(..., $ft): $!"; } +sub dump_project_list ($$) { + my ($self, $m) = @_; + my $f = $self->{'-project-list'} // return; + my $old = PublicInbox::Git::try_cat($f); + my %new; + + open my $dh, '<', '.' or die "open(.): $!"; + chdir($self->{dst}) or die "chdir($self->{dst}): $!"; + my @local = grep { -e $_ ? ($new{$_} = undef) : 1 } split(/\n/s, $old); + chdir($dh) or die "chdir(restore): $!"; + + $new{substr($_, 1)} = 1 for keys %$m; # drop leading '/' + my @list = sort keys %new; + my @remote = grep { !defined($new{$_}) } @list; + my %lnk = map { substr($_, 1) => undef } @{$self->{-new_symlinks}}; + @remote = grep { !exists($lnk{$_}) } @remote; + + warn <{src}: +EOM + warn "\t", $_, "\n" for @remote; + warn <splitpath($f); + atomic_write($dn, $bn, join("\n", @list, '')); +} + # FIXME: this gets confused by single inbox instance w/ global manifest.js.gz sub try_manifest { my ($self) = @_; @@ -994,7 +1068,7 @@ sub try_manifest { delete $opt{TMPDIR}; } my $ft = File::Temp->new(TEMPLATE => '.manifest-XXXX', %opt); - my $cmd = $curl->for_uri($lei, $uri, qw(-f -R -o), $ft->filename); + my $cmd = $curl->for_uri($lei, $uri, qw(-R -o), $ft->filename); push(@$cmd, '-z', $manifest) if -f $manifest; my $mf_url = "$uri"; %opt = map { $_ => $lei->{$_} } (0..2); @@ -1014,6 +1088,7 @@ sub try_manifest { } local $self->{chg} = {}; local $self->{-local_manifest} = load_current_manifest($self); + local $self->{-new_symlinks} = []; my ($path_pfx, $n, $multi) = multi_inbox($self, \$path, $m); return $lei->child_error(1, $multi) if !ref($multi); my $v2 = delete $multi->{v2}; @@ -1074,8 +1149,7 @@ EOM my $mis = delete $self->{chg}->{fp_mismatch}; if ($mis) { my $t = (stat($ft))[9]; - require POSIX; - $t = POSIX::strftime('%Y-%m-%d %k:%M:%S %z', localtime($t)); + $t = strftime('%F %k:%M:%S %z', localtime($t)); warn <{dry_run} = 1 if $lei->{opt}->{'dry-run'}; umask($lei->{client_umask}) if defined $lei->{client_umask}; $self->{-initial_clone} = 1 if !-d $self->{dst}; + if (defined(my $puh = $lei->{opt}->{'post-update-hook'})) { + require Text::ParseWords; + for (@$puh) { + my $pfx = [ Text::ParseWords::shellwords($_) ]; + push @{$self->{-puh}}, $pfx; + } + } eval { my $ic = $lei->{opt}->{'inbox-config'} //= 'always'; $ic =~ /\A(?:v1|v2|always|never)\z/s or die <<""; --inbox-config must be one of `always', `v2', `v1', or `never' - # we support --objstore= and --manifest= with '' (empty string) - for my $default (qw(objstore manifest.js.gz)) { - my ($k) = (split(/\./, $default))[0]; + # we support these switches with '' (empty string). + # defaults match example conf distributed with grokmirror + my @pairs = qw(objstore objstore manifest manifest.js.gz + project-list projects.list); + while (@pairs) { + my ($k, $default) = splice(@pairs, 0, 2); my $v = $lei->{opt}->{$k} // next; $v = $default if $v eq ''; $v = "$self->{dst}/$v" if $v !~ m!\A\.{0,2}/!; $self->{"-$k"} = $v; } + local $LIVE = {}; local $TODO = {}; local $FGRP_TODO = {};