X-Git-Url: http://www.git.stargrave.org/?a=blobdiff_plain;f=lib%2FPublicInbox%2FLeiMirror.pm;h=9843d1a67e3ab964d8e08a78d583a5ec4afcf527;hb=f1817699f3f3feb964653eaf3586477abbf7947d;hp=b30cc519b99de1dae1a294ff056ca946df3f3fad;hpb=f6f581b57915e0bc07361d8a03da863b4117f910;p=public-inbox.git diff --git a/lib/PublicInbox/LeiMirror.pm b/lib/PublicInbox/LeiMirror.pm index b30cc519..9843d1a6 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,11 @@ 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"); + return if $self->{dry_run}; + $LIVE->{spawn($cmd, undef, $opt)} = [ \&fp_done, $self, $cmd, + $cb, @arg ]; } sub resume_fetch { @@ -533,6 +539,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 +674,14 @@ 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); +} + # modifies the to-be-written manifest entry, and sets values from it, too sub update_ent { my ($self) = @_; @@ -681,7 +697,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 +714,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 +733,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; @@ -943,9 +967,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 +1005,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 +1052,8 @@ 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); my $cerr = run_reap($lei, $cmd, \%opt); @@ -1002,6 +1061,10 @@ sub try_manifest { return try_scrape($self) if ($cerr >> 8) == 22; # 404 missing return $lei->child_error($cerr, "@$cmd failed"); } + + # bail out if curl -z/--timecond hit 304 Not Modified, $ft will be empty + return $lei->qerr("# $manifest unchanged") if -f $manifest && !-s $ft; + my $m = eval { decode_manifest($ft, $ft, $uri) }; if ($@) { warn $@; @@ -1009,6 +1072,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}; @@ -1069,8 +1133,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 <{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 = {};