summaryrefslogtreecommitdiff
path: root/drpmsync
diff options
context:
space:
mode:
Diffstat (limited to 'drpmsync')
-rwxr-xr-xdrpmsync3968
1 files changed, 3968 insertions, 0 deletions
diff --git a/drpmsync b/drpmsync
new file mode 100755
index 0000000..f629aad
--- /dev/null
+++ b/drpmsync
@@ -0,0 +1,3968 @@
+#!/usr/bin/perl -w
+
+#
+# Copyright (c) 2005 Michael Schroeder (mls@suse.de)
+#
+# This program is licensed under the BSD license, read LICENSE.BSD
+# for further information
+#
+
+use Socket;
+use Fcntl qw(:DEFAULT :flock);
+use POSIX;
+use Digest::MD5 ();
+use Net::Domain ();
+use bytes;
+my $have_zlib;
+my $have_time_hires;
+eval {
+ require Compress::Zlib;
+ $have_zlib = 1;
+};
+eval {
+ require Time::HiRes;
+ $have_time_hires = 1 if defined &Time::HiRes::gettimeofday;
+};
+use strict;
+
+$SIG{'PIPE'} = 'IGNORE';
+
+#######################################################################
+# Common code user for Client and Server
+#######################################################################
+
+my $makedeltarpm = 'makedeltarpm';
+my $combinedeltarpm = 'combinedeltarpm';
+my $applydeltarpm = 'applydeltarpm';
+my $fragiso = 'fragiso';
+
+sub stdinopen {
+ local *F = shift;
+ local *I = shift;
+ my $pid;
+ while (1) {
+ $pid = open(F, '-|');
+ last if defined $pid;
+ return if $! != POSIX::EAGAIN;
+ sleep(5);
+ }
+ return 1 if $pid;
+ if (fileno(I) != 0) {
+ open(STDIN, "<&I") || die("dup stdin: $!\n");
+ close(I);
+ }
+ exec @_;
+ die("$_[0]: $!\n");
+}
+
+sub tmpopen {
+ local *F = shift;
+ my $tmpdir = shift;
+
+ my $tries = 0;
+ for ($tries = 0; $tries < 100; $tries++) {
+ if (sysopen(F, "$tmpdir/drpmsync.$$.$tries", POSIX::O_RDWR|POSIX::O_CREAT|POSIX::O_EXCL, 0600)) {
+ unlink("$tmpdir/drpmsync.$$.$tries");
+ return 1;
+ }
+ }
+ return;
+}
+
+# cannot use IPC::Open3, sigh...
+sub runprg {
+ return runprg_job(undef, @_);
+}
+
+sub runprg_job {
+ my ($job, $if, $of, @prg) = @_;
+ local (*O, *OW, *E, *EW);
+ if (!$of) {
+ pipe(O, OW) || die("pipe: $!\n");
+ }
+ pipe(E, EW) || die("pipe: $!\n");
+ my $pid;
+ while (1) {
+ $pid = fork();
+ last if defined $pid;
+ return ('', "runprg: fork: $!") if $! != POSIX::EAGAIN;
+ sleep(5);
+ }
+ if ($pid == 0) {
+ if ($of) {
+ *OW = $of;
+ } else {
+ close(O);
+ }
+ close(E);
+ if (fileno(OW) != 1) {
+ open(STDOUT, ">&OW") || die("dup stdout: $!\n");
+ close(OW);
+ }
+ if (fileno(EW) != 2) {
+ open(STDERR, ">&EW") || die("dup stderr: $!\n");
+ close(EW);
+ }
+ if (defined($if)) {
+ local (*I) = $if;
+ if (fileno(I) != 0) {
+ open(STDIN, "<&I") || die("dup stdin: $!\n");
+ close(I);
+ }
+ } else {
+ open(STDIN, "</dev/null");
+ }
+ exec @prg;
+ die("$prg[0]: $!\n");
+ }
+ close(OW) unless $of;
+ close(EW);
+
+ if ($job) {
+ $job->{'PID'} = $pid;
+ $job->{'E'} = *E;
+ delete $job->{'O'};
+ $job->{'O'} = *O unless $of;
+ return $job;
+ }
+ $job = {};
+ $job->{'PID'} = $pid;
+ $job->{'E'} = *E;
+ $job->{'O'} = *O unless $of;
+ return runprg_finish($job);
+}
+
+sub runprg_finish {
+ my ($job) = @_;
+
+ die("runprg_finish: no job running\n") unless $job && $job->{'PID'};
+ my ($out, $err) = ('', '');
+ my $pid = $job->{'PID'};
+ local *E = $job->{'E'};
+ local *O;
+ my $of = 1;
+ if (exists $job->{'O'}) {
+ $of = undef;
+ *O = $job->{'O'};
+ }
+ delete $job->{'PID'};
+ delete $job->{'O'};
+ delete $job->{'E'};
+ my $rin = '';
+ my $efd = fileno(E);
+ my $ofd;
+ if (!$of) {
+ $ofd = fileno(O);
+ vec($rin, $ofd, 1) = 1;
+ }
+ vec($rin, $efd, 1) = 1;
+ my $nfound;
+ my $rout;
+ my $openfds = $of ? 2 : 3;
+ while ($openfds) {
+ $nfound = select($rout = $rin, undef, undef, undef);
+ if (!defined($nfound)) {
+ $err .= "select: $!";
+ close(O) if $openfds & 1;
+ close(E) if $openfds & 2;
+ last;
+ }
+ if (!$of && vec($rout, $ofd, 1)) {
+ if (!sysread(O, $out, 4096, length($out))) {
+ vec($rin, $ofd, 1) = 0;
+ close(O);
+ $openfds &= ~1;
+ }
+ }
+ if (vec($rout, $efd, 1)) {
+ if (!sysread(E, $err, 4096, length($err))) {
+ vec($rin, $efd, 1) = 0;
+ close(E);
+ $openfds &= ~2;
+ }
+ }
+ }
+ while(1) {
+ if (waitpid($pid, 0) == $pid) {
+ $err = "Error $?" if $? && $err eq '';
+ last;
+ }
+ if ($! != POSIX::EINTR) {
+ $err = "waitpid: $!";
+ last;
+ }
+ }
+ return ($out, $err);
+}
+
+sub cprpm {
+ local *F = shift;
+ my ($wri, $verify, $ml) = @_;
+
+ local *WF;
+ *WF = $wri if $wri;
+
+ my $ctx;
+ $ctx = Digest::MD5->new if $verify;
+
+ my $buf = '';
+ my $l;
+ while (length($buf) < 96 + 16) {
+ $l = sysread(F, $buf, defined($ml) && $ml < 4096 ? $ml : 4096, length($buf));
+ return "read error" unless $l;
+ $ml -= $l if defined $ml;
+ }
+ my ($magic, $sigtype) = unpack('N@78n', $buf);
+ return "not a rpm (bad magic of header type" unless $magic == 0xedabeedb && $sigtype == 5;
+ my ($headmagic, $cnt, $cntdata) = unpack('@96N@104NN', $buf);
+ return "not a rpm (bad sig header magic)" unless $headmagic == 0x8eade801;
+ my $hlen = 96 + 16 + $cnt * 16 + $cntdata;
+ $hlen = ($hlen + 7) & ~7;
+ while (length($buf) < $hlen) {
+ $l = sysread(F, $buf, defined($ml) && $ml < 4096 ? $ml : 4096, length($buf));
+ return "read error" unless $l;
+ $ml -= $l if defined $ml;
+ }
+ my $lmd5 = Digest::MD5::md5_hex(substr($buf, 0, $hlen));
+ my $idxarea = substr($buf, 96 + 16, $cnt * 16);
+ if (!($idxarea =~ /\A(?:.{16})*\000\000\003\354\000\000\000\007(....)\000\000\000\020/s)) {
+ return "no md5 signature header";
+ }
+ my $md5off = unpack('N', $1);
+ return "bad md5 offset" if $md5off >= $cntdata;
+ $md5off += 96 + 16 + $cnt * 16;
+ my $hmd5 = unpack("\@${md5off}H32", $buf);
+ return "write error" if $wri && (syswrite(WF, substr($buf, 0, $hlen)) || 0) != $hlen;
+ $buf = substr($buf, $hlen);
+ while (length($buf) < 16) {
+ $l = sysread(F, $buf, defined($ml) && $ml < 4096 ? $ml : 4096, length($buf));
+ return "read error" unless $l;
+ $ml -= $l if defined $ml;
+ }
+ ($headmagic, $cnt, $cntdata) = unpack('N@8NN', $buf);
+ return "not a rpm (bad header magic)" unless $headmagic == 0x8eade801;
+ $hlen = 16 + $cnt * 16;
+ while (length($buf) < $hlen) {
+ $l = sysread(F, $buf, defined($ml) && $ml < 4096 ? $ml : 4096, length($buf));
+ return "read error" unless $l;
+ $ml -= $l if defined $ml;
+ }
+ my ($nameoff, $archoff, $btoff);
+ $idxarea = substr($buf, 0, $hlen);
+ my $srctype = '';
+ if (!($idxarea =~ /\A(?:.{16})*\000\000\004\024/s)) {
+ if (($idxarea =~ /\A(?:.{16})*\000\000\004[\033\034]/s)) {
+ $srctype = 'nosrc';
+ } else {
+ $srctype = 'src';
+ }
+ }
+ if (($idxarea =~ /\A(?:.{16})*\000\000\003\350\000\000\000\006(....)\000\000\000\001/s)) {
+ $nameoff = unpack('N', $1);
+ }
+ if (($idxarea =~ /\A(?:.{16})*\000\000\003\376\000\000\000\006(....)\000\000\000\001/s)) {
+ $archoff = unpack('N', $1);
+ }
+ if (($idxarea =~ /\A(?:.{16})*\000\000\003\356\000\000\000\004(....)\000\000\000\001/s)) {
+ $btoff = unpack('N', $1);
+ }
+ return "rpm contains no name tag" unless defined $nameoff;
+ return "rpm contains no arch tag" unless defined $archoff;
+ return "rpm contains no build time" unless defined $btoff;
+ return "bad name/arch offset" if $nameoff >= $cntdata || $archoff >= $cntdata || $btoff + 3 >= $cntdata;
+ $ctx->add(substr($buf, 0, $hlen)) if $verify;
+ return "write error" if $wri && (syswrite(WF, substr($buf, 0, $hlen)) || 0) != $hlen;
+ $buf = substr($buf, $hlen);
+ my $maxoff = $nameoff > $archoff ? $nameoff : $archoff;
+ $maxoff += 1024; # should be enough
+ $maxoff = $btoff + 4 if $btoff + 4 > $maxoff;
+ $maxoff = $cntdata if $maxoff > $cntdata;
+ while (length($buf) < $maxoff) {
+ $l = sysread(F, $buf, defined($ml) && $ml < 4096 ? $ml : 4096, length($buf));
+ return "read error" unless $l;
+ $ml -= $l if defined $ml;
+ }
+ my $name = unpack("\@${nameoff}Z*", $buf);
+ my $arch = unpack("\@${archoff}Z*", $buf);
+ my $bt = unpack("\@${btoff}H8", $buf);
+ if ($verify || $wri) {
+ $ctx->add($buf) if $verify;
+ return "write error" if $wri && (syswrite(WF, $buf) || 0) != length($buf);
+ while(1) {
+ last if defined($ml) && $ml == 0;
+ $l = sysread(F, $buf, defined($ml) && $ml < 8192 ? $ml : 8192);
+ last if !$l && !defined($ml);
+ return "read error" unless $l;
+ $ml -= $l if defined $ml;
+ $ctx->add($buf) if $verify;
+ return "write error" if $wri && (syswrite(WF, $buf) || 0) != $l;
+ }
+ if ($verify) {
+ my $rmd5 = $ctx->hexdigest;
+ return "rpm checksum error ($rmd5 != $hmd5)" if $rmd5 ne $hmd5;
+ }
+ }
+ $name = "unknown" if $name =~ /[\000-\040\/]/;
+ $arch = "unknown" if $arch =~ /[\000-\040\/]/;
+ $arch = $srctype if $srctype;
+ return ("$lmd5$hmd5", $bt, "$name.$arch");
+}
+
+sub cpfile {
+ local *F = shift;
+ my ($wri) = @_;
+
+ local *WF;
+ *WF = $wri if $wri;
+ my $ctx;
+ $ctx = Digest::MD5->new;
+ my ($buf, $l);
+ while(1) {
+ $l = sysread(F, $buf, 8192);
+ last if !$l;
+ die("cpfile read error\n") unless $l;
+ $ctx->add($buf);
+ die("cpfile write error\n") if $wri && (syswrite(WF, $buf) || 0) != $l;
+ }
+ return ($ctx->hexdigest);
+}
+
+sub rpminfo_f {
+ my ($fd, $rpm) = @_;
+ my @info = cprpm($fd);
+ if (@info == 1) {
+ warn("$rpm: $info[0]\n");
+ return ();
+ }
+ return @info;
+}
+
+sub rpminfo {
+ my $rpm = shift;
+ local *RPM;
+ if (!open(RPM, '<', $rpm)) {
+ warn("$rpm: $!\n");
+ return ();
+ }
+ my @ret = rpminfo_f(*RPM, $rpm);
+ close RPM;
+ return @ret;
+}
+
+sub fileinfo_f {
+ local (*F) = shift;
+
+ my $ctx = Digest::MD5->new;
+ $ctx->addfile(*F);
+ return $ctx->hexdigest;
+}
+
+sub fileinfo {
+ my $fn = shift;
+ local *FN;
+ if (!open(FN, '<', $fn)) {
+ warn("$fn: $!\n");
+ return ();
+ }
+ my @ret = fileinfo_f(*FN, $fn);
+ close FN;
+ return @ret;
+}
+
+sub linkinfo {
+ my $fn = shift;
+ my $fnc = readlink($fn);
+ if (!defined($fnc)) {
+ warn("$fn: $!\n");
+ return ();
+ }
+ return Digest::MD5::md5_hex($fnc);
+}
+
+my @filter_comp;
+my @filter_arch_comp;
+
+sub run_filter {
+ my @x = @_;
+
+ my @f = @filter_comp;
+ my @r;
+ while (@f) {
+ my ($ft, $fre) = splice(@f, 0, 3);
+ my @xx = grep {/$fre/} @x;
+ my %xx = map {$_ => 1} @xx;
+ push @r, @xx if $ft;
+ @x = grep {!$xx{$_}} @x;
+ }
+ return (@r, @x);
+}
+
+sub run_filter_one {
+ my ($n) = @_;
+ my @f = @filter_comp;
+ while (@f) {
+ my ($ft, $fre) = splice(@f, 0, 3);
+ if ($ft) {
+ return 1 if $n =~ /$fre/;
+ } else {
+ return if $n =~ /$fre/;
+ }
+ }
+ return 1;
+}
+
+sub compile_filter {
+ my @rules = @_;
+
+ my @comp = ();
+ for my $rule (@rules) {
+ die("bad filter type, must be '+' or '-'\n") unless $rule =~ /^([+-])(.*)$/;
+ my $type = $1 eq '+' ? 1 : 0;
+ my $match = $2;
+ my $anchored = $match =~ s/^\///;
+ my @match = split(/\[(\^?.(?:\\.|[^]])*)\]/, $match, -1);
+ my $i = 0;
+ for (@match) {
+ $i = 1 - $i;
+ if (!$i) {
+ s/([^-\^a-zA-Z0-9])/\\$1/g;
+ s/\\\\(\\[]\\\]]|-)/"\\".substr($1, -1)/ge;
+ $_ = "[$_]";
+ next;
+ }
+ $_ = "\Q$_\E";
+ s/\\\*\\\*/.*/g;
+ s/\\\*/[^\/]*/g;
+ s/\\\?/[^\/]/g;
+ }
+ $match = join('', @match);
+ if ($anchored) {
+ $match = "^$match";
+ } else {
+ $match = "(?:^|\/)$match";
+ }
+ $match .= '\/?' if $match !~ /\/$/;
+ $match .= '$';
+ eval {
+ push @comp, $type, qr/$match/s, $rule;
+ };
+ die("bad filter rule: $rule\n") if $@;
+ }
+ return @comp;
+}
+
+sub filelist_apply_filter {
+ my ($flp) = @_;
+ return unless @filter_comp;
+ my @ns = ();
+ my $x;
+ for my $e (@$flp) {
+ if (defined($x)) {
+ next if substr($e->[0], 0, length($x)) eq $x;
+ undef $x;
+ }
+ if (@$e == 3) {
+ if (!run_filter_one("$e->[0]/")) {
+ $x = "$e->[0]/";
+ next;
+ }
+ } else {
+ next if !run_filter_one("$e->[0]");
+ }
+ push @ns, $e;
+ }
+ @$flp = @ns;
+}
+
+sub filelist_apply_filter_arch {
+ my ($flp) = @_;
+ return unless @filter_arch_comp;
+ my %filtered;
+ my @filter_comp_save = @filter_comp;
+ @filter_comp = @filter_arch_comp;
+ my @ns = ();
+ for my $e (@$flp) {
+ if (@$e > 5 && !run_filter_one((split('\.', $e->[5]))[-1])) {
+ if ($e->[0] =~ /(.*)\.rpm$/) {
+ $filtered{"$1.changes"} = 1;
+ $filtered{"$1-MD5SUMS.meta"} = 1;
+ $filtered{"$1-MD5SUMS.srcdir"} = 1;
+ }
+ next;
+ }
+ push @ns, $e;
+ }
+ @filter_comp = @filter_comp_save;
+ @$flp = @ns;
+ if (%filtered) {
+ # second pass to remove meta files
+ @ns = ();
+ for my $e (@$flp) {
+ next if @$e == 4 && $filtered{$e->[0]};
+ push @ns, $e;
+ }
+ @$flp = @ns;
+ }
+}
+
+sub filelist_exclude_drpmsync {
+ my ($flp) = @_;
+ @$flp = grep {$_->[0] =~ /(?:^|\/)drpmsync\//s || (@$_ == 3 && $_->[0] =~ /(?:^|\/)drpmsync$/s)} @$flp;
+}
+
+my @files;
+my %cache;
+my $cachehits = 0;
+my $cachemisses = 0;
+
+sub findfiles {
+ my ($bdir, $dir, $keepdrpmdir, $norecurse) = @_;
+
+ local *DH;
+ if (!opendir(DH, "$bdir$dir")) {
+ warn("$dir: $!\n");
+ return;
+ }
+ my @ents = sort readdir(DH);
+ closedir(DH);
+ $bdir .= '/' if $dir eq '';
+ $dir .= '/' if $dir ne '';
+ if ($dir ne '' && grep {$_ eq 'drpmsync'} @ents) {
+ readcache("$bdir${dir}drpmsync/cache") if -f "$bdir${dir}drpmsync/cache";
+ }
+ my %fents;
+ if (@filter_comp) {
+ @ents = grep {$_ ne '.' && $_ ne '..'} @ents;
+ my @fents = run_filter(map {"$dir$_"} @ents);
+ if (@fents != @ents) {
+ %fents = map {("$dir$_" => 1)} @ents;
+ delete $fents{$_} for @fents;
+ }
+ }
+ for my $ent (@ents) {
+ next if $ent eq '.' || $ent eq '..';
+ next if $ent =~ /\.new\d*$/;
+ my @s = lstat "$bdir$dir$ent";
+ if (!@s) {
+ warn("$bdir$dir$ent: $!\n");
+ next;
+ }
+ next unless -l _ || -d _ || -f _;
+ my $id = "$s[9]/$s[7]/$s[1]";
+ my $mode = -l _ ? 0x2000 : -f _ ? 0x1000 : 0x0000;
+ $mode |= $s[2] & 07777;
+ my @data = ($id, sprintf("%04x%08x", $mode, $s[9]));
+ if (-d _) {
+ next if $ent eq 'drpmsync' && ($dir eq '' || !$keepdrpmdir);
+ next if @filter_comp && !run_filter_one("$dir$ent/");
+ push @files, [ "$dir$ent", @data ];
+ next if $norecurse;
+ findfiles($bdir, "$dir$ent", $keepdrpmdir);
+ } else {
+ next if @filter_comp && $fents{"$dir$ent"};
+ my @xdata;
+ if ($cache{$id}) {
+ @xdata = @{$cache{$id}};
+ if (@xdata == ($ent =~ /\.[sr]pm$/) ? 3 : 1) {
+ $cachehits++;
+ push @files, [ "$dir$ent", @data, @xdata ];
+ next;
+ }
+ }
+ # print "miss $id ($ent)\n";
+ $cachemisses++;
+ if (-l _) {
+ @xdata = linkinfo("$bdir$dir$ent");
+ next if !@xdata;
+ $cache{$id} = \@xdata;
+ push @files, [ "$dir$ent", @data, @xdata ];
+ next;
+ }
+ local *F;
+ if (!open(F, '<', "$bdir$dir$ent")) {
+ warn("$bdir$dir$ent: $!\n");
+ next;
+ }
+ @s = stat F;
+ if (!@s || ! -f _) {
+ warn("$bdir$dir$ent: $!\n");
+ next;
+ }
+ $id = "$s[9]/$s[7]/$s[1]";
+ @data = ($id, sprintf("1%03x%08x", ($s[2] & 07777), $s[9]));
+ if ($ent =~ /\.[sr]pm$/) {
+ @xdata = rpminfo_f(*F, "$bdir$dir$ent");
+ } else {
+ @xdata = fileinfo_f(*F, "$bdir$dir$ent");
+ }
+ close F;
+ next if !@xdata;
+ $cache{$id} = \@xdata;
+ push @files, [ "$dir$ent", @data, @xdata ];
+ }
+ }
+}
+
+sub readcache {
+ my $cf = shift;
+
+ local *CF;
+ open(CF, '<', $cf) || return;
+ while(<CF>) {
+ chomp;
+ my @s = split(' ');
+ next unless @s == 4 || @s == 2;
+ my $s = shift @s;
+ $cache{$s} = \@s;
+ }
+ close CF;
+}
+
+sub writecache {
+ my $cf = shift;
+
+ local *CF;
+ open(CF, '>', "$cf.new") || die("$cf.new: $!\n");
+ for (@files) {
+ next if @$_ < 4; # no need to cache dirs
+ if (@$_ > 5) {
+ print CF "$_->[1] $_->[3] $_->[4] $_->[5]\n";
+ } else {
+ print CF "$_->[1] $_->[3]\n";
+ }
+ }
+ close CF;
+ rename("$cf.new", $cf) || die("rename $cf.new $cf: $!\n");
+}
+
+#######################################################################
+# Server stuff
+#######################################################################
+
+sub escape {
+ my $x = shift;
+ $x =~ s/\&/&amp;/g;
+ $x =~ s/\</&lt;/g;
+ $x =~ s/\>/&gt;/g;
+ $x =~ s/\"/&quot;/g;
+ return $x;
+}
+
+sub aescape {
+ my $x = shift;
+ $x =~ s/([\000-\040<>\"#&\+=%[\177-\377])/sprintf("%%%02X",ord($1))/ge;
+ return $x;
+}
+
+sub readfile {
+ my $fn = shift;
+ local *FN;
+ open(FN, '<', $fn) || return ('', "$fn: $!");
+ my $out = '';
+ while ((sysread(FN, $out, 8192, length($out)) || 0) == 8192) {}
+ close FN;
+ return ($out, '');
+}
+
+# server config
+my %trees;
+my %chld;
+my $standalone;
+my $sendlogid;
+my $servername;
+my $serveraddr;
+my $serveruser;
+my $servergroup;
+my $serverlog;
+my $maxclients = 10;
+my $servertmp = '/var/tmp';
+my $serverpidfile;
+
+sub readconfig_server {
+ my $cf = shift;
+
+ my @allow;
+ my @deny;
+ my $no_combine;
+ my $log;
+ my $slog;
+ my $deltadirs;
+ my $maxdeltasize;
+ my $maxdeltasizeabs;
+ my @denymsg;
+ local *CF;
+ die("config not set\n") unless $cf;
+ open(CF, '<', $cf) || die("$cf: $!\n");
+ while(<CF>) {
+ chomp;
+ s/^\s+//;
+ s/\s+$//;
+ next if $_ eq '' || /^#/;
+ my @s = split(' ', $_);
+ my $s0 = lc($s[0]);
+ $s0 =~ s/:$//;
+ my $s1 = @s > 1 ? $s[1] : undef;
+ shift @s;
+ if ($s0 eq 'allow' || $s0 eq 'deny') {
+ for (@s) {
+ if (/^\/(.*)\/$/) {
+ $_ = $1;
+ eval { local $::SIG{'__DIE__'}; "" =~ /^$_$/; };
+ die("$s0: bad regexp: $_\n") if $@;
+ } else {
+ s/([^a-zA-Z0-9*])/\\$1/g;
+ s/\*/.*/g;
+ }
+ }
+ if ($s0 eq 'allow') {
+ @allow = @s;
+ } else {
+ @deny = @s;
+ }
+ } elsif ($s0 eq 'denymsg') {
+ if (!@s) {
+ @denymsg = ();
+ next;
+ }
+ if ($s1 =~ /^\/(.*)\/$/) {
+ $s1 = $1;
+ eval { local $::SIG{'__DIE__'}; "" =~ /^$s1$/; };
+ die("$s0: bad regexp: $s1\n") if $@;
+ } else {
+ $s1 =~ s/([^a-zA-Z0-9*])/\\$1/g;
+ $s1 =~ s/\*/.*/g;
+ }
+ shift @s;
+ push @denymsg, [ $s1, join(' ', @s) ];
+ } elsif ($s0 eq 'no_combine') {
+ $no_combine = ($s1 && $s1 =~ /true/i);
+ } elsif ($s0 eq 'log') {
+ $log = $s1;
+ } elsif ($s0 eq 'serverlog') {
+ $slog = $s1;
+ } elsif ($s0 eq 'deltadirs') {
+ $deltadirs = $s1;
+ } elsif ($s0 eq 'deltarpmpath') {
+ my $p = defined($s1) ? "$s1/" : '';
+ $makedeltarpm = "${p}makedeltarpm";
+ $combinedeltarpm = "${p}combinedeltarpm";
+ $fragiso = "${p}fragiso";
+ } elsif ($s0 eq 'maxclients') {
+ $maxclients = $s1 || 1;
+ } elsif ($s0 eq 'servername') {
+ $servername = $s1;
+ } elsif ($s0 eq 'serveraddr') {
+ $serveraddr = $s1;
+ } elsif ($s0 eq 'serveruser') {
+ $serveruser = $s1;
+ } elsif ($s0 eq 'servergroup') {
+ $servergroup = $s1;
+ } elsif ($s0 eq 'pidfile') {
+ $serverpidfile = $s1;
+ } elsif ($s0 eq 'maxdeltasize') {
+ $maxdeltasize = $s1;
+ } elsif ($s0 eq 'maxdeltasizeabs') {
+ $maxdeltasizeabs = $s1;
+ } elsif ($s0 eq 'tree') {
+ die("tree: two arguments required\n") if @s != 2;
+ $trees{$s[0]} = { 'allow' => [ @allow ],
+ 'deny' => [ @deny ],
+ 'denymsg' => [ @denymsg ],
+ 'no_combine' => $no_combine,
+ 'maxdeltasize' => $maxdeltasize,
+ 'maxdeltasizeabs' => $maxdeltasizeabs,
+ 'deltadirs' => $deltadirs,
+ 'log' => $log,
+ 'root' => $s[1],
+ 'id' => $s[0]
+ };
+ } else {
+ die("$cf: unknown configuration parameter: $s0\n");
+ }
+ }
+ close CF;
+ $serverlog = $slog;
+}
+
+sub gethead {
+ my $h = shift;
+ my $t = shift;
+
+ my ($field, $data);
+ $field = undef;
+ for (split(/[\r\n]+/, $t)) {
+ next if $_ eq '';
+ if (/^[ \t]/) {
+ next unless defined $field;
+ s/^\s*/ /;
+ $h->{$field} .= $_;
+ } else {
+ ($field, $data) = split(/\s*:\s*/, $_, 2);
+ $field =~ tr/A-Z/a-z/;
+ if ($h->{$field} && $h->{$field} ne '') {
+ $h->{$field} = $h->{$field}.','.$data;
+ } else {
+ $h->{$field} = $data;
+ }
+ }
+ }
+}
+
+sub serverlog {
+ my $id = shift;
+ my $str = shift;
+ return unless $serverlog;
+ $str =~ s/\n$//s;
+ my @lt = localtime(time());
+ $lt[5] += 1900;
+ $lt[4] += 1;
+ $id = defined($id) ? " [$id]" : '';
+ printf SERVERLOG "%04d-%02d-%02d %02d:%02d:%02d%s: %s\n", @lt[5,4,3,2,1,0], $id, $str;
+}
+
+sub serverdetach {
+ my $pid;
+ local (*SR, *SW);
+ pipe(SR, SW) || die("setsid pipe: $!\n");
+ while (1) {
+ $pid = fork();
+ last if defined $pid;
+ die("fork: $!") if $! != POSIX::EAGAIN;
+ sleep(5);
+ }
+ if ($pid) {
+ close SW;
+ my $dummy = '';
+ sysread(SR, $dummy, 1);
+ exit(0);
+ }
+ POSIX::setsid();
+ close SW;
+ close SR;
+ open(STDIN, "</dev/null");
+ open(STDOUT, ">/dev/null");
+ open(STDERR, ">/dev/null");
+}
+
+sub startserver {
+ my $config = shift;
+ my $nobg = shift;
+
+ # not called from web server, go for standalone
+ $standalone = 1;
+ readconfig_server($config);
+ unlink($serverpidfile) if $serverpidfile;
+ if ($serverlog && !open(SERVERLOG, '>>', $serverlog)) {
+ my $err = "$serverlog: $!\n";
+ undef $serverlog; # do not log in die() hook
+ die($err);
+ }
+ serverlog(undef, "server start");
+ $servername = '' unless defined $servername;
+ $servername = Net::Domain::hostfqdn().$servername if $servername eq '' || $servername =~ /^:\d+$/;
+ die("need servername for standalone mode\n") unless $servername;
+ if (defined($serveruser) && $serveruser =~ /[^\d]/) {
+ my $uid = getpwnam($serveruser);
+ die("$serveruser: unknown user\n") unless defined $uid;
+ $serveruser = $uid;
+ }
+ if (defined($servergroup) && $servergroup =~ /[^\d]/) {
+ my $gid = getgrnam($servergroup);
+ die("$servergroup: unknown group\n") unless defined $gid;
+ $servergroup = $gid;
+ }
+ my ($servern, $servera, $serverp);
+ ($servern, $serverp) = $servername =~ /^([^\/]+?)(?::(\d+))?$/;
+ die("bad servername: $servername\n") unless $servern;
+ $serverp ||= 80;
+ $servera = INADDR_ANY;
+ if ($serveraddr) {
+ $servera = inet_aton($serveraddr) || die("could not resolv $serveraddr\n");
+ }
+ my $tcpproto = getprotobyname('tcp');
+ socket(MS , PF_INET, SOCK_STREAM, $tcpproto) || die("socket: $!\n");
+ setsockopt(MS, SOL_SOCKET, SO_REUSEADDR, pack("l",1));
+ bind(MS, sockaddr_in($serverp, $servera)) || die "bind: $!\n";
+ listen(MS , 512) || die "listen: $!\n";
+
+ local *SERVERPID;
+ if ($serverpidfile) {
+ open(SERVERPID, '>', $serverpidfile) || die("$serverpidfile: $!\n");
+ }
+
+ if (defined($servergroup)) {
+ ($(, $)) = ($servergroup, $servergroup);
+ die "setgid: $!\n" if $) != $servergroup;
+ }
+ if (defined($serveruser)) {
+ ($<, $>) = ($serveruser, $serveruser);
+ die "setuid: $!\n" if $> != $serveruser;
+ }
+ serverdetach() unless $nobg;
+
+ if ($serverpidfile) {
+ syswrite(SERVERPID, "$$\n");
+ close(SERVERPID) || die("$serverpidfile: $!\n");
+ }
+
+ fcntl(MS, F_SETFL, 0);
+ my $remote_addr;
+ while (1) {
+ $remote_addr = accept(S, MS) || die "accept: $!\n";
+ my $pid;
+ while (1) {
+ $pid = fork();
+ last if defined($pid);
+ sleep(5);
+ }
+ last if $pid == 0;
+ close(S);
+ $chld{$pid} = 1;
+ $remote_addr = inet_ntoa((sockaddr_in($remote_addr))[1]);
+ while(1) {
+ $pid = waitpid(-1, keys %chld < $maxclients ? WNOHANG : 0);
+ delete $chld{$pid} if $pid && $pid > 0;
+ last if !($pid && $pid > 0) && keys %chld < $maxclients;
+ }
+ }
+ close MS;
+ $standalone = 2;
+ setsockopt(S, SOL_SOCKET, SO_KEEPALIVE, pack("l",1));
+ $remote_addr = inet_ntoa((sockaddr_in($remote_addr))[1]);
+ return $remote_addr;
+}
+
+sub parse_cgi {
+ my ($cgip, $query_string) = @_;
+
+ %$cgip = ();
+ my @query_string = split('&', $query_string);
+ while (@query_string) {
+ my ($name, $value) = split('=', shift(@query_string), 2);
+ next unless defined $name && $name ne '';
+ $name =~ tr/+/ /;
+ $name =~ s/%([a-fA-F0-9]{2})/chr(hex($1))/ge;
+ if (defined($value)) {
+ $value =~ tr/+/ /;
+ $value =~ s/%([a-fA-F0-9]{2})/chr(hex($1))/ge;
+ }
+ if ($name eq 'filter' || $name eq 'filter_arch') {
+ push @{$cgip->{$name}}, $value;
+ } else {
+ $cgip->{$name} = $value;
+ }
+ }
+}
+
+sub getrequest {
+ my $qu = '';
+ do {
+ die($qu eq '' ? "empty query\n" : "received truncated query\n") if !sysread(S, $qu, 1024, length($qu));
+ } while ($qu !~ /^(.*?)\r?\n/s);
+ my $req = $1;
+ my ($act, $path, $vers, undef) = split(' ', $req, 4);
+ my %headers;
+ die("400 No method name\n") if !$act;
+ if ($vers ne '') {
+ die("501 Bad method: $act\n") if $act ne 'GET' && $act ne 'HEAD' && $act ne 'POST';
+ while ($qu !~ /^(.*?)\r?\n\r?\n(.*)$/s) {
+ die("received truncated query\n") if !sysread(S, $qu, 1024, length($qu));
+ }
+ $qu =~ /^(.*?)\r?\n\r?\n(.*)$/s;
+ $qu = $2;
+ gethead(\%headers, "Request: $1");
+ } elsif ($act ne 'GET') {
+ die("501 Bad method, must be GET\n");
+ $qu = '';
+ }
+ my $query_string = '';
+ if ($path =~ /^(.*?)\?(.*)$/) {
+ $path = $1;
+ $query_string = $2;
+ }
+ if ($act eq 'POST') {
+ $query_string = '';
+ my $cl = $headers{'content-length'};
+ while (length($qu) < $cl) {
+ sysread(S, $qu, $cl - length($qu), length($qu)) || die("400 Truncated body\n");
+ }
+ $query_string = substr($qu, 0, $cl);
+ $qu = substr($qu, $cl);
+ }
+ $path =~ s/%([a-fA-F0-9]{2})/chr(hex($1))/ge;
+ return ($path, $query_string, $headers{'via'} ? 1 : 0);
+}
+
+sub replystream {
+ local (*FF) = shift;
+ my ($flen, $str, $ctx, @hi) = @_;
+ die("replystream: bad param\n") unless $flen;
+ unshift @hi, "HTTP/1.1 200 OK";
+ push @hi, "Server: drpmsync";
+ push @hi, "Cache-Control: no-cache";
+ push @hi, "Content-length: ".(length($str) + $flen + 32);
+ $str = join("\r\n", @hi)."\r\n\r\n".$str;
+ if ($standalone) {
+ fcntl(S, F_SETFL,O_NONBLOCK);
+ my $dummy = '';
+ 1 while sysread(S, $dummy, 1024, 0);
+ fcntl(S, F_SETFL,0);
+ }
+ my $r;
+ while (length($str) || $flen) {
+ if ($flen && length($str) < 16384) {
+ my $d;
+ my $r = sysread(FF, $d, $flen > 8192 ? 8192 : $flen);
+ if (!$r) {
+ die("replystream: read error: $!\n") unless defined $r;
+ die("replystream: unexpected EOF\n");
+ }
+ die("replystream: too much data\n") if $r > $flen;
+ $ctx->add($d);
+ $str .= $d;
+ $flen -= $r;
+ $str .= $ctx->hexdigest if !$flen;
+ }
+ $r = syswrite(S, $str, length($str));
+ die("replystream: write error: $!\n") unless $r;
+ $str = substr($str, $r);
+ }
+}
+
+sub reply {
+ my ($str, @hi) = @_;
+
+ if ($standalone) {
+ if (@hi && $hi[0] =~ /^status: (\d+.*)/i) {
+ $hi[0] = "HTTP/1.1 $1";
+ } else {
+ unshift @hi, "HTTP/1.1 200 OK";
+ }
+ }
+ push @hi, "Server: drpmsync";
+ push @hi, "Cache-Control: no-cache";
+ push @hi, "Content-length: ".length($str);
+ $str = join("\r\n", @hi)."\r\n\r\n$str";
+ if (!$standalone) {
+ print $str;
+ return;
+ }
+ fcntl(S, F_SETFL,O_NONBLOCK);
+ my $dummy = '';
+ 1 while sysread(S, $dummy, 1024, 0);
+ fcntl(S, F_SETFL,0);
+ my $l;
+ while (length($str)) {
+ $l = syswrite(S, $str, length($str));
+ die("write error: $!\n") unless $l;
+ $str = substr($str, $l);
+ }
+}
+
+sub reply_err {
+ my ($err, $cgi, $remote_addr) = @_;
+ serverlog($remote_addr, $err) if $serverlog && !$sendlogid;
+ sendlog($err) if $sendlogid;
+ die($err) if $standalone == 1;
+ $err =~ s/\n$//s;
+ if (exists($cgi->{'drpmsync'})) {
+ my $data = 'DRPMSYNC0001ERR 00000000'.sprintf("%08x", length($err)).$err;
+ reply($data, "Content-type: application/octet-stream");
+ } elsif ($err =~ /^(\d+[^\r\n]*)/) {
+ reply("<pre>$err</pre>\n", "Status: $1", "Content-type: text/html");
+ } else {
+ reply("<pre>$err</pre>\n", "Status: 404 Error", "Content-type: text/html");
+ }
+ exit(0);
+}
+
+my $check_access_cache_addr;
+my $check_access_cache_name;
+
+sub check_access {
+ my ($tree, $remote_addr) = @_;
+ my ($remote_name, $access_ok);
+
+ $remote_name = $check_access_cache_name if $check_access_cache_addr && $check_access_cache_addr eq $remote_addr;
+
+ if (@{$tree->{'deny'}}) {
+ if (!$remote_name) {
+ $remote_name = gethostbyaddr(inet_aton($remote_addr), AF_INET);
+ die("could not resolve $remote_addr\n") unless $remote_name;
+ $check_access_cache_addr = $remote_addr;
+ $check_access_cache_name = $remote_name;
+ }
+ for my $deny (@{$tree->{'deny'}}) {
+ if ($deny =~ /^!/) {
+ my $d1 = substr($deny, 1);
+ last if $remote_name =~ /^$d1$/i;
+ last if $remote_addr =~ /^$d1$/i;
+ }
+ goto denied if $remote_name =~ /^$deny$/i;
+ goto denied if $remote_addr =~ /^$deny$/i;
+ }
+ }
+ for my $allow (@{$tree->{'allow'}}) {
+ last if $allow =~ /^!/;
+ return if $remote_addr =~ /^$allow$/i;
+ }
+ if (!$remote_name) {
+ $remote_name = gethostbyaddr(inet_aton($remote_addr), AF_INET);
+ die("could not resolve $remote_addr\n") unless $remote_name;
+ $check_access_cache_addr = $remote_addr;
+ $check_access_cache_name = $remote_name;
+ }
+ for my $allow (@{$tree->{'allow'}}) {
+ if ($allow =~ /^!/) {
+ my $a1 = substr($allow, 1);
+ last if $remote_name =~ /^$a1$/i;
+ last if $remote_addr =~ /^$a1$/i;
+ }
+ return if $remote_addr =~ /^$allow$/i;
+ return if $remote_name =~ /^$allow$/i;
+ }
+denied:
+ my $denymsg = "access denied [%h]";
+ for my $dmsg (@{$tree->{'denymsg'}}) {
+ if ($remote_name =~ /^$dmsg->[0]$/i || $remote_addr =~ /^$dmsg->[0]$/i) {
+ $denymsg = $dmsg->[1];
+ last;
+ }
+ }
+ $denymsg =~ s/%h/$remote_addr/g;
+ $denymsg =~ s/%n/$remote_name/g;
+ die("$denymsg\n");
+}
+
+sub sendlog {
+ my $str = shift;
+ return unless $sendlogid;
+ $str =~ s/\n$//s;
+ my @lt = localtime(time());
+ $lt[5] += 1900;
+ $lt[4] += 1;
+ printf SENDLOG "%05d %04d-%02d-%02d %02d:%02d:%02d %s: %s\n", $$, @lt[5,4,3,2,1,0], $sendlogid, $str;
+}
+
+sub solve {
+ my ($have2, $info2, @dirs) = @_;
+
+ my @avail;
+ for my $dir (@dirs) {
+ if (opendir(D, $dir)) {
+ push @avail, map {"$dir/$_"} grep {/^[0-9a-f]{96}$/} readdir(D);
+ closedir D;
+ }
+ }
+ return () unless @avail;
+ my $gotone;
+ for (@avail) {
+ if ($have2->{substr($_, -96, 32)}) {
+ $gotone = 1;
+ last;
+ }
+ }
+ return () unless $gotone;
+ my @chains = ([$info2]);
+ my %avail;
+ push @{$avail{substr($_, -32, 32)}}, $_ for @avail;
+ while (@chains && @{$chains[0]} <= @avail) {
+ for my $pos (splice @chains) {
+ for my $a (@{$avail{$pos->[0]}}) {
+ my @n = (@$pos, $a);
+ $n[0] = substr($a, -96, 32);
+ if ($have2->{$n[0]}) {
+ shift @n;
+ return reverse @n;
+ }
+ push @chains, \@n;
+ }
+ }
+ }
+ return ();
+}
+
+sub extractrpm {
+ local *F = shift;
+ my ($o, $l) = @_;
+ local *F2;
+ tmpopen(*F2, $servertmp);
+ defined(sysseek(F, $o, 0)) || die("extractrpm: sysseek: $!\n");
+ my $buf;
+ while ($l > 0) {
+ my $r = sysread(F, $buf, $l > 8192 ? 8192 : $l);
+ if (!$r) {
+ die("extractrpm: read error: $!\n") unless defined $r;
+ die("extractrpm: unexpected EOF\n");
+ }
+ die("extractrpm: read too much data\n") if $r > $l;
+ die("extractrpm: write error: $!\n") if (syswrite(F2, $buf) || 0) != $r;
+ $l -= $r;
+ }
+ close(F);
+ seek(F2, 0, 0);
+ sysseek(F2, 0, 0);
+ open(F, "<&F2") || die("extractrpm: dup: $!\n");
+ close(F2);
+}
+
+sub hexit {
+ my $v = shift;
+ if ($v >= 4294967295) {
+ my $v2 = int($v / 4294967296);
+ return sprintf("FFFFFFFF%02x%08x", $v2, $v - 4294967296 * $v2);
+ } else {
+ return sprintf("%08x", $v);
+ }
+}
+
+my $deltadirscache;
+my $deltadirscacheid;
+
+sub getdeltadirs {
+ my ($ddconfig, $path) = @_;
+
+ my @dirs;
+ if ($deltadirscache) {
+ my @ddstat = stat($ddconfig);
+ undef $deltadirscache if !@ddstat || "$ddstat[9]/$ddstat[7]/$ddstat[1]" ne $deltadirscacheid;
+ }
+ if (!$deltadirscache) {
+ local *DD;
+ my @ddc;
+ if (open(DD, '<', $ddconfig)) {
+ while(<DD>) {
+ chomp;
+ next if /^\s*$/;
+ if (@ddc && /^\s*\+\s*(.*)/) {
+ push @{$ddc[-1]}, split(' ', $1);
+ } else {
+ push @ddc, [ split(' ', $_) ];
+ }
+ }
+ my @ddstat = stat(DD);
+ close DD;
+ $deltadirscache = \@ddc;
+ $deltadirscacheid = "$ddstat[9]/$ddstat[7]/$ddstat[1]";
+ }
+ }
+ if ($deltadirscache) {
+ for my $dd (@$deltadirscache) {
+ my @dd = @$dd;
+ my $ddre = shift @dd;
+ eval {
+ push @dirs, @dd if $path =~ /$ddre/;
+ };
+ }
+ }
+ return @dirs;
+}
+
+sub serve_request {
+ my ($cgi, $path_info, $script_name, $remote_addr, $keep_ok) = @_;
+
+ my $tree;
+ $path_info = '' unless defined $path_info;
+ die("invalid path\n") if $path_info =~ /\/(\.|\.\.)?\//;
+ die("invalid path\n") if $path_info =~ /\/(\.|\.\.)$/;
+ die("invalid path\n") if "$path_info/" =~ /(\.|\.\.)\//;
+ die("invalid path\n") if $path_info ne '' && ($path_info !~ /^\//);
+ die("$script_name not exported\n") unless $trees{$script_name};
+
+ my $sendlog = $trees{$script_name}->{'log'};
+ if ($tree && $tree->{'log'} && (!$sendlog || $tree->{'log'} ne $sendlog)) {
+ close(SENDLOG);
+ undef $sendlogid;
+ }
+ if ($sendlog && (!$tree || !$tree->{'log'} || $tree->{'log'} ne $sendlog)) {
+ open(SENDLOG, '>>', $sendlog) || die("$sendlog: $!\n");
+ select(SENDLOG);
+ $| = 1;
+ select(STDOUT);
+ $sendlogid = "[$remote_addr] $trees{$script_name}->{'id'}";
+ }
+ $tree = $trees{$script_name};
+ check_access($tree, $remote_addr);
+
+ my $spath_info = $path_info;
+ $spath_info =~ s/^\///;
+
+ my $root = $tree->{'root'};
+ die("$root: $!\n") unless -d $root;
+
+ my $replyid = $keep_ok ? 'DRPMSYNK' : 'DRPMSYNC';
+
+ if ($path_info =~ /(.*)\/drpmsync\/closesock$/ && exists $cgi->{'drpmsync'}) {
+ my $croot = $1;
+ sendlog(". $croot bye");
+ close(S);
+ exit(0);
+ }
+
+ if ($path_info =~ /^(.*)\/drpmsync\/contents$/) {
+ my $croot = $1;
+ die("$croot: does not exist\n") unless -e "$root$croot";
+ die("$croot: not a directory\n") unless -d "$root$croot";
+ sendlog("# $croot contents request");
+ my $ti = time();
+ readcache("$root$croot/drpmsync/cache");
+ @files = ();
+ $cachehits = $cachemisses = 0;
+ @filter_comp = compile_filter(@{$cgi->{'filter'} || []});
+ @filter_arch_comp = compile_filter(@{$cgi->{'filter_arch'} || []});
+ findfiles("$root$croot", '', 0, exists($cgi->{'norecurse'}) ? 1 : 0);
+ filelist_apply_filter_arch(\@files) if @filter_arch_comp;
+ %cache = ();
+ $ti = time() - $ti;
+ my ($stamp1, $stamp2);
+ $stamp1 = $stamp2 = sprintf("%08x", time());
+ if (open(STAMP, '<', "$root$croot/drpmsync/timestamp")) {
+ my $s = '';
+ if ((sysread(STAMP, $s, 16) || 0) == 16 && $s !~ /[^0-9a-f]/) {
+ $stamp1 = substr($s, 0, 8);
+ $stamp2 = substr($s, 8, 8);
+ }
+ close STAMP;
+ }
+ my $data = '';
+ if (!exists $cgi->{'drpmsync'}) {
+ for (@files) {
+ my @l = @$_;
+ $l[0] = aescape($l[0]);
+ $l[5] = aescape($l[5]) if @l > 5;
+ splice(@l, 1, 1);
+ $data .= join(' ', @l)."\n";
+ }
+ sendlog("h $croot contents ($cachehits/$cachemisses/$ti)");
+ reply($data, "Content-type: text/plain");
+ exit(0);
+ }
+ $data = pack('H*', "$stamp1$stamp2");
+ $data = pack("Nw/a*w/a*", scalar(@files), $tree->{'id'}, $data);
+ for (@files) {
+ my @l = @$_;
+ my $b;
+ if (@l > 5) {
+ $b = pack('H*', "$l[2]$l[3]$l[4]").$l[5];
+ } elsif (@l > 3) {
+ $b = pack('H*', "$l[2]$l[3]");
+ } else {
+ $b = pack('H*', $l[2]);
+ }
+ $data .= pack("w/a*w/a*", $l[0], $b);
+ }
+ @files = ();
+ my $dataid = 'SYNC';
+ if ($have_zlib && exists($cgi->{'zlib'})) {
+ $data = Compress::Zlib::compress($data);
+ $dataid = 'SYNZ';
+ sendlog("z $croot contents ($cachehits/$cachemisses/$ti)");
+ } else {
+ sendlog("f $croot contents ($cachehits/$cachemisses/$ti)");
+ }
+ $data = sprintf("1%03x%08x", 0644, time()).$data;
+ $data = "${replyid}0001${dataid}00000000".sprintf("%08x", length($data)).$data.Digest::MD5::md5_hex($data);
+ reply($data, "Content-type: application/octet-stream");
+ return;
+ }
+
+ my @s = lstat("$root$path_info");
+
+ if (!exists($cgi->{'drpmsync'})) {
+ die("$spath_info: $!\n") unless @s;
+ if (! -d _) {
+ die("$spath_info: bad file type\n") unless -f _;
+ sendlog("h $path_info");
+ open(F, '<', "$root$path_info") || die("$spath_info: $!\n");
+ my $c = '';
+ while ((sysread(F, $c, 4096, length($c)) || 0) == 4096) {}
+ close F;
+ my $ct = 'text/plain';
+ if ($spath_info =~ /\.(gz|rpm|spm|bz2|tar|tgz|jpg|jpeg|gif|png|pdf)$/) {
+ $ct = 'application/octet-stream';
+ }
+ reply($c, "Content-type: $ct");
+ exit(0);
+ }
+ if (($path_info !~ s/\/$//)) {
+ if ($standalone) {
+ reply("The document has moved", "Status: 302 Found", "Content-type: text/html", "Location: http://$servername$tree->{'id'}$path_info/");
+ } else {
+ reply("The document has moved", "Status: 302 Found", "Content-type: text/html", "Location: http://$ENV{'SERVER_NAME'}$tree->{'id'}$path_info/");
+ }
+ exit(0);
+ }
+ sendlog("h $path_info");
+ opendir(DIR, "$root$path_info") || die("$root$path_info: $!\n");
+ my @ents = sort readdir(DIR);
+ closedir DIR;
+ @ents = grep {$_ ne '.' && $_ ne '..'} @ents;
+ unshift @ents, '.', '..';
+ my $data = "<pre>\n";
+ for my $ent (@ents) {
+ @s = lstat("$root$path_info/$ent");
+ if (!@s) {
+ $data .= escape("$ent: $!\n");
+ next;
+ }
+ my $ent2 = '';
+ my $info = '?';
+ $info = 'c' if -c _;
+ $info = 'b' if -b _;
+ $info = '-' if -f _;
+ $info = 'd' if -d _;
+ if (-l _) {
+ $info = 'l';
+ $ent2 = readlink("$root$path_info/$ent");
+ die("$root$path_info/$ent: $!") unless defined $ent2;
+ $ent2 = escape(" -> $ent2");
+ }
+ my $mode = $s[2] & 0777;
+ for (split('', 'rwxrwxrwx')) {
+ $info .= $mode & 0400 ? $_ : '-';
+ $mode *= 2;
+ }
+ my @lt = localtime($s[9]);
+ $lt[4] += 1;
+ $lt[5] += 1900;
+ $info = sprintf("%s %4d root root %8d %04d-%02d-%02d %02d:%02d:%02d", $info, $s[3], $s[7], @lt[5, 4, 3, 2, 1, 0]);
+ $info = escape($info);
+ my $ne = "$path_info/$ent";
+ $ne = $path_info if $ent eq '.';
+ if ($ent eq '..') {
+ $ne = $path_info;
+ $ne =~ s/[^\/]+$//;
+ $ne =~ s/\/$//;
+ }
+ if ((-d _) && ! (-l _)) {
+ $ent = "<a href=\"".aescape("$script_name$ne/")."\">".escape("$ent")."</a>$ent2";
+ } elsif ((-f _) && ! (-l _)) {
+ $ent = "<a href=\"".aescape("$script_name$ne")."\">".escape("$ent")."</a>$ent2";
+ } else {
+ $ent = escape("$ent").$ent2;
+ }
+ $data .= "$info $ent\n";
+ }
+ $data .= "</pre>\n";
+ reply($data, "Content-type: text/html");
+ exit(0);
+ }
+
+ if (!@s) {
+ sendlog("- $path_info");
+ my $data = "${replyid}0001GONE".sprintf("%08x", length($spath_info)).'00000000'.$spath_info;
+ reply($data, "Content-type: application/octet-stream");
+ return;
+ }
+
+ if (-d _) {
+ # oops, this is bad, the file is now a directory
+ # send GONE so it will get removed
+ sendlog("X $path_info");
+ my $data = "${replyid}0001GONE".sprintf("%08x", length($spath_info)).'00000000'.$spath_info;
+ reply($data, "Content-type: application/octet-stream");
+ return;
+ }
+
+ if (-l _) {
+ sendlog("f $path_info");
+ my $lc = readlink("$root$path_info");
+ die("readlink: $!\n") unless defined($lc);
+ $lc = sprintf("2%03x%08x", $s[2] & 07777, $s[9]).$lc;
+ my $data = "${replyid}0001FILE".sprintf("%08x%08x", length($spath_info), length($lc)).$spath_info.$lc.Digest::MD5::md5_hex($lc);
+ reply($data, "Content-type: application/octet-stream");
+ return;
+ }
+
+ die("$spath_info: bad file type\n") unless -f _;
+ open(F, '<', "$root$path_info") || die("$spath_info: $!\n");
+
+ my $extracto = 0;
+ my $extractl;
+
+ if ((exists($cgi->{'fiso'}) || exists($cgi->{'extract'})) && ($spath_info =~ /(?<!\.delta)\.iso$/i)) {
+ if (!$cgi->{'extract'}) {
+ tmpopen(*F2, $servertmp);
+ my (undef, $err) = runprg(*F, *F2, $fragiso, 'make', '-', '-');
+ die("fragiso make failed: $err\n") if $err;
+ close F;
+ sysseek(F2, 0, 0); # currently at EOF
+ sendlog("i $path_info");
+ my $flen = -s F2;
+ my $ctx = Digest::MD5->new;
+ my $data = sprintf("1%03x%08x", $s[2] & 07777, $s[9]);
+ $ctx->add($data);
+ $data = "${replyid}0001FISO".sprintf("%08x", length($spath_info)).hexit(length($data) + $flen).$spath_info.$data;
+ replystream(*F2, $flen, $data, $ctx, "Content-type: application/octet-stream");
+ close F2;
+ return;
+ } else {
+ die("bad extract: $cgi->{'extract'}\n") unless $cgi->{'extract'} =~ /^([0-9a-fA-F]{2})([0-9a-fA-F]{8}):([0-9a-fA-F]{8})$/;
+ # always fits in perl's floats
+ $extracto = hex($1) * 4294967296 + hex($2);
+ $extractl = hex($3);
+ defined(sysseek(F, $extracto, 0)) || die("seek error: $!\n");
+ $path_info .= "\@$cgi->{'extract'}";
+ }
+ } elsif ($spath_info !~ /\.[sr]pm$/) {
+ my $flen = $s[7];
+ my $data = sprintf("1%03x%08x", $s[2] & 07777, $s[9]);
+ if ($s[7] >= 67108864) {
+ sendlog("f $path_info");
+ my $ctx = Digest::MD5->new;
+ $ctx->add($data);
+ $data = "${replyid}0001FILE".sprintf("%08x", length($spath_info)).hexit(length($data) + $flen).$spath_info.$data;
+ replystream(*F, $flen, $data, $ctx, "Content-type: application/octet-stream");
+ return;
+ }
+ while ((sysread(F, $data, 4096, length($data)) || 0) == 4096) {}
+ close F;
+ my $dataid = 'FILE';
+ if (length($data) >= 12 + 64 && $have_zlib && exists($cgi->{'zlib'}) && substr($data, 12, 2) ne "\037\213" && substr($data, 12, 2) ne "BZ") {
+ $data = substr($data, 0, 12).Compress::Zlib::compress(substr($data, 12));
+ $dataid = 'FILZ';
+ sendlog("z $path_info");
+ } else {
+ sendlog("f $path_info");
+ }
+ $data = "${replyid}0001$dataid".sprintf("%08x%08x", length($spath_info), length($data)).$spath_info.$data.Digest::MD5::md5_hex($data);
+ reply($data, "Content-type: application/octet-stream");
+ return;
+ }
+
+ my $deltadata = '';
+ my $deltaintro = '';
+ my $deltanum = 0;
+ my $sendrpm = exists($cgi->{'withrpm'}) ? 1 : 0;
+ my $key = '';
+ if ($cgi->{'have'}) {
+ my %have2;
+ for (split(',', $cgi->{'havealso'} ? "$cgi->{'have'},$cgi->{'havealso'}" : $cgi->{'have'})) {
+ die("bad have parameter\n") if (length($_) != 32 && length($_) != 64) || /[^0-9a-f]/;
+ $have2{substr($_, -32, 32)} = 1;
+ }
+ my @info = rpminfo_f(*F, $spath_info);
+ die("$spath_info: bad info\n") unless @info;
+ # seek needed because of perl's autoflush when forking
+ seek(F, $extracto, 0);
+ # only sysread after this!
+ defined(sysseek(F, $extracto, 0)) || die("sysseek: $!\n");
+ $path_info .= " ($info[2])" if $extracto;
+ my $info = $info[0];
+ my $info1 = substr($info, 0, 32);
+ my $info2 = substr($info, 32, 32);
+ if ($have2{$info2}) {
+ if ($extracto) {
+ # switch to real rpm
+ extractrpm(*F, $extracto, $extractl);
+ $extracto = 0;
+ $extractl = undef;
+ }
+ # identical payload, create sign only delta
+ # sendlog("$path_info: makedeltarpm sign only");
+ my ($out, $err) = runprg(*F, undef, $makedeltarpm, '-u', '-r', '-', '-');
+ die("makedeltarpm failed: $err\n") if $err;
+ $deltaintro .= sprintf("1%03x%08x$info2$info1$info2%08x", $s[2] & 07777, $s[9], length($out));
+ $deltadata .= $out;
+ $deltanum++;
+ $key = 's';
+ $sendrpm = 0; # no need to send full rpm in this case
+ } elsif (!exists($cgi->{'nocomplexdelta'})) {
+ # ok, lets see if we can build a chain from info2 back to have2
+ my $dpn = $info[2];
+ lost_delta:
+ $key = '';
+ $deltadata = '';
+ $deltaintro = '';
+ $deltanum = 0;
+
+ my $deltadir = "$root$path_info";
+ if ($path_info ne '') {
+ $deltadir =~ s/[^\/]+$//;
+ $deltadir =~ s/\/$//;
+ while ($deltadir ne $root) {
+ last if -d "$deltadir/drpmsync/deltas";
+ $deltadir =~ s/[^\/]+$//;
+ $deltadir =~ s/\/$//;
+ }
+ }
+ $deltadir = "$deltadir/drpmsync/deltas/$dpn";
+ my @solution;
+ if (length($cgi->{'have'}) == 64 && -f "$deltadir/$cgi->{'have'}$info2") {
+ @solution = ("$deltadir/$cgi->{'have'}$info2");
+ } else {
+ my @deltadirs = ( $deltadir );
+ push @deltadirs, map {"$_/$dpn"} getdeltadirs($tree->{'deltadirs'}, $spath_info) if $tree->{'deltadirs'};
+ @solution = solve(\%have2, $info2, @deltadirs);
+ }
+ my $dsize = 0;
+ for (@solution) {
+ goto lost_delta if ! -e $_;
+ die("bad deltarpm: $_\n") if ! -f _;
+ if (!exists($cgi->{'uncombined'}) && !$tree->{'no_combine'}) {
+ $dsize = -s _ if (-s _) > $dsize;
+ } else {
+ $dsize += -s _;
+ }
+ }
+ my $maxdeltasize = $cgi->{'maxdeltasize'};
+ $maxdeltasize = $tree->{'maxdeltasize'} if defined($tree->{'maxdeltasize'}) && (!defined($maxdeltasize) || $maxdeltasize > $tree->{'maxdeltasize'});
+ if (defined($maxdeltasize)) {
+ my $flen = -s F;
+ $flen = $extractl if defined $extractl;
+ @solution = () if $dsize >= ($flen * $maxdeltasize) / 100;
+ }
+ my $maxdeltasizeabs = $cgi->{'maxdeltasizeabs'};
+ $maxdeltasizeabs = $tree->{'maxdeltasizeabs'} if defined($tree->{'maxdeltasizeabs'}) && (!defined($maxdeltasizeabs) || $maxdeltasizeabs > $tree->{'maxdeltasizeabs'});
+ @solution = () if defined($maxdeltasizeabs) && $dsize >= $maxdeltasizeabs;
+ if (@solution) {
+ # sendlog("$path_info: solution @solution");
+ my @combine = ();
+ $key = scalar(@solution) if @solution > 1;
+ $key .= 'd';
+ for my $dn (@solution) {
+ push @combine, $dn;
+ next if @combine < @solution && !exists($cgi->{'uncombined'}) && !$tree->{'no_combine'};
+ my @ds = stat($combine[0]);
+ goto lost_delta if !@ds || ! -f _;
+ my ($out, $err);
+ if ($dn eq $solution[-1] && substr($dn, -64, 32) ne $info1) {
+ # sendlog("$path_info: combinedeltarpm -S @combine");
+ if ($extracto) {
+ # switch to real rpm
+ extractrpm(*F, $extracto, $extractl);
+ $extracto = 0;
+ $extractl = undef;
+ }
+ ($out, $err) = runprg(*F, undef, $combinedeltarpm, '-S', '-', @combine, '-');
+ defined(sysseek(F, 0, 0)) || die("sysseek: $!\n");
+ substr($combine[-1], -64, 32) = $info1 unless $err;
+ $key .= 's';
+ } elsif (@combine > 1) {
+ # sendlog("$path_info: combinedeltarpm @combine");
+ ($out, $err) = runprg(undef, undef, $combinedeltarpm, @combine, '-');
+ } else {
+ # sendlog("$path_info: readfile @combine");
+ ($out, $err) = readfile($dn);
+ }
+ if ($err) {
+ goto lost_delta if grep {! -f $_} @combine;
+ $err =~ s/\n$//s;
+ sendlog("! $path_info $err");
+ %have2 = (); # try without deltas
+ goto lost_delta;
+ }
+ $deltaintro .= sprintf("1%03x%08x".substr($combine[0], -96, 32).substr($combine[-1], -64, 64)."%08x", $ds[2] & 07777, $ds[9], length($out));
+ $deltadata .= $out;
+ $deltanum++;
+ @combine = ();
+ }
+ $key .= $deltanum if $deltanum != 1;
+ }
+ }
+ }
+ if (exists($cgi->{'deltaonly'}) && !$deltanum) {
+ sendlog("O $path_info");
+ my $data = "${replyid}0001NODR".sprintf("%08x", length($spath_info)).'00000000'.$spath_info;
+ reply($data, "Content-type: application/octet-stream");
+ return;
+ }
+ $sendrpm = 1 if !$deltanum;
+ $key .= 'r' if $sendrpm;
+ $key = '?' if $key eq '';
+ sendlog("$key $path_info");
+ if ($sendrpm) {
+ my $flen = -s F;
+ $flen = $extractl if defined $extractl;
+ if ($flen > 100000 || defined($extractl)) {
+ my $data = sprintf("1%03x%08x", $s[2] & 07777, $s[9]);
+ $data .= sprintf("%08x%08x", $deltanum, $sendrpm).$deltaintro.$deltadata;
+ my $ctx = Digest::MD5->new;
+ $ctx->add($data);
+ $data = "${replyid}0001RPM ".sprintf("%08x%08x", length($spath_info), length($data) + $flen).$spath_info.$data;
+ replystream(*F, $flen, $data, $ctx, "Content-type: application/octet-stream");
+ close F;
+ return;
+ }
+ }
+ my $rdata = '';
+ if ($sendrpm) {
+ while ((sysread(F, $rdata, 4096, length($rdata)) || 0) == 4096) {}
+ }
+ my $data = sprintf("1%03x%08x", $s[2] & 07777, $s[9]);
+ $data .= sprintf("%08x%08x", $deltanum, $sendrpm).$deltaintro.$deltadata.$rdata;
+ undef $deltadata;
+ $data = "${replyid}0001RPM ".sprintf("%08x%08x", length($spath_info), length($data)).$spath_info.$data.Digest::MD5::md5_hex($data);
+ reply($data, "Content-type: application/octet-stream");
+ close F;
+ undef $data;
+}
+
+if ($::ENV{'REQUEST_METHOD'} || (@ARGV && ($ARGV[0] eq '-s' || $ARGV[0] eq '-S'))) {
+ # server mode
+ my %cgi;
+ my $request_method = $::ENV{'REQUEST_METHOD'};
+ if ($request_method) {
+ my $query_string = $::ENV{'QUERY_STRING'};
+ my $script_name = $::ENV{'SCRIPT_NAME'};
+ my $path_info = $::ENV{'PATH_INFO'};
+ my $remote_addr = $::ENV{'REMOTE_ADDR'};
+ if ($request_method eq 'POST') {
+ $query_string = '';
+ read(STDIN, $query_string, 0 + $::ENV{'CONTENT_LENGTH'});
+ }
+ eval {
+ parse_cgi(\%cgi, $query_string);
+ my $config = $::ENV{'DRPMSYNC_CONFIG'};
+ readconfig_server($config);
+ serve_request(\%cgi, $path_info, $script_name, $remote_addr, 0);
+ exit(0);
+ };
+ reply_err($@, \%cgi, $remote_addr);
+ exit(0);
+ }
+ my $remote_addr = startserver($ARGV[1], $ARGV[0] eq '-S' ? 1 : 0);
+ eval {
+ while (1) {
+ %cgi = ();
+ my ($path, $query_string, $has_via) = getrequest(\%cgi);
+ $request_method = 'GET';
+ parse_cgi(\%cgi, $query_string);
+ my $keep_ok = !$has_via && exists($cgi{'drpmsync'});
+ my @mtrees = grep {$path eq $_->{'id'} || substr($path, 0, length($_->{'id'}) + 1) eq "$_->{'id'}/" } sort {length($b->{'id'}) <=> length($a->{'id'})} values %trees;
+ die("not exported\n") unless @mtrees;
+ my $script_name = $mtrees[0]->{'id'};
+ my $path_info = substr($path, length($script_name));
+ serve_request(\%cgi, $path_info, $script_name, $remote_addr, $keep_ok);
+ exit(0) unless $keep_ok;
+ }
+ };
+ reply_err($@, \%cgi, $remote_addr);
+ exit(0);
+}
+
+
+#######################################################################
+# Client code
+#######################################################################
+
+my @config_source;
+my $config_generate_deltas;
+my $config_keep_deltas;
+my $config_keep_uncombined;
+my $config_always_get_rpm;
+my @config_generate_delta_compression;
+my $config_recvlog;
+my $config_delta_max_age;
+my $config_repo;
+my $config_timeout;
+my @config_filter;
+my @config_filter_arch;
+
+my $syncport;
+my $syncaddr;
+my $syncproto;
+my $syncuser;
+my $syncpassword;
+my $syncurl;
+my $syncroot;
+my $esyncroot;
+my $synctree = '';
+my $synchost = Net::Domain::hostfqdn();
+
+my $newstamp1;
+my $newstamp2;
+
+my $runningjob;
+
+sub readconfig_client {
+ my $cf = shift;
+ local *CF;
+ open(CF, '<', $cf) || die("$cf: $!\n");
+ while (<CF>) {
+ chomp;
+ s/^\s+//;
+ s/\s+$//;
+ next if $_ eq '' || /^#/;
+ my @s = split(' ', $_);
+ $s[0] = lc($s[0]);
+ if ($s[0] eq 'source:') {
+ shift @s;
+ @config_source = @s;
+ } elsif ($s[0] eq 'generate_deltas:') {
+ $config_generate_deltas = ($s[1] && $s[1] =~ /true/i);
+ } elsif ($s[0] eq 'generate_delta_compression:') {
+ @config_generate_delta_compression = ();
+ @config_generate_delta_compression = ('-z', $s[1]) if $s[1];
+ } elsif ($s[0] eq 'keep_deltas:') {
+ $config_keep_deltas = ($s[1] && $s[1] =~ /true/i);
+ } elsif ($s[0] eq 'keep_uncombined:') {
+ $config_keep_uncombined = ($s[1] && $s[1] =~ /true/i);
+ } elsif ($s[0] eq 'always_get_rpm:') {
+ $config_always_get_rpm = ($s[1] && $s[1] =~ /true/i);
+ } elsif ($s[0] eq 'delta_max_age:') {
+ $config_delta_max_age = @s > 1 ? $s[1] : undef;
+ } elsif ($s[0] eq 'timeout:') {
+ $config_timeout = @s > 1 ? $s[1] : undef;
+ } elsif ($s[0] eq 'deltarpmpath:') {
+ my $p = defined($s[1]) ? "$s[1]/" : '';
+ $makedeltarpm = "${p}makedeltarpm";
+ $combinedeltarpm = "${p}combinedeltarpm";
+ $applydeltarpm = "${p}applydeltarpm";
+ $fragiso = "${p}fragiso";
+ } elsif ($s[0] eq 'log:') {
+ $config_recvlog = @s > 1 ? $s[1] : undef;
+ } elsif ($s[0] eq 'repo:') {
+ $config_repo = @s > 1 ? $s[1] : undef;
+ } elsif ($s[0] eq 'exclude:') {
+ push @config_filter, map {"-$_"} @s;
+ } elsif ($s[0] eq 'include:') {
+ push @config_filter, map {"+$_"} @s;
+ } elsif ($s[0] eq 'exclude_arch:') {
+ push @config_filter_arch, map {"-$_"} @s;
+ } elsif ($s[0] eq 'include_arch:') {
+ push @config_filter_arch, map {"+$_"} @s;
+ } else {
+ $s[0] =~ s/:$//;
+ die("$cf: unknown configuration parameter: $s[0]\n");
+ }
+ }
+ $config_keep_deltas ||= $config_generate_deltas;
+ $config_keep_deltas ||= $config_keep_uncombined;
+ close CF;
+}
+
+#######################################################################
+
+sub mkdir_p {
+ my $dir = shift;
+ return if -d $dir;
+ mkdir_p($1) if $dir =~ /^(.*)\//;
+ mkdir($dir, 0777) || die("mkdir: $dir: $!\n");
+}
+
+#######################################################################
+
+sub toiso {
+ my @lt = localtime($_[0]);
+ $lt[5] += 1900;
+ $lt[4] += 1;
+ return sprintf "%04d-%02d-%02d %02d:%02d:%02d", @lt[5,4,3,2,1,0];
+}
+
+#######################################################################
+
+sub recvlog {
+ my $str = shift;
+
+ return unless $config_recvlog;
+ my @lt = localtime(time());
+ $lt[5] += 1900;
+ $lt[4] += 1;
+ printf RECVLOG "%04d-%02d-%02d %02d:%02d:%02d %s\n", @lt[5,4,3,2,1,0], $str;
+}
+
+sub recvlog_print {
+ my $str = shift;
+ print "$str\n";
+ recvlog($str);
+}
+
+#######################################################################
+
+sub makedelta {
+ my ($from, $to, $drpm) = @_;
+ # print "makedeltarpm $from $to\n";
+ if (substr($drpm, -96, 32) eq substr($drpm, -32, 32)) {
+ system($makedeltarpm, @config_generate_delta_compression, '-u', '-r', $to, $drpm) && die("makedeltarpm failed\n");
+ } else {
+ system($makedeltarpm, @config_generate_delta_compression, '-r', $from, $to, $drpm) && die("makedeltarpm failed\n");
+ }
+ die("makedeltarpm did not create delta\n") unless -s $drpm;
+ return $drpm;
+}
+
+sub applydeltas {
+ my ($job, $from, $to, $extractoff, @deltas) = @_;
+ my $dn = $deltas[0];
+ if (@deltas > 1) {
+ my $ddir = $deltas[0];
+ $ddir =~ s/\/[^\/]+$//;
+ my $d1 = $deltas[0];
+ my $d2 = $deltas[-1];
+ my @d1s = stat($d1);
+ die("$d1: $!\n") if !@d1s;
+ $d1 =~ s/.*\///;
+ $d2 =~ s/.*\///;
+ $dn = "$ddir/".substr($d1, 0, 32).substr($d2, 32, 64);
+ die("combined delta already exists?\n") if -f $dn;
+ # print "combinedeltarpm @deltas\n";
+ if (system($combinedeltarpm, @deltas, $dn) || ! -s $dn) {
+ recvlog_print("! combinedeltarpm @deltas $dn failed");
+ unlink @deltas;
+ return ();
+ }
+ utime($d1s[9], $d1s[9], $dn);
+ }
+ # print "applydeltarpm $from $dn\n";
+ my $err;
+ if ($extractoff) {
+ local *EXTR;
+ if (!open(EXTR, '+<', $to)) {
+ recvlog_print("! open $to failed: $!");
+ unlink(@deltas);
+ return ();
+ }
+ if (!defined(sysseek(EXTR, $extractoff, 0))) {
+ recvlog_print("! sysseek $to failed: $!");
+ unlink(@deltas);
+ return ();
+ }
+ (undef, $err) = runprg_job($job, undef, *EXTR, $applydeltarpm, '-r', $from, $dn, '-');
+ close(EXTR);
+ } else {
+ (undef, $err) = runprg_job($job, undef, undef, $applydeltarpm, '-r', $from, $dn, $to);
+ }
+ if ($err) {
+ recvlog_print("! applydeltarpm -r $from $dn $to failed: $err");
+ unlink(@deltas);
+ return ();
+ }
+ if ($job) {
+ $job->{'applydeltas'} = [$from, $dn, $to, @deltas];
+ return ($job);
+ }
+ if ($config_keep_uncombined || @deltas <= 1) {
+ if (@deltas > 1) {
+ unlink($dn) || die("unlink $dn: $!\n");
+ }
+ return @deltas;
+ }
+ for my $d (@deltas) {
+ unlink($d) || die("unlink $d: $!\n");
+ }
+ return ($dn);
+}
+
+sub applydeltas_finish {
+ my ($job) = @_;
+ die("job not running\n") unless $job && $job->{'applydeltas'};
+ my ($from, $dn, $to, @deltas) = @{$job->{'applydeltas'}};
+ delete $job->{'applydeltas'};
+ my $err;
+ (undef, $err) = runprg_finish($job);
+ if ($err) {
+ recvlog_print("! applydeltarpm -r $from $dn $to failed: $err");
+ unlink(@deltas);
+ return ();
+ }
+ if ($config_keep_uncombined || @deltas <= 1) {
+ if (@deltas > 1) {
+ unlink($dn) || die("unlink $dn: $!\n");
+ }
+ return @deltas;
+ }
+ for my $d (@deltas) {
+ unlink($d) || die("unlink $d: $!\n");
+ }
+ return ($dn);
+}
+
+sub checkjob {
+ my ($pn) = @_;
+ return unless $runningjob;
+ my $job = $runningjob;
+ if (defined($pn)) {
+ return if $job->{'wip'} ne $pn;
+ }
+ undef $runningjob;
+ my @args = @{$job->{'finishargs'}};
+ delete $job->{'finishargs'};
+ $job->{'finish'}->(@args);
+}
+
+
+#######################################################################
+# repo functions
+#######################################################################
+
+sub repo_search {
+ my ($dpn, $k) = @_;
+ local *F;
+ open(F, '<', "$config_repo/$dpn") || return ();
+ my $k2 = substr($k, 32, 32);
+ my ($l, @l);
+ my (@r1, @r2, @r3);
+ while (defined($l = <F>)) {
+ chomp $l;
+ my @l = split(' ', $l, 3);
+ if ($l[0] eq $k) {
+ push @r1, \@l;
+ } elsif (substr($l[0], 32, 32) eq $k2) {
+ push @r2, \@l;
+ } else {
+ push @r3, \@l;
+ }
+ }
+ close F;
+ return (@r1, @r2, @r3);
+}
+
+sub repo_check {
+ my (@r) = @_;
+
+ my @s;
+ for my $r (splice(@r)) {
+ if ($r->[2] =~ /^(.*)@([0-9a-f]{10}:[0-9a-f]{8}$)/) {
+ @s = stat($1);
+ } else {
+ @s = stat($r->[2]);
+ }
+ push @r, $r if @s && $r->[1] eq "$s[9]/$s[7]";
+ }
+ return @r;
+}
+
+sub repo_cp {
+ my ($r, $bdir, $to, $extractoff) = @_;
+
+ my $d = "$bdir/$to";
+
+ local(*F, *OF);
+ my @s;
+ my $len;
+ if ($r->[2] =~ /^(.*)@([0-9a-f]{2})([0-9a-f]{8}):([0-9a-f]{8}$)/) {
+ my $iso = $1;
+ open(F, '<', $iso) || return undef;
+ @s = stat(F);
+ if (!@s || $r->[1] ne "$s[9]/$s[7]") {
+ close F;
+ return undef;
+ }
+ $len = hex($4);
+ if (!$len || !defined(sysseek(F, hex($2) * 4294967296 + hex($3), 0))) {
+ close F;
+ return undef;
+ }
+ } else {
+ open(F, '<', $r->[2]) || return undef;
+ @s = stat(F);
+ if (!@s || $r->[1] ne "$s[9]/$s[7]") {
+ close F;
+ return undef;
+ }
+ }
+ if ($extractoff) {
+ if (!open(OF, '+<', $d)) {
+ close F;
+ return undef;
+ }
+ if (!defined(sysseek(OF, $extractoff, 0))) {
+ close F;
+ close OF;
+ return undef;
+ }
+ } else {
+ if (!open(OF, '>', $d)) {
+ close F;
+ return undef;
+ }
+ }
+ my @info = cprpm(*F, *OF, 1, $len);
+ if (!close(OF)) {
+ close(F);
+ unlink($d);
+ return undef;
+ }
+ close(F);
+ if (@info != 3 || $info[0] ne $r->[0]) {
+ unlink($d);
+ return undef;
+ }
+ @s = stat($d);
+ if (!@s) {
+ unlink($d);
+ return undef;
+ }
+ return [ $to, "$s[9]/$s[7]/$s[1]", sprintf("1%03x%08x", ($s[2] & 07777), $s[9]), @info ];
+}
+
+sub repo_add_iso {
+ my ($fn, $d) = @_;
+ local *F;
+ return unless open(F, '-|', $fragiso, 'listiso', $fn);
+ my @frags = <F>;
+ return unless close(F);
+ chomp @frags;
+ for my $f (@frags) {
+ my @f = split(' ', $f, 3);
+ repo_add("$fn\@$f[0]", [ "$fn\@$f[0]", $d->[1], $d->[2], $f[1], undef, $f[2] ] );
+ }
+}
+
+sub repo_add {
+ my ($fn, $d) = @_;
+
+ return if $fn =~ m!drpmsync/wip.*/!;
+ if (@$d < 6) {
+ repo_add_iso($fn, $d) if $fn =~ /(?<!\.delta)\.iso$/i;
+ return;
+ }
+ return if $fn =~ /[\000-\037]/;
+ return if $d->[5] =~ /[\000-\037\/]/ || length($d->[5]) < 3;
+ local *OLD;
+ local *NEW;
+ my $nlid = $d->[1];
+ $nlid =~ s/\/[^\/]*$//;
+ my $nl;
+ $nl = "$d->[3] $nlid $fn" if $nlid;
+ my $kill;
+ $kill = $1 if $fn =~ /^(.*)@[0-9a-f]{2}[0-9a-f]{8}:[0-9a-f]{8}$/;
+ $kill = $fn if !$nlid && $fn =~ /(?<!\.delta)\.iso$/i;
+lock_retry:
+ if (!sysopen(OLD, "$config_repo/$d->[5]", POSIX::O_RDWR|POSIX::O_CREAT, 0666)) {
+ if (!sysopen(OLD, "$config_repo/$d->[5]", POSIX::O_RDONLY)) {
+ warn("$config_repo/$d->[5]: $!\n");
+ return;
+ }
+ }
+ if (!flock(OLD, LOCK_EX)) {
+ warn("$config_repo/$d->[5]: flock: $!\n");
+ return;
+ }
+ if (!(stat(OLD))[3]) {
+ close(OLD);
+ goto lock_retry;
+ }
+ my $old = '';
+ my $new = '';
+ while ((sysread(OLD, $old, 8192, length($old)) || 0) == 8192) {};
+ for my $l (split("\n", $old)) {
+ if ($nl && $l eq $nl) {
+ undef $nl;
+ } else {
+ if ($kill) {
+ my @lf = split(' ', $l);
+ next if $lf[2] =~ /^(.*)@[0-9a-f]{2}[0-9a-f]{8}:[0-9a-f]{8}$/ && $kill eq $1 && $lf[1] ne $nlid;
+ } else {
+ next if (split(' ', $l))[2] eq $fn;
+ }
+ }
+ $new .= "$l\n";
+ }
+ if ($nl) {
+ $new .= "$nl\n";
+ } elsif ($old eq $new) {
+ close OLD;
+ return;
+ }
+ if (!sysopen(NEW, "$config_repo/$d->[5].new", POSIX::O_WRONLY|POSIX::O_CREAT|POSIX::O_TRUNC, 0666)) {
+ warn("$config_repo/$d->[5].new open: $!\n");
+ close(OLD);
+ return;
+ }
+ if ((syswrite(NEW, $new) || 0) != length($new) || !close(NEW)) {
+ warn("$config_repo/$d->[5].new write: $!\n");
+ close(NEW);
+ close(OLD);
+ unlink("$config_repo/$d->[5].new");
+ return;
+ }
+ if (!rename("$config_repo/$d->[5].new", "$config_repo/$d->[5]")) {
+ warn("$config_repo/$d->[5] rename: $!\n");
+ close(OLD);
+ unlink("$config_repo/$d->[5].new");
+ return;
+ }
+ close(OLD);
+}
+
+sub repo_del {
+ my ($fn, $d) = @_;
+ my $dir;
+ if (@$d > 5) {
+ $dir = $d->[5];
+ } else {
+ return if $fn !~ /(?<!\.delta)\.iso$/i;
+ }
+ if (!$dir) {
+ local *DIR;
+ opendir(DIR, $config_repo) || return;
+ my @ds = grep {$_ ne '.' && $_ ne '..' && !/\..*\.new$/} readdir(DIR);
+ closedir(DIR);
+ for my $ds (@ds) {
+ repo_add($fn, [undef, '', undef, undef, undef, $ds]);
+ }
+ } else {
+ repo_add($fn, [undef, '', undef, undef, undef, $dir]);
+ }
+}
+
+sub repo_validate {
+ my $d = shift;
+ if (!$d) {
+ local *DIR;
+ opendir(DIR, $config_repo) || return;
+ my @ds = grep {$_ ne '.' && $_ ne '..' && !/\..*\.new$/} readdir(DIR);
+ closedir(DIR);
+ for my $ds (@ds) {
+ repo_validate($ds);
+ }
+ return;
+ }
+ local *OLD;
+ local *NEW;
+lock_retry:
+ if (!sysopen(OLD, "$config_repo/$d", POSIX::O_RDWR|POSIX::O_CREAT, 0666)) {
+ if (!sysopen(OLD, "$config_repo/$d", POSIX::O_RDONLY)) {
+ warn("$config_repo/$d: $!\n");
+ return;
+ }
+ }
+ if (!flock(OLD, LOCK_EX)) {
+ warn("$config_repo/$d: flock: $!\n");
+ return;
+ }
+ if (!(stat(OLD))[3]) {
+ close(OLD);
+ goto lock_retry;
+ }
+ my $old = '';
+ my $new = '';
+ while ((sysread(OLD, $old, 8192, length($old)) || 0) == 8192) {};
+ for my $l (split("\n", $old)) {
+ my @lf = split(' ', $l);
+ my @s;
+ if ($lf[2] =~ /^(.*)@[0-9a-f]{2}[0-9a-f]{8}:[0-9a-f]{8}$/) {
+ @s = stat($1);
+ } else {
+ @s = stat($lf[2]);
+ }
+ next if !@s || "$s[9]/$s[7]" ne $lf[1];
+ $new .= "$l\n";
+ }
+ if ($new eq $old) {
+ close OLD;
+ return;
+ }
+ if (!sysopen(NEW, "$config_repo/$d.new", POSIX::O_WRONLY|POSIX::O_CREAT|POSIX::O_TRUNC, 0666)) {
+ warn("$config_repo/$d.new open: $!\n");
+ close(OLD);
+ return;
+ }
+ if ((syswrite(NEW, $new) || 0) != length($new) || !close(NEW)) {
+ warn("$config_repo/$d.new write: $!\n");
+ close(NEW);
+ close(OLD);
+ unlink("$config_repo/$d.new");
+ return;
+ }
+ if (!rename("$config_repo/$d.new", "$config_repo/$d")) {
+ warn("$config_repo/$d rename: $!\n");
+ close(OLD);
+ unlink("$config_repo/$d.new");
+ return;
+ }
+ close(OLD);
+}
+
+#######################################################################
+
+my %files;
+my %syncfiles;
+my $had_gone;
+
+sub dirchanged {
+ my $dir = shift;
+ $dir =~ s/[^\/]+$//;
+ $dir =~ s/\/+$//;
+ return unless $dir ne '';
+ my $d = $files{$dir};
+ return unless $d && $d->[2] =~ /^0/;
+ $d->[2] = substr($d->[2], 0, 4)."ffffffff";
+}
+
+
+##################################################################
+
+my $net_start_tv;
+my $net_start_rvbytes;
+my $net_recv_bytes = 0;
+my $net_spent_time = 0;
+
+my $txbytes = 0;
+my $rvbytes = 0;
+my $sabytes = 0;
+
+sub setup_proto {
+ my $proto = shift;
+ if ($proto eq 'file') {
+ *get_syncfiles = \&file_get_syncfiles;
+ *get_update = \&file_get_update;
+ *send_fin = \&file_send_fin;
+ } elsif ($proto eq 'drpmsync') {
+ *get_syncfiles = \&drpmsync_get_syncfiles;
+ *get_update = \&drpmsync_get_update;
+ *send_fin = \&drpmsync_send_fin;
+ } elsif ($proto eq 'rsync') {
+ *get_syncfiles = \&rsync_get_syncfiles;
+ *get_update = \&rsync_get_update;
+ *send_fin = \&rsync_send_fin;
+ } elsif ($proto eq 'null') {
+ *get_syncfiles = sub {return ()};
+ *get_update = sub {die;};
+ *send_fin = sub {};
+ } else {
+ die("unsupported protocol: $proto\n");
+ }
+}
+
+#######################################################################
+# file protocol
+#######################################################################
+
+sub file_get_syncfiles {
+ my $norecurse = shift;
+
+ my @oldfiles = @files;
+ my @oldcache = %cache;
+ my $oldcachehits = $cachehits;
+ my $oldcachemisses = $cachemisses;
+ @files = ();
+ $cachehits = $cachemisses = 0;
+ readcache("$syncroot/drpmsync/cache");
+ findfiles($syncroot, '', 0, $norecurse);
+ my @syncfiles = @files;
+ @files = @oldfiles;
+ %cache = @oldcache;
+ $cachehits = $oldcachehits;
+ $cachemisses = $oldcachemisses;
+ $newstamp1 = $newstamp2 = sprintf("%08x", time);
+ return @syncfiles;
+}
+
+sub file_get_update {
+ my ($dto, $tmpnam, $reqext, $rextract) = @_;
+
+ die("rextract in FILE transport\n") if $rextract;
+ my @s = lstat("$syncroot/$dto->[0]");
+ return 'GONE' unless @s;
+ my $type;
+ my @info;
+ if (-l _) {
+ $type = '2';
+ my $lc = readlink("$syncroot/$dto->[0]");
+ return 'GONE' unless defined $lc;
+ symlink($lc, $tmpnam) || die("symlink: $!\n");
+ @info = linkinfo($tmpnam);
+ } elsif (! -f _) {
+ return 'GONE';
+ } else {
+ $type = '1';
+ local *F;
+ local *NF;
+ open(F, '<', "$syncroot/$dto->[0]") || return 'GONE';
+ @s = stat(F);
+ die("stat: $!\n") unless @s;
+ open(NF, '>', $tmpnam) || die("$tmpnam: $!\n");
+ if ($dto->[0] !~ /\.[sr]pm$/) {
+ @info = cpfile(*F, *NF);
+ } else {
+ @info = cprpm(*F, *NF);
+ if (@info != 3) {
+ defined(sysseek(F, 0, 0)) || die("sysseek: $!\n");
+ close(NF);
+ open(NF, '>', $tmpnam) || die("$tmpnam: $!\n");
+ @info = cpfile(*F, *NF);
+ }
+ }
+ close(F);
+ close(NF) || die("$tmpnam: $!\n");
+ fixmodetime($tmpnam, sprintf("1%03x%08x", ($s[2] & 07777), $s[9]));
+ }
+ @s = lstat($tmpnam);
+ die("$tmpnam: $!\n") unless @s;
+ if (@info == 3) {
+ return 'RPM ', [ $dto->[0], "$s[9]/$s[7]/$s[1]", sprintf("1%03x%08x", ($s[2] & 07777), $s[9]), @info ];
+ } else {
+ return 'FILE', [ $dto->[0], "$s[9]/$s[7]/$s[1]", sprintf("$type%03x%08x", ($s[2] & 07777), $s[9]), @info ];
+ }
+}
+
+sub file_send_fin {
+}
+
+
+#######################################################################
+# rsync protocol
+#######################################################################
+
+sub sread {
+ local *SS = shift;
+ my $len = shift;
+ $rvbytes += $len;
+ my $ret = '';
+ while ($len > 0) {
+ my $r = sysread(SS, $ret, $len, length($ret));
+ die("read error") unless $r;
+ $len -= $r;
+ die("read too much") if $r < 0;
+ }
+ return $ret;
+}
+
+sub swrite {
+ local *SS = shift;
+ my ($var, $len) = @_;
+ $len = length($var) unless defined $len;
+ $txbytes += $len;
+ (syswrite(SS, $var, $len) || 0) == $len || die("syswrite: $!\n");
+}
+
+my $rsync_muxbuf = '';
+
+sub muxread {
+ local *SS = shift;
+ my $len = shift;
+
+ #print "muxread $len\n";
+ while(length($rsync_muxbuf) < $len) {
+ #print "muxbuf len now ".length($muxbuf)."\n";
+ my $tag = '';
+ $tag = sread(*SS, 4);
+ $tag = unpack('V', $tag);
+ my $tlen = 0+$tag & 0xffffff;
+ $tag >>= 24;
+ if ($tag == 7) {
+ $rsync_muxbuf .= sread(*SS, $tlen);
+ next;
+ }
+ if ($tag == 8 || $tag == 9) {
+ my $msg = sread(*SS, $tlen);
+ die("$msg\n") if $tag == 8;
+ print "info: $msg\n";
+ next;
+ }
+ die("unknown tag: $tag\n");
+ }
+ my $ret = substr($rsync_muxbuf, 0, $len);
+ $rsync_muxbuf = substr($rsync_muxbuf, $len);
+ return $ret;
+}
+
+my $have_md4;
+my $rsync_checksum_seed;
+my $rsync_protocol;
+
+sub rsync_get_syncfiles {
+ my $norecurse = shift;
+
+ my $user = $syncuser;
+ my $password = $syncpassword;
+ if (!defined($have_md4)) {
+ $have_md4 = 0;
+ eval {
+ require Digest::MD4;
+ $have_md4 = 1;
+ };
+ }
+ $syncroot =~ s/^\/+//;
+ my $module = $syncroot;
+ $module =~ s/\/.*//;
+ my $tcpproto = getprotobyname('tcp');
+ socket(S, PF_INET, SOCK_STREAM, $tcpproto) || die("socket: $!\n");
+ connect(S, sockaddr_in($syncport, $syncaddr)) || die("connect: $!\n");
+ my $hello = "\@RSYNCD: 28\n";
+ swrite(*S, $hello);
+ my $buf = '';
+ sysread(S, $buf, 4096);
+ die("protocol error [$buf]\n") if $buf !~ /^\@RSYNCD: (\d+)\n/s;
+ $rsync_protocol = $1;
+ $rsync_protocol = 28 if $rsync_protocol > 28;
+ swrite(*S, "$module\n");
+ while(1) {
+ sysread(S, $buf, 4096);
+ die("protocol error [$buf]\n") if $buf !~ s/\n//s;
+ last if $buf eq "\@RSYNCD: OK";
+ die("$buf\n") if $buf =~ /^\@ERROR/s;
+ if ($buf =~ /^\@RSYNCD: AUTHREQD /) {
+ die("'$module' needs authentification, but Digest::MD4 is not installed\n") unless $have_md4;
+ $user = "nobody" if !defined($user) || $user eq '';
+ $password = '' unless defined $password;
+ my $digest = "$user ".Digest::MD4::md4_base64("\0\0\0\0$password".substr($buf, 18))."\n";
+ swrite(*S, $digest);
+ next;
+ }
+ }
+ my @args = ('--server', '--sender', '-rl');
+ push @args, '--exclude=/*/*' if $norecurse;
+ for my $arg (@args, '.', "$syncroot/.", '') {
+ swrite(*S, "$arg\n");
+ }
+ $rsync_checksum_seed = unpack('V', sread(*S, 4));
+ swrite(*S, "\0\0\0\0");
+ my @filelist;
+ my $name = '';
+ my $mtime = 0;
+ my $mode = 0;
+ my $uid = 0;
+ my $gid = 0;
+ my $flags;
+ while(1) {
+ $flags = muxread(*S, 1);
+ $flags = ord($flags);
+ # printf "flags = %02x\n", $flags;
+ last if $flags == 0;
+ $flags |= ord(muxread(*S, 1)) << 8 if $rsync_protocol >= 28 && ($flags & 0x04) != 0;
+ my $l1 = $flags & 0x20 ? ord(muxread(*S, 1)) : 0;
+ my $l2 = $flags & 0x40 ? unpack('V', muxread(*S, 4)) : ord(muxread(*S, 1));
+ $name = substr($name, 0, $l1).muxread(*S, $l2);
+ my $len = unpack('V', muxread(*S, 4));
+ if ($len == 0xffffffff) {
+ $len = unpack('V', muxread(*S, 4));
+ my $len2 = unpack('V', muxread(*S, 4));
+ $len += $len2 * 4294967296;
+ }
+ $mtime = unpack('V', muxread(*S, 4)) unless $flags & 0x80;
+ $mode = unpack('V', muxread(*S, 4)) unless $flags & 0x02;
+ my $id = "$mtime/$len/";
+ my @info = ();
+ my $mmode = $mode & 07777;
+ if (($mode & 0170000) == 0100000) {
+ @info = ('x');
+ $mmode |= 0x1000;
+ } elsif (($mode & 0170000) == 0040000) {
+ $mmode |= 0x0000;
+ } elsif (($mode & 0170000) == 0120000) {
+ $mmode |= 0x2000;
+ my $ln = muxread(*S, unpack('V', muxread(*S, 4)));
+ @info = (Digest::MD5::md5_hex($ln));
+ $id .= "$ln/";
+ } else {
+ print "$name: unknown mode: $mode\n";
+ next;
+ }
+ push @filelist, [$name, $id, sprintf("%04x%08x", $mmode, $mtime), @info];
+ }
+ my $io_error = unpack('V', muxread(*S, 4));
+ @filelist = sort {$a->[0] cmp $b->[0]} @filelist;
+ my $fidx = 0;
+ $_->[1] .= $fidx++ for @filelist;
+ $newstamp1 = $newstamp2 = sprintf("%08x", time);
+ return grep {$_->[0] ne '.'} @filelist;
+}
+
+sub rsync_adapt_filelist {
+ my $fl = shift;
+ my %c;
+ for (@files) {
+ my $i = $_->[1];
+ $i =~ s/[^\/]+$//;
+ $c{$i} = $_;
+ }
+ for (@$fl) {
+ next if @$_ == 3 || $_->[3] ne 'x';
+ my $i = $_->[1];
+ $i =~ s/[^\/]+$//;
+ next unless $c{$i};
+ my @info = @{$c{$i}};
+ splice(@info, 0, 3);
+ splice(@$_, 3, 1, @info);
+ }
+}
+
+sub rsync_get_update {
+ my ($dto, $tmpnam, $reqext, $rextract) = @_;
+
+ die("rextract in RSYNC transport\n") if $rextract;
+ my $fidx = $dto->[1];
+ if ($dto->[2] =~ /^2/) {
+ $fidx =~ s/^[^\/]*\/[^\/]*\///s;
+ $fidx =~ s/\/[^\/]*$//s;
+ symlink($fidx, $tmpnam) || die("symlink: $!\n");
+ my @s = lstat($tmpnam);
+ die("$tmpnam: $!\n") unless @s;
+ return 'FILE', [ $dto->[0], "$s[9]/$s[7]/$s[1]", sprintf("2%03x%08x", ($s[2] & 07777), $s[9]), linkinfo($tmpnam) ];
+ }
+ $fidx =~ s/.*\///;
+ swrite(*S, pack('V', $fidx));
+ swrite(*S, ("\0\0\0\0" x ($rsync_protocol >= 27 ? 4 : 3)));
+ my $rfidx = unpack('V', muxread(*S, 4));
+ die("rsync file mismatch $rfidx - $fidx\n") if $rfidx != $fidx;
+ my $sumhead = muxread(*S, 4 * ($rsync_protocol >= 27 ? 4 : 3));
+ my $md4ctx;
+ $md4ctx = Digest::MD4->new if $have_md4;
+ $md4ctx->add(pack('V', $rsync_checksum_seed)) if $have_md4;
+ local *OF;
+ open(OF, '>', $tmpnam) || die("$tmpnam: $!\n");
+ while(1) {
+ my $l = unpack('V', muxread(*S, 4));
+ last if $l == 0;
+ die("received negative token\n") if $l < 0;
+ my $chunk = muxread(*S, $l);
+ $md4ctx->add($chunk) if $have_md4;
+ syswrite(OF, $chunk) == $l || die("syswrite: $!\n");
+ }
+ close(OF) || die("close: $!\n");
+ my $md4sum = muxread(*S, 16);
+ if ($have_md4) {
+ die("data corruption on net\n") if unpack("H32", $md4sum) ne $md4ctx->hexdigest();
+ }
+ fixmodetime($tmpnam, $dto->[2]);
+ my @s = lstat($tmpnam);
+ die("$tmpnam: $!\n") unless @s;
+ if ($dto->[0] =~ /\.[sr]pm$/) {
+ return 'RPM ', [ $dto->[0], "$s[9]/$s[7]/$s[1]", sprintf("1%03x%08x", ($s[2] & 07777), $s[9]), rpminfo($tmpnam) ];
+ } else {
+ return 'FILE', [ $dto->[0], "$s[9]/$s[7]/$s[1]", sprintf("1%03x%08x", ($s[2] & 07777), $s[9]), fileinfo($tmpnam) ];
+ }
+}
+
+sub rsync_send_fin {
+ swrite(*S, pack('V', -1)); # switch to phase 2
+ swrite(*S, pack('V', -1)); # switch to phase 3
+ if ($rsync_protocol >= 24) {
+ swrite(*S, pack('V', -1)); # goodbye
+ }
+ close(S);
+}
+
+#######################################################################
+# drpmsync protocol
+#######################################################################
+
+my $sock_isopen;
+
+sub tolength {
+ local (*SOCK) = shift;
+ my ($ans, $l) = @_;
+ while (length($ans) < $l) {
+ die("received truncated answer\n") if !sysread(SOCK, $ans, $l - length($ans), length($ans));
+ }
+ return $ans;
+}
+
+sub copytofile {
+ return copytofile_seek($_[0], $_[1], 0, $_[2], $_[3], $_[4]);
+}
+
+sub copytofile_seek {
+ local (*SOCK) = shift;
+ my ($fn, $extractoff, $ans, $l, $ctx) = @_;
+
+ local *FD;
+ if ($extractoff) {
+ open(FD, '+<', $fn) || die("$fn: $!\n");
+ defined(sysseek(FD, $extractoff, 0)) || die("sysseek: $!\n");
+ } else {
+ open(FD, '>', $fn) || die("$fn: $!\n");
+ }
+ my $al = length($ans);
+ if ($al >= $l) {
+ die("$fn: write error\n") if syswrite(FD, $ans, $l) != $l;
+ die("$fn: write error\n") unless close(FD);
+ $ctx->add(substr($ans, 0, $l));
+ return substr($ans, $l);
+ }
+ if ($al > 0) {
+ die("$fn: write error\n") if syswrite(FD, $ans, $al) != $al;
+ $ctx->add($ans);
+ $l -= $al;
+ $ans = '';
+ }
+ while ($l > 0) {
+ die("received truncated answer\n") if !sysread(SOCK, $ans, $l > 8192 ? 8192 : $l, 0);
+ $al = length($ans);
+ die("$fn: write error\n") if syswrite(FD, $ans, $al) != $al;
+ $ctx->add($ans);
+ $l -= $al;
+ $ans = '';
+ }
+ die("$fn: write error\n") unless close(FD);
+ return '';
+}
+
+sub opensock {
+ return if $sock_isopen;
+ my $tcpproto = getprotobyname('tcp');
+ socket(S, PF_INET, SOCK_STREAM, $tcpproto) || die("socket: $!\n");
+ connect(S, sockaddr_in($syncport, $syncaddr)) || die("connect: $!\n");
+ $sock_isopen = 1;
+}
+
+sub finishreq {
+ local (*SOCK) = shift;
+ my ($ans, $ctx, $id) = @_;
+
+ if ($ctx) {
+ $ans = tolength(*SOCK, $ans, 32);
+ my $netmd5 = substr($ans, 0, 32);
+ die("network error: bad md5 digest\n") if $netmd5 =~ /[^a-f0-9]/;
+ my $md5 = $ctx->hexdigest;
+ die("network error: $md5 should be $netmd5\n") if $md5 ne $netmd5;
+ $ans = substr($ans, 32);
+ }
+ alarm(0) if $config_timeout;
+ if ($have_time_hires && defined($net_start_tv)) {
+ $net_spent_time += Time::HiRes::tv_interval($net_start_tv);
+ $net_recv_bytes += $rvbytes - $net_start_rvbytes;
+ $net_start_rvbytes = $rvbytes;
+ undef $net_start_tv;
+ }
+ if ($id && ($id ne 'DRPMSYNK' || length($ans))) {
+ close(SOCK);
+ undef $sock_isopen;
+ }
+ return $ans;
+}
+
+sub drpmsync_get_syncfiles {
+ my ($norecurse, $filelist_data) = @_;
+
+ my $data;
+ if (defined($filelist_data)) {
+ $data = $filelist_data;
+ goto use_filelist_data;
+ }
+ alarm($config_timeout) if $config_timeout;
+ opensock() unless $sock_isopen;
+ my $opts = '';
+ $opts .= '&zlib' if $have_zlib;
+ $opts .= '&norecurse' if $norecurse;
+ if (@filter_comp) {
+ my @fc = @filter_comp;
+ while (@fc) {
+ splice(@fc, 0, 2);
+ my $r = shift @fc;
+ $r =~ s/([\000-\040<>\"#&\+=%[\177-\377])/sprintf("%%%02X",ord($1))/sge;
+ $opts .= "&filter=$r";
+ }
+ }
+ if (@filter_arch_comp) {
+ my @fc = @filter_arch_comp;
+ while (@fc) {
+ splice(@fc, 0, 2);
+ my $r = shift @fc;
+ $r =~ s/([\000-\040<>\"#&\+=%[\177-\377])/sprintf("%%%02X",ord($1))/sge;
+ $opts .= "&filter_arch=$r";
+ }
+ }
+ my $query = "GET $esyncroot/drpmsync/contents?drpmsync$opts HTTP/1.0\r\nHost: $synchost\r\n\r\n";
+ $txbytes += length($query);
+ (syswrite(S, $query, length($query)) || 0) == length($query) || die("network write failed\n");
+ my $ans = '';
+ do {
+ die("received truncated answer\n") if !sysread(S, $ans, 1024, length($ans));
+ } while ($ans !~ /\n\r?\n/s);
+ $rvbytes += length($ans);
+ $ans =~ /\n\r?\n(.*)$/s;
+ $rvbytes -= length($1);
+ $ans = tolength(*S, $1, 32);
+ my $id = substr($ans, 0, 8);
+ die("received bad answer\n") if $id ne 'DRPMSYNC' && $id ne 'DRPMSYNK';
+ my $vers = hex(substr($ans, 8, 4));
+ die("answer has bad version\n") if $vers != 1;
+ my $type = substr($ans, 12, 4);
+ if ($type eq 'ERR ') {
+ my $anssize = hex(substr($ans, 24, 8));
+ $ans = tolength(*S, $ans, 32 + $anssize);
+ die("remote error: ".substr($ans, 32, $anssize)."\n");
+ }
+ die("can only sync complete trees\n") if $type eq 'GONE';
+ die("server send wrong answer\n") if $type ne 'SYNC' && $type ne 'SYNZ';
+ die("server send bad answer\n") if hex(substr($ans, 16, 8));
+ my $anssize = hex(substr($ans, 24, 8));
+ die("answer is too short\n") if $anssize < 28;
+ $rvbytes += 32 + $anssize + 32;
+ $ans = substr($ans, 32);
+ $ans = tolength(*S, $ans, $anssize);
+ $data = substr($ans, 0, $anssize);
+ $ans = substr($ans, $anssize);
+ my $ctx = Digest::MD5->new;
+ $ctx->add($data);
+ $ans = finishreq(*S, $ans, $ctx, $id);
+ $data = substr($data, 12);
+ if ($type eq 'SYNZ') {
+ die("cannot uncompress\n") unless $have_zlib;
+ $data = Compress::Zlib::uncompress($data);
+ }
+use_filelist_data:
+ my $filesnum = unpack('N', $data);
+ # work around perl 5.8.0 bug, where "(w/a*w/a*)*" does not work
+ my @data = unpack("x[N]".("w/a*w/a*" x ($filesnum + 1)), $data);
+ die("bad tree start\n") if @data < 2 || length($data[1]) != 8;
+ die("bad number of file entries\n") if @data != 2 * $filesnum + 2;
+ $synctree = shift @data;
+ $synctree .= '/' if $synctree ne '/';
+ ($newstamp1, $newstamp2) = unpack('H8H8', shift @data);
+ my @syncfiles = ();
+ while (@data) {
+ my ($name, $hex) = splice @data, 0, 2;
+ die("bad file name in list: $name\n") if "/$name/" =~ /\/(\.|\.\.|)\//;
+ if (length($hex) == 6) {
+ push @syncfiles, [ $name, undef, unpack('H12', $hex) ];
+ } elsif (length($hex) == 6 + 16) {
+ push @syncfiles, [ $name, undef, unpack('H12H32', $hex) ];
+ } elsif (length($hex) >= 6 + 32 + 4) {
+ my @l = ($name, undef, unpack('H12H64H8a*', $hex));
+ die("bad name.arch in file list: $l[5]\n") if $l[5] eq '.' || $l[5] eq '..' || $l[5] =~ /\//;
+ push @syncfiles, \@l;
+ } else {
+ die("bad line for $name: $hex\n");
+ }
+ }
+ # validate that no entry is listed twice
+ my %ents;
+ my %dirs;
+ for (@syncfiles) {
+ die("entry $_->[0] is listed twice\n") if exists $ents{$_->[0]};
+ $ents{$_->[0]} = 1;
+ if ($_->[2] =~ /^0/) {
+ $dirs{$_->[0]} = 1;
+ die("directory $_->[0] has bad data\n") unless @$_ == 3;
+ } else {
+ die("entry $_->[0] has bad data\n") unless @$_ > 3;
+ }
+ }
+ # validate that all files are connected to dirs
+ for (@syncfiles) {
+ next unless /^(.*)\//;
+ die("entry $_->[0] is not connected\n") unless $dirs{$1};
+ }
+ return @syncfiles;
+}
+
+sub drpmsync_send_fin {
+ return unless $sock_isopen;
+ my $query = "GET $esyncroot/drpmsync/closesock?drpmsync HTTP/1.0\r\nHost: $synchost\r\n\r\n";
+ $txbytes += length($query);
+ syswrite(S, $query, length($query)) == length($query) || die("network write failed\n");
+ close(S);
+ undef $sock_isopen;
+}
+
+sub drpmsync_get_update {
+ my ($dto, $tmpnam, $reqext, $rextract) = @_;
+
+ my $d;
+ my $extractoff = 0;
+ if ($rextract) {
+ die("bad extract parameter\n") unless $rextract =~ /^([0-9a-fA-F]{2})([0-9a-fA-F]{8}):[0-9a-fA-F]{8}$/;
+ $extractoff = hex($1) * 4294967296 + hex($2);
+ }
+
+ my $req = aescape($dto->[0]);
+ $req = "/$req?drpmsync";
+ $req .= "&extract=$rextract" if $rextract;
+ $req .= $reqext if $reqext;
+# XXX print "-> $req\n";
+ alarm($config_timeout) if $config_timeout;
+ opensock() unless $sock_isopen;
+ my $query = "GET $esyncroot$req HTTP/1.0\r\nHost: $synchost\r\n\r\n";
+ $txbytes += length($query);
+ if (syswrite(S, $query, length($query)) != length($query)) {
+ die("network write failed\n");
+ }
+ $net_start_tv = [Time::HiRes::gettimeofday()] if $have_time_hires;
+ $net_start_rvbytes = $rvbytes;
+ my $ans = '';
+ do {
+ die("received truncated answer\n") if !sysread(S, $ans, 1024, length($ans));
+ } while ($ans !~ /\n\r?\n/s);
+ $rvbytes += length($ans);
+ $ans =~ /\n\r?\n(.*)$/s;
+ $rvbytes -= length($1);
+ $ans = tolength(*S, $1, 32);
+ my $id = substr($ans, 0, 8);
+ die("received bad answer: $ans\n") if $id ne 'DRPMSYNC' && $id ne 'DRPMSYNK';
+ my $vers = hex(substr($ans, 8, 4));
+ die("answer has bad version\n") if $vers != 1;
+ my $type = substr($ans, 12, 4);
+ my $namelen = hex(substr($ans, 16, 8));
+ my $anssize = hex(substr($ans, 24, 8));
+ if ($anssize == 4294967295) {
+ $ans = tolength(*S, $ans, 32 + 10);
+ $anssize = hex(substr($ans, 32, 2)) * 4294967296 + hex(substr($ans, 32 + 2, 8));
+ $ans = substr($ans, 10);
+ }
+ $rvbytes += 32 + $namelen + $anssize + 32;
+ if ($type eq 'ERR ') {
+ $ans = tolength(*S, $ans, 32 + $namelen + $anssize);
+ return $type , substr($ans, 32 + $namelen, $anssize);
+ }
+ $ans = tolength(*S, $ans, 32 + $namelen);
+ die("answer does not match request $syncroot/$dto->[0] - $synctree".substr($ans, 32, $namelen)."\n") if "$syncroot/$dto->[0]" ne $synctree.substr($ans, 32, $namelen);
+ $ans = substr($ans, 32 + $namelen);
+
+ if ($type eq 'GONE' || $type eq 'NODR') {
+ $ans = finishreq(*S, $ans, undef, $id);
+ return $type;
+ }
+ my $extra = '';
+ my $extralen = 12;
+ $extralen = 12 + 16 if $type eq 'RPM ';
+
+ die("answer is too short\n") if $anssize < $extralen;
+ my $ctx = Digest::MD5->new;
+ my $ndrpm = 0;
+ my $nrpm = 0;
+ if ($extralen) {
+ $ans = tolength(*S, $ans, $extralen);
+ $extra = substr($ans, 0, $extralen);
+ die("illegal extra block\n") if $extra =~ /[^a-f0-9]/;
+ if ($type eq 'RPM ') {
+ $ndrpm = hex(substr($extra, 12, 8));
+ $nrpm = hex(substr($extra, 12 + 8, 8));
+ die("more than one rpm?\n") if $nrpm > 1;
+ if ($ndrpm) {
+ $extralen += $ndrpm * (12 + 32 * 3 + 8);
+ $ans = tolength(*S, $ans, $extralen);
+ $extra = substr($ans, 0, $extralen);
+ die("illegal extra block\n") if $extra =~ /[^a-f0-9]/;
+ }
+ }
+ $ans = substr($ans, $extralen);
+ $anssize -= $extralen;
+ $ctx->add($extra);
+ }
+
+ die("unexpected type $type\n") if $rextract && $type ne 'RPM ';
+
+ if ($type eq 'FILZ') {
+ die("cannot uncompress\n") unless $have_zlib;
+ $ans = tolength(*S, $ans, $anssize);
+ my $data = substr($ans, 0, $anssize);
+ $ctx->add($data);
+ $ans = finishreq(*S, substr($ans, $anssize), $ctx, $id);
+ $data = Compress::Zlib::uncompress($data);
+ my $datamd5 = Digest::MD5::md5_hex($data);
+ if ($dto->[2] =~ /^2/) {
+ symlink($data, $tmpnam) || die("symlink: $!\n");
+ } else {
+ open(FD, '>', $tmpnam) || die("$tmpnam: $!\n");
+ die("$tmpnam: write error\n") if (syswrite(FD, $data) || 0) != length($data);
+ close(FD) || die("$tmpnam: $!\n");
+ fixmodetime($tmpnam, substr($extra, 0, 12));
+ }
+ my @s = lstat($tmpnam);
+ die("$tmpnam: $!\n") unless @s;
+ if ($dto->[2] =~ /^2/) {
+ $d = [ $dto->[0], "$s[9]/$s[7]/$s[1]", sprintf("2%03x%08x", ($s[2] & 07777), $s[9]), linkinfo($tmpnam) ];
+ } else {
+ $d = [ $dto->[0], "$s[9]/$s[7]/$s[1]", sprintf("1%03x%08x", ($s[2] & 07777), $s[9]), $datamd5 ];
+ }
+ return ('FILZ', $d);
+ } elsif ($type eq 'FILE') {
+ if ($dto->[2] =~ /^2/) {
+ $ans = tolength(*S, $ans, $anssize);
+ $ctx->add(substr($ans, 0, $anssize));
+ symlink(substr($ans, 0, $anssize), $tmpnam) || die("symlink: $!\n");
+ $ans = substr($ans, $anssize);
+ } else {
+ $ans = copytofile(*S, $tmpnam, $ans, $anssize, $ctx);
+ }
+ $ans = finishreq(*S, $ans, $ctx, $id);
+ fixmodetime($tmpnam, substr($extra, 0, 12)) if $dto->[2] !~ /^2/;
+ my @s = lstat($tmpnam);
+ die("$tmpnam: $!\n") unless @s;
+ if ($dto->[2] =~ /^2/) {
+ $d = [ $dto->[0], "$s[9]/$s[7]/$s[1]", sprintf("2%03x%08x", ($s[2] & 07777), $s[9]), linkinfo($tmpnam) ];
+ } else {
+ $d = [ $dto->[0], "$s[9]/$s[7]/$s[1]", sprintf("1%03x%08x", ($s[2] & 07777), $s[9]), fileinfo($tmpnam) ];
+ }
+ return ('FILE', $d);
+ } elsif ($type eq 'FISO') {
+ $ans = copytofile(*S, "$tmpnam.fiso", $ans, $anssize, $ctx);
+ $ans = finishreq(*S, $ans, $ctx, $id);
+ return 'FISO', [ $tmpnam, undef, substr($extra, 0, 12) ];
+ } elsif ($type eq 'RPM ') {
+ $sabytes -= $anssize;
+ my $delta;
+ die("more than one rpm?\n") if $nrpm > 1;
+ die("nothing to do?\n") if $nrpm == 0 && $ndrpm == 0;
+ my @deltas;
+ my $dextra = substr($extra, 12 + 16);
+ while ($ndrpm > 0) {
+ $delta = $tmpnam;
+ $delta =~ s/[^\/]*$//;
+ $delta .= substr($dextra, 12, 32 * 3);
+ # end old job if we have a delta conflict
+ checkjob() if $runningjob && -e $delta;
+ my $size = hex(substr($dextra, 12 + 3 * 32, 8));
+ die("delta rpm bigger than answer? $size > $anssize\n") if $size > $anssize;
+ $ans = copytofile(*S, $delta, $ans, $size, $ctx);
+ $anssize -= $size;
+ fixmodetime($delta, substr($dextra, 0, 12));
+ $dextra = substr($dextra, 12 + 32 * 3 + 8);
+ push @deltas, $delta;
+ $ndrpm--;
+ }
+ if ($nrpm == 1) {
+ $ans = copytofile_seek(*S, $tmpnam, $extractoff, $ans, $anssize, $ctx);
+ $ans = finishreq(*S, $ans, $ctx, $id);
+ return 'RPM ', [ $dto->[0] ], @deltas if $rextract;
+ fixmodetime($tmpnam, substr($extra, 0, 12));
+ my @s = stat($tmpnam);
+ die("$tmpnam: $!\n") unless @s;
+ $sabytes += $s[7];
+ $d = [ $dto->[0], "$s[9]/$s[7]/$s[1]", sprintf("1%03x%08x", ($s[2] & 07777), $s[9]), rpminfo($tmpnam) ];
+ } else {
+ die("junk at end of answer\n") if $anssize;
+ $ans = finishreq(*S, $ans, $ctx, $id);
+ $d = [ undef, undef, substr($extra, 0, 12) ];
+ }
+ return 'RPM ', $d, @deltas;
+ } else {
+ die("received strange answer type: $type\n");
+ }
+}
+
+
+#######################################################################
+# update functions
+#######################################################################
+
+sub save_or_delete_deltas {
+ my ($bdir, $dpn, @deltas) = @_;
+
+ if (!$config_keep_deltas || !$dpn) {
+ for my $delta (@deltas) {
+ unlink($delta) || die("unlink $delta: $!\n");
+ }
+ return;
+ }
+ my $ddir = "$bdir/drpmsync/deltas/$dpn";
+ mkdir_p($ddir);
+ for my $delta (@deltas) {
+ my $dn = $delta;
+ $dn =~ s/.*\///;
+ if (substr($dn, 0, 32) eq substr($dn, 64, 32)) {
+ # print("detected signature-only delta\n");
+ local(*DDIR);
+ opendir(DDIR, "$ddir") || die("opendir $ddir: $!\n");
+ my @dh = grep {$_ =~ /^[0-9a-f]{96}$/} readdir(DDIR);
+ closedir(DDIR);
+ @dh = grep {substr($_, 64, 32) eq substr($dn, 64, 32)} @dh;
+ @dh = grep {substr($_, 32, 32) ne substr($dn, 32, 32)} @dh;
+ for my $dh (@dh) {
+ # recvlog_print("! $dh");
+ my $nn = substr($dh, 0, 32).substr($dn, 32, 64);
+ my @oldstat = stat("$ddir/$dh");
+ die("$ddir/$dh: $!") unless @oldstat;
+ if (system($combinedeltarpm, "$ddir/$dh", $delta, "$bdir/drpmsync/wip/$nn") || ! -f "$bdir/drpmsync/wip/$nn") {
+ recvlog_print("! combinedeltarpm $ddir/$dh $delta $bdir/drpmsync/wip/$nn failed");
+ unlink("$bdir/drpmsync/wip/$nn");
+ next;
+ }
+ utime($oldstat[9], $oldstat[9], "$bdir/drpmsync/wip/$nn");
+ rename("$bdir/drpmsync/wip/$nn", "$ddir/$nn") || die("rename $bdir/drpmsync/wip/$nn $ddir/$nn: $!\n");
+ unlink("$bdir/drpmsync/deltas/$dpn/$dh") || die("unlink $bdir/drpmsync/deltas/$dpn/$dh: $!\n");
+ }
+ unlink($delta) || die("unlink $delta: $!\n");
+ } else {
+ rename($delta, "$ddir/$dn") || die("rename $delta $ddir/$dn: $!\n");
+ }
+ }
+}
+
+
+# get rpms for fiso, fill iso
+
+sub update_fiso {
+ my ($bdir, $pn, $dto, $rights) = @_;
+
+ local *F;
+ if (!open(F, '-|', $fragiso, 'list', "$bdir/drpmsync/wip/$pn.fiso")) {
+ unlink("$bdir/drpmsync/wip/$pn.fiso");
+ return undef;
+ }
+ my @frags = <F>;
+ close(F) || return undef;
+ chomp @frags;
+ open(F, '>', "$bdir/drpmsync/wip/$pn") || die("$bdir/drpmsync/wip/$pn: $!\n");
+ close(F);
+ for my $f (@frags) {
+ my @f = split(' ', $f, 3);
+ update($bdir, [ $dto->[0], undef, $rights, $f[1], undef, $f[2] ], $f[0]);
+ }
+ checkjob() if $runningjob;
+ my ($md5, $err) = runprg(undef, undef, $fragiso, 'fill', '-m', "$bdir/drpmsync/wip/$pn.fiso", "$bdir/drpmsync/wip/$pn");
+ unlink("$bdir/drpmsync/wip/$pn.fiso") || die("unlink $bdir/drpmsync/wip/$pn.fiso: $!\n");;
+ my $tmpnam = "$bdir/drpmsync/wip/$pn";
+ if ($err) {
+ recvlog_print("! fragiso fill failed: $err");
+ unlink($tmpnam);
+ return undef;
+ }
+ die("fragiso did not return md5\n") unless $md5 =~ /^[0-9a-f]{32}$/;
+ fixmodetime($tmpnam, $rights);
+ my @s = lstat($tmpnam);
+ die("$tmpnam: $!\n") unless @s;
+ $rights = sprintf("1%03x%08x", ($s[2] & 07777), $s[9]);
+ $files{$dto->[0]} = [ $dto->[0], "$s[9]/$s[7]/$s[1]", $rights, $md5 ];
+ rename($tmpnam, "$bdir/$dto->[0]") || die("rename $tmpnam $bdir/$dto->[0]: $!\n");
+ if ($config_repo) {
+ for my $f (@frags) {
+ my @f = split(' ', $f, 3);
+ repo_add("$bdir/$dto->[0]\@$f[0]", [ "$dto->[0]\@$f[0]", "$s[9]/$s[7]/$s[1]", $rights, $f[1], undef, $f[2] ] );
+ }
+ }
+ return 1;
+}
+
+
+# called for files and rpms
+
+sub update {
+ my ($bdir, $dto, $rextract, $play_it_safe) = @_;
+
+ my ($d, $nd, $md);
+ my $pdto0;
+ my @deltas;
+ my $extractoff;
+ my $tmpnam;
+
+ if ($play_it_safe && ref($play_it_safe)) {
+ # poor mans co-routine implementation...
+ my $job = $play_it_safe;
+ $d = $job->{'d'};
+ $nd = $job->{'nd'};
+ $md = $job->{'md'};
+ $pdto0 = $job->{'pdto0'};
+ $tmpnam = $job->{'tmpnam'};
+ $extractoff = $job->{'extractoff'};
+ @deltas = applydeltas_finish($job);
+ goto applydeltas_finished;
+ }
+
+ die("can only update files and symlinks\n") if $dto->[2] !~ /^[12]/;
+ $pdto0 = $dto->[0]; # for recvlog_print;
+
+ # hack: patch source/dest for special fiso request
+ if ($rextract) {
+ die("bad extract parameter\n") unless $rextract =~ /^([0-9a-fA-F]{2})([0-9a-fA-F]{8}):[0-9a-fA-F]{8}$/;
+ $extractoff = hex($1) * 4294967296 + hex($2);
+ die("bad extract offset\n") unless $extractoff;
+ $pdto0 = "$dto->[0]\@$rextract ($dto->[5])";
+ }
+
+ $d = $files{$dto->[0]};
+ if ($d && !$rextract && $d->[3] eq $dto->[3]) {
+ return if $d->[2] eq $dto->[2]; # already identical
+ if (substr($d->[2], 0, 1) eq substr($dto->[2], 0, 1)) {
+ return if substr($d->[2], 0, 1) eq '2'; # can't change links
+ fixmodetime("$bdir/$d->[0]", $dto->[2]);
+ $d->[2] = $dto->[2];
+ my $newmtime = hex(substr($dto->[2], 4, 8));
+ $d->[1] =~ s/^.*?\//$newmtime\//; # patch cache id
+ return;
+ }
+ }
+
+ # check for simple renames
+ if (!$d && !$rextract && substr($dto->[2], 0, 1) eq '1') {
+ # search for same md5, same mtime and removed files
+ my @oldds = grep {@$_ > 3 && $_->[3] eq $dto->[3] && substr($_->[2], 4) eq substr($dto->[2], 4) && !$syncfiles{$_->[0]}} values %files;
+ if (@oldds) {
+ $d = $oldds[0];
+ my $pn = $dto->[0];
+ $pn =~ s/.*\///;
+ $tmpnam = "$bdir/drpmsync/wip/$pn";
+ checkjob($pn) if $runningjob;
+ # rename it
+ if (rename("$bdir/$d->[0]", $tmpnam)) {
+ delete $files{$d->[0]};
+ recvlog_print("- $d->[0]");
+ repo_del("$bdir/$d->[0]", $d) if $config_repo;
+ my @s = stat($tmpnam);
+ # check link count, must be 1
+ if (!@s || $s[3] != 1) {
+ unlink($tmpnam); # oops
+ } else {
+ fixmodetime($tmpnam, $dto->[2]);
+ @s = stat($tmpnam);
+ die("$tmpnam: $!\n") unless @s;
+ my @info = @$d;
+ splice(@info, 0, 3);
+ $files{$dto->[0]} = [ $dto->[0], "$s[9]/$s[7]/$s[1]", sprintf("1%03x%08x", ($s[2] & 07777), $s[9]), @info ];
+ recvlog_print("M $dto->[0]");
+ rename($tmpnam, "$bdir/$dto->[0]") || die("rename $tmpnam $bdir/$dto->[0]: $!\n");
+ repo_add("$bdir/$dto->[0]", $files{$dto->[0]}) if $config_repo;
+ # no need to create delta, as file was already in tree...
+ return;
+ }
+ }
+ undef $d;
+ }
+ }
+
+ if (!$d && @$dto > 5) {
+ my @oldds = grep {@$_ > 5 && $_->[5] eq $dto->[5]} values %files;
+ $d = $oldds[0] if @oldds;
+ }
+
+ $md = $d; # make delta against this entry ($d may point to repo)
+ my $repo_key = '';
+ my @repo;
+ my $deltaonly;
+
+ if ($config_repo && @$dto > 5) {
+ @repo = repo_search($dto->[5], $dto->[3]);
+ # we must not use the repo if we need to store the deltas.
+ # in this case we will send a delta-only request and retry the
+ # repo if it fails
+ if (@repo && !$rextract && !$config_generate_deltas && $config_keep_deltas) {
+ @repo = repo_check(@repo);
+ $deltaonly = 1 if @repo;
+ }
+ }
+
+##################################################################
+##################################################################
+
+send_again:
+
+ while (@repo && !$deltaonly) {
+ my $rd;
+ my $pn = $dto->[0];
+ $pn =~ s/^.*\///;
+ checkjob($pn) if $runningjob;
+ if ($repo[0]->[0] eq $dto->[3]) {
+ # exact match, great!
+ $tmpnam = "$bdir/drpmsync/wip/$pn";
+ $rd = repo_cp($repo[0], $bdir, "drpmsync/wip/$pn", $extractoff);
+ if (!$rd) {
+ shift @repo;
+ next;
+ }
+ if ($rextract) {
+ recvlog_print("R $pdto0");
+ return;
+ }
+ fixmodetime($tmpnam, $dto->[2]);
+ my @s = stat($tmpnam);
+ die("$tmpnam: $!\n") unless @s;
+ my $oldd5 = $md ? substr($md->[3], 32) : undef;
+ $files{$dto->[0]} = [ $dto->[0], "$s[9]/$s[7]/$s[1]", sprintf("1%03x%08x", ($s[2] & 07777), $s[9]), $rd->[3], $rd->[4], $rd->[5] ];
+ if ($oldd5 && $config_generate_deltas) {
+ recvlog_print("Rm $pdto0");
+ @deltas = makedelta("$bdir/$md->[0]", $tmpnam, "$bdir/drpmsync/wip/$oldd5$files{$dto->[0]}->[3]");
+ save_or_delete_deltas($bdir, $dto->[5], @deltas);
+ } else {
+ recvlog_print("R $pdto0");
+ }
+ rename($tmpnam, "$bdir/$dto->[0]") || die("rename $tmpnam $bdir/$dto->[0]: $!\n");
+ repo_add("$bdir/$dto->[0]", $files{$dto->[0]});
+ return;
+ } elsif (substr($repo[0]->[0], 32, 32) eq substr($dto->[3], 32, 32)) {
+ # have sign only rpm, copy right away
+ checkjob() if $runningjob;
+ $rd = repo_cp($repo[0], $bdir, "drpmsync/wip/repo-$pn");
+ if (!$rd) {
+ shift @repo;
+ next;
+ }
+ $d = $rd;
+ $d->[1] = undef; # mark as temp, don't gen/save delta
+ $repo_key = 'R';
+ @repo = ();
+ }
+ @repo = repo_check(@repo) if @repo;
+ last;
+ }
+
+ # ok, we really need to send a request our server
+ my $reqext = '';
+ if (@repo && !$deltaonly && !$play_it_safe) {
+ my @h = map {$_->[0]} @repo;
+ unshift @h, $d->[3] if $d && @$d > 5;
+ $reqext .= "&have=" . shift(@h);
+ if (@h) {
+ my %ha = map {substr($_, -32, 32) => 1} @h;
+ $reqext .= "&havealso=" . join(',', keys %ha);
+ }
+ } elsif ($d && @$d > 5 && !$play_it_safe) {
+ $reqext .= "&have=$d->[3]";
+ $reqext .= "&uncombined" if $config_keep_uncombined;
+ $reqext .= "&withrpm" if $config_always_get_rpm && substr($d->[3], 32) ne substr($dto->[3], 32);
+ $reqext .= "&deltaonly" if $deltaonly;
+ $reqext .= "&nocomplexdelta" if (!$config_keep_deltas || $rextract) && $config_always_get_rpm;
+ } else {
+ $reqext .= "&zlib" if $have_zlib;
+ $reqext .= "&fiso" if $config_repo && !$play_it_safe && ($dto->[0] =~ /(?<!\.delta)\.iso$/i);
+ }
+
+ my $pn = $dto->[0];
+ $pn =~ s/^.*\///;
+ die("no file name?\n") unless $pn ne '';
+ checkjob($pn) if $runningjob;
+ $tmpnam = "$bdir/drpmsync/wip/$pn";
+ my $type;
+ ($type, $nd, @deltas) = get_update($dto, $tmpnam, $reqext, $rextract);
+ if ($type eq 'ERR ') {
+ die("$nd\n");
+ } elsif ($type eq 'NODR') {
+ die("unexpected NODR answer\n") unless $deltaonly;
+ $deltaonly = 0;
+ goto send_again;
+ } elsif ($type eq 'GONE') {
+ warn("$dto->[0] is gone\n");
+ recvlog_print("${repo_key}G $pdto0");
+ if (-e "$bdir/$dto->[0]") {
+ unlink("$bdir/$dto->[0]") || die("unlink $bdir/$dto->[0]: $!\n");
+ }
+ delete $files{$dto->[0]};
+ $had_gone = 1;
+ } elsif ($type eq 'FILZ') {
+ recvlog_print("${repo_key}z $pdto0");
+ rename($tmpnam, "$bdir/$dto->[0]") || die("rename $tmpnam $bdir/$dto->[0]: $!\n");
+ $files{$dto->[0]} = $nd;
+ } elsif ($type eq 'FILE') {
+ recvlog_print("${repo_key}f $pdto0");
+ rename($tmpnam, "$bdir/$dto->[0]") || die("rename $tmpnam $bdir/$dto->[0]: $!\n");
+ $files{$dto->[0]} = $nd;
+ } elsif ($type eq 'FISO') {
+ checkjob() if $runningjob;
+ recvlog_print("${repo_key}i $pdto0");
+ if (!update_fiso($bdir, $pn, $dto, $nd->[2])) {
+ $play_it_safe = 1;
+ goto send_again;
+ }
+ } elsif ($type eq 'RPM ') {
+ if (!$nd->[0]) {
+ checkjob() if $runningjob;
+ die("no deltas?") unless @deltas;
+ undef $d if $d && (@$d <= 4 || substr($d->[3], 32, 32) ne substr($deltas[0], -96, 32));
+ if (!$d && @repo) {
+ my $dmd5 = substr($deltas[0], -96, 32);
+ my @mrepo = grep {substr($_->[0], 32, 32) eq $dmd5} @repo;
+ for my $rd (@mrepo) {
+ $d = repo_cp($rd, $bdir, "drpmsync/wip/repo-$pn");
+ last if $d;
+ }
+ if (!$d && @mrepo) {
+ recvlog_print("R! $pdto0");
+ save_or_delete_deltas($bdir, undef, @deltas);
+ @repo = grep {substr($_->[0], 32, 32) ne $dmd5} @repo;
+ goto send_again; # now without bad repo entries
+ }
+ $d->[1] = undef if $d;
+ $repo_key = 'R';
+ }
+ if (@deltas == 1 && substr($deltas[0], -96, 32) eq substr($deltas[0], -32, 32)) {
+ recvlog_print("${repo_key}s $pdto0");
+ } else {
+ recvlog_print("${repo_key}d $pdto0");
+ }
+ die("received delta doesn't match request\n") unless $d;
+
+#######################################################################
+
+ if (1) {
+ my $job = {};
+ $job->{'d'} = $d;
+ $job->{'nd'} = $nd;
+ $job->{'md'} = $md;
+ $job->{'pdto0'} = $pdto0;
+ $job->{'tmpnam'} = $tmpnam;
+ $job->{'extractoff'} = $extractoff;
+ $job->{'wip'} = $pn;
+ $job->{'finish'} = \&update;
+ $job->{'finishargs'} = [$bdir, $dto, $rextract, $job];
+ @deltas = applydeltas($job, "$bdir/$d->[0]", $tmpnam, $extractoff, @deltas);
+ if (@deltas) {
+ $runningjob = $job;
+ return;
+ }
+ delete $job->{'finishargs'}; # break circ ref
+ }
+
+#######################################################################
+
+ #recvlog("applying deltarpm to $d->[0]");
+ #@deltas = applydeltas("$bdir/$d->[0]", $tmpnam, $extractoff, @deltas);
+applydeltas_finished:
+ if (!@deltas) {
+ return update($bdir, $dto, $rextract, 1);
+ }
+ if (!$rextract) {
+ fixmodetime($tmpnam, $nd->[2]);
+ my @s = stat($tmpnam);
+ die("$tmpnam: $!\n") unless @s;
+ $sabytes += $s[7];
+ $nd = [ $dto->[0], "$s[9]/$s[7]/$s[1]", sprintf("1%03x%08x", ($s[2] & 07777), $s[9]), rpminfo($tmpnam) ];
+ }
+ } else {
+ recvlog_print("${repo_key}r $pdto0") if $rextract || !(!@deltas && $md && $md->[1] && $config_generate_deltas);
+ }
+ if ($rextract) {
+ save_or_delete_deltas($bdir, undef, @deltas);
+ unlink("$bdir/$d->[0]") if $d && ($d->[0] =~ m!drpmsync/wip/repo-!);
+ return;
+ }
+ if (@deltas && $d && !$d->[1]) {
+ # deltas made against some repo rpm, always delete
+ save_or_delete_deltas($bdir, undef, @deltas);
+ @deltas = ();
+ }
+ if (!@deltas && $md && $md->[1] && $config_generate_deltas) {
+ recvlog_print("${repo_key}m $pdto0");
+ @deltas = makedelta("$bdir/$md->[0]", $tmpnam, "$bdir/drpmsync/wip/".substr($md->[3], 32).$nd->[3]);
+ }
+ save_or_delete_deltas($bdir, $dto->[5], @deltas);
+
+ rename($tmpnam, "$bdir/$dto->[0]") || die("rename $tmpnam $bdir/$dto->[0]: $!\n");
+ $files{$dto->[0]} = $nd;
+ repo_add("$bdir/$dto->[0]", $nd) if $config_repo;
+ } else {
+ die("received strange answer type: $type\n");
+ }
+ unlink("$bdir/$d->[0]") if $d && ($d->[0] =~ m!drpmsync/wip/repo-!);
+}
+
+sub fixmodetime {
+ my ($fn, $mthex) = @_;
+ my $mode = hex(substr($mthex, 1, 3));
+ my $ti = hex(substr($mthex, 4, 8));
+ chmod($mode, $fn) == 1 || die("chmod $fn: $!\n");
+ utime($ti, $ti, $fn) == 1 || die("utime $fn: $!\n");
+}
+
+my $cmdline_cf;
+my $cmdline_source;
+my $cmdline_repo;
+my $cmdline_repo_add;
+my $cmdline_repo_validate;
+my $cmdline_get_filelist;
+my $cmdline_use_filelist;
+my $cmdline_norecurse;
+my $cmdline_list;
+my @cmdline_filter;
+my @cmdline_filter_arch;
+
+sub find_source {
+ my ($syncfilesp, $norecurse, $verbose, @sources) = @_;
+ my %errors;
+
+ if (!@sources) {
+ setup_proto('null');
+ @$syncfilesp = ();
+ return;
+ }
+ for my $s (@sources) {
+ $syncurl = $s;
+ my $ss = $s;
+ $syncproto = 'drpmsync';
+ if ($ss =~ /^(file|drpmsync|rsync):(.*)$/) {
+ $syncproto = lc($1);
+ $ss = $2;
+ if ($syncproto ne 'file') {
+ $ss =~ s/^\/\///;
+ if ($ss =~ /^([^\/]+)\@(.*)$/) {
+ $syncuser = $1;
+ $ss = $2;
+ ($syncuser, $syncpassword) = split(':', $syncuser, 2);
+ }
+ }
+ }
+ if ($syncproto eq 'file') {
+ $syncroot = $ss;
+ $syncroot =~ s/\/\.$//;
+ $syncroot =~ s/\/$// unless $syncroot eq '/';
+ } else {
+ ($syncaddr, $syncport, $syncroot) = $ss =~ /^([^\/]+?)(?::(\d+))?(\/.*)$/;
+ if (!$syncaddr) {
+ $errors{$s} = "bad url";
+ next;
+ }
+ $syncroot =~ s/\/\.$//;
+ $syncroot =~ s/\/$// unless $syncroot eq '/';
+ $esyncroot = aescape($syncroot);
+ $syncport ||= $syncproto eq 'rsync' ? 873 : 80;
+ $syncaddr = inet_aton($syncaddr);
+ if (!$syncaddr) {
+ $errors{$s} = "could not resolve host";
+ next;
+ }
+ print "trying $s\n" if $verbose;
+ }
+ eval {
+ setup_proto($syncproto);
+ @$syncfilesp = get_syncfiles($norecurse);
+ };
+ alarm(0) if $config_timeout;
+ last unless $@;
+ $errors{$s} = "$@";
+ $errors{$s} =~ s/\n$//s;
+ undef $syncaddr;
+ }
+ if ($syncproto ne 'file' && !$syncaddr) {
+ if (@sources == 1) {
+ die("could not connect to $sources[0]: $errors{$sources[0]}\n");
+ } else {
+ print STDERR "could not connect to any server:\n";
+ print STDERR " $_: $errors{$_}\n" for @sources;
+ exit(1);
+ }
+ }
+ filelist_apply_filter($syncfilesp);
+ filelist_apply_filter_arch($syncfilesp);
+}
+
+sub filelist_from_file {
+ my ($flp, $fn) = @_;
+
+ local *FL;
+ if ($fn eq '-') {
+ open(FL, '<&STDIN') || die("STDIN dup: $!\n");
+ } else {
+ open(FL, '<', $fn) || die("$fn: $!\n");
+ }
+ my $fldata;
+ my $data;
+ my $is_compressed;
+ die("not a drpmsync filelist\n") if read(FL, $data, 32) != 32;
+ if (substr($data, 0, 2) eq "\037\213") {
+ { local $/; $data .= <FL>; }
+ $data = Compress::Zlib::memGunzip($data);
+ die("filelist uncompress error\n") unless defined $data;
+ $is_compressed = 1;
+ }
+ die("not a drpmsync filelist\n") if (substr($data, 0, 24) ne 'DRPMSYNC0001SYNC00000000' && substr($data, 0, 24) ne 'DRPMSYNC0001SYNZ00000000');
+ if ($is_compressed) {
+ $fldata = substr($data, 32);
+ $data = substr($data, 0, 32);
+ } else {
+ { local $/; $fldata = <FL>; }
+ }
+ close FL;
+ my $md5 = substr($fldata, -32, 32);
+ $fldata = substr($fldata, 0, -32);
+ die("drpmsync filelist checksum error\n") if Digest::MD5::md5_hex($fldata) ne $md5;
+ $fldata = substr($fldata, 12);
+ if (substr($data, 16, 4) eq 'SYNZ') {
+ die("cannot uncompress filelist\n") unless $have_zlib;
+ $fldata = Compress::Zlib::uncompress($fldata);
+ }
+ @$flp = drpmsync_get_syncfiles($cmdline_norecurse, $fldata);
+ filelist_apply_filter($flp);
+ filelist_apply_filter_arch($flp);
+}
+
+while (@ARGV) {
+ last if $ARGV[0] !~ /^-/;
+ my $opt = shift @ARGV;
+ last if $opt eq '--';
+ if ($opt eq '-c') {
+ die("-c: argument required\n") unless @ARGV;
+ $cmdline_cf = shift @ARGV;
+ } elsif ($opt eq '--repo') {
+ die("--repo: argument required\n") unless @ARGV;
+ $cmdline_repo = shift @ARGV;
+ } elsif ($opt eq '--repo-add') {
+ $cmdline_repo_add = 1;
+ } elsif ($opt eq '--repo-validate') {
+ $cmdline_repo_validate = 1;
+ } elsif ($opt eq '--norecurse-validate') {
+ $cmdline_norecurse = 1;
+ } elsif ($opt eq '--list') {
+ $cmdline_list = 1;
+ $cmdline_norecurse = 1;
+ } elsif ($opt eq '--list-recursive') {
+ $cmdline_list = 1;
+ } elsif ($opt eq '--get-filelist') {
+ die("--get-filelist: argument required\n") unless @ARGV;
+ $cmdline_get_filelist = shift @ARGV;
+ } elsif ($opt eq '--filelist-synctree') {
+ $synctree = shift @ARGV;
+ $synctree .= '/';
+ } elsif ($opt eq '--use-filelist') {
+ die("--use-filelist: argument required\n") unless @ARGV;
+ $cmdline_use_filelist = shift @ARGV;
+ } elsif ($opt eq '--exclude') {
+ die("--exclude: argument required\n") unless @ARGV;
+ push @cmdline_filter, '-'.shift(@ARGV);
+ } elsif ($opt eq '--include') {
+ die("--include: argument required\n") unless @ARGV;
+ push @cmdline_filter, '+'.shift(@ARGV);
+ } elsif ($opt eq '--exclude-arch') {
+ die("--exclude-arch: argument required\n") unless @ARGV;
+ push @cmdline_filter_arch, '-'.shift(@ARGV);
+ } elsif ($opt eq '--include-arch') {
+ die("--include-arch: argument required\n") unless @ARGV;
+ push @cmdline_filter_arch, '+'.shift(@ARGV);
+ } else {
+ die("$opt: unknown option\n");
+ }
+}
+
+if ($cmdline_repo_validate) {
+ my $basedir;
+ $basedir = shift @ARGV if @ARGV;
+ die("illegal source parameter for repo operation\n") if @ARGV;
+ if (defined($cmdline_cf) || (defined($basedir) && -e "$basedir/drpmsync/config")) {
+ readconfig_client(defined($cmdline_cf) ? $cmdline_cf : "$basedir/drpmsync/config");
+ }
+ $config_repo = $cmdline_repo if defined $cmdline_repo;
+ die("--repo-validate: no repo specified\n") unless $config_repo;
+ repo_validate();
+ exit(0);
+}
+
+my $basedir;
+if (@ARGV == 2) {
+ die("illegal source parameter for repo operation\n") if $cmdline_repo_add;
+ $cmdline_source = shift @ARGV;
+ $basedir = $ARGV[0];
+} elsif (@ARGV == 1) {
+ if ($cmdline_list || defined($cmdline_get_filelist)) {
+ $cmdline_source = $ARGV[0];
+ } else {
+ $basedir = $ARGV[0];
+ }
+} else {
+ die("Usage: drpmsync [-c config] [source] <dir> | -s <serverconfig>\n") unless $cmdline_list && defined($cmdline_use_filelist);
+}
+
+if (defined($basedir)) {
+ if (-f $basedir) {
+ die("$basedir: not a directory (did you forget -s?)\n");
+ }
+ mkdir_p($basedir);
+}
+
+if (defined($cmdline_cf)) {
+ readconfig_client($cmdline_cf);
+} elsif (defined($basedir) && (-e "$basedir/drpmsync/config")) {
+ readconfig_client("$basedir/drpmsync/config");
+}
+
+@config_source = $cmdline_source if defined $cmdline_source;
+$config_repo = $cmdline_repo if defined $cmdline_repo;
+@filter_comp = compile_filter(@cmdline_filter, @config_filter);
+@filter_arch_comp = compile_filter(@cmdline_filter_arch, @config_filter_arch);
+
+if ($config_repo && defined($basedir)) {
+ my $nbasedir = `cd $basedir && /bin/pwd`;
+ chomp $nbasedir;
+ die("could not canonicalize $basedir\n") if !$nbasedir || !-d "$nbasedir";
+ $basedir = $nbasedir;
+}
+
+if ($cmdline_repo_add) {
+ die("--repo-add: no repo specified\n") unless $config_repo;
+ die("need a destination\n") unless defined $basedir;
+ readcache("$basedir/drpmsync/cache");
+ print "getting state of local tree...\n";
+ findfiles($basedir, '');
+ print("cache: $cachehits hits, $cachemisses misses\n");
+ for my $d (@files) {
+ repo_add("$basedir/$d->[0]", $d);
+ }
+ exit(0);
+}
+
+if (defined($cmdline_get_filelist)) {
+ die("need a source for get-filelist\n") unless @config_source;
+ $SIG{'ALRM'} = sub {die("network timeout\n");};
+ my @syncfiles;
+ find_source(\@syncfiles, $cmdline_norecurse, $cmdline_get_filelist eq '-' ? 0 : 1, @config_source);
+ send_fin();
+ filelist_from_file(\@syncfiles, $cmdline_use_filelist) if defined $cmdline_use_filelist;
+ local *FL;
+ if ($cmdline_get_filelist eq '-') {
+ open(FL, '>&STDOUT') || die("STDOUT dup: $!\n");
+ } else {
+ open(FL, '>', $cmdline_get_filelist) || die("$cmdline_get_filelist: $!\n");
+ }
+ my $data;
+ $data = pack('H*', "$newstamp1$newstamp2");
+ $data = pack("Nw/a*w/a*", scalar(@syncfiles), $synctree ne '/' ? substr($synctree, 0, -1) : '/', $data);
+ $data = sprintf("1%03x%08x", 0644, time()).$data;
+ for (@syncfiles) {
+ my @l = @$_;
+ my $b;
+ if (@l > 5) {
+ $b = pack('H*', "$l[2]$l[3]$l[4]").$l[5];
+ } elsif (@l > 3) {
+ if ($l[3] eq 'x') {
+ $b = pack('H*', $l[2])."\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0";
+ } else {
+ $b = pack('H*', "$l[2]$l[3]");
+ }
+ } else {
+ $b = pack('H*', $l[2]);
+ }
+ $data .= pack("w/a*w/a*", $l[0], $b);
+ }
+ $data = "DRPMSYNC0001SYNC00000000".sprintf("%08x", length($data)).$data.Digest::MD5::md5_hex($data);
+ print FL $data;
+ close(FL) || die("close: $!\n");
+ exit(0);
+}
+
+if ($cmdline_list) {
+ $SIG{'ALRM'} = sub {die("network timeout\n");};
+ my @syncfiles;
+ find_source(\@syncfiles, $cmdline_norecurse, 0, @config_source);
+ send_fin();
+ filelist_from_file(\@syncfiles, $cmdline_use_filelist) if defined $cmdline_use_filelist;
+ for my $f (@syncfiles) {
+ my $p = substr($f->[2], 0, 1) eq '0' ? '/' : '';
+ print "$f->[0]$p\n";
+ }
+ exit(0);
+}
+
+# get the lock
+
+die("need a destination\n") unless defined $basedir;
+mkdir_p("$basedir/drpmsync");
+sysopen(LOCK, "$basedir/drpmsync/lock", POSIX::O_RDWR|POSIX::O_CREAT, 0666) || die("$basedir/drpmsync/lock: $!\n");
+if (!flock(LOCK, LOCK_EX | LOCK_NB)) {
+ my $lockuser = '';
+ sysread(LOCK, $lockuser, 1024);
+ close LOCK;
+ $lockuser = "somebody else\n" unless $lockuser =~ /.*[\S].*\n$/s;
+ print "update already in progress by $lockuser";
+ exit(1);
+}
+truncate(LOCK, 0);
+syswrite(LOCK, "drpmsync[$$]\@$synchost\n");
+
+my ($oldstamp1, $oldstamp2);
+if (open(STAMP, '<', "$basedir/drpmsync/timestamp")) {
+ my $s = '';
+ if ((sysread(STAMP, $s, 16) || 0) == 16 && $s !~ /[^0-9a-f]/) {
+ $oldstamp1 = substr($s, 0, 8);
+ $oldstamp2 = substr($s, 8, 8);
+ }
+ close STAMP;
+}
+$oldstamp1 ||= "00000000";
+
+# clear the wip
+if (opendir(WIP, "$basedir/drpmsync/wip")) {
+ for (readdir(WIP)) {
+ next if $_ eq '.' || $_ eq '..';
+ unlink("$basedir/drpmsync/wip/$_") || die("unlink $basedir/drpmsync/wip/$_: $!\n");
+ }
+ closedir(WIP);
+}
+
+readcache("$basedir/drpmsync/cache");
+print "getting state of local tree...\n";
+findfiles($basedir, '', 1);
+print("cache: $cachehits hits, $cachemisses misses\n");
+writecache("$basedir/drpmsync/cache");
+
+if (!@config_source) {
+ # just a cache update...
+ unlink("$basedir/drpmsync/lock");
+ close(LOCK);
+ exit(0);
+}
+
+mkdir_p("$basedir/drpmsync/wip");
+
+$SIG{'ALRM'} = sub {die("network timeout\n");};
+
+my @syncfiles;
+find_source(\@syncfiles, $cmdline_norecurse || $cmdline_use_filelist, 1, @config_source);
+filelist_from_file(\@syncfiles, $cmdline_use_filelist) if defined $cmdline_use_filelist;
+
+$config_recvlog = "$basedir/drpmsync/$config_recvlog" if $config_recvlog && $config_recvlog !~ /^\//;
+if ($config_recvlog) {
+ open(RECVLOG, '>>', $config_recvlog) || die("$config_recvlog: $!\n");
+ select(RECVLOG);
+ $| = 1;
+ select(STDOUT);
+ recvlog("started update from $syncurl");
+ $SIG{'__DIE__'} = sub {
+ my $err = $_[0];
+ $err =~ s/\n$//s;
+ recvlog($err);
+ die("$err\n");
+ };
+}
+
+if ($oldstamp1 ne '00000000' && $oldstamp1 gt $newstamp1) {
+ if ($newstamp1 eq '00000000') {
+ die("remote tree is incomplete\n");
+ }
+ die("remote tree is older than local tree (last completion): ".toiso(hex($newstamp1))." < ".toiso(hex($oldstamp1))."\n");
+}
+if ($oldstamp2 && $oldstamp2 gt $newstamp2) {
+ die("remote tree is older than local tree (last start): ".toiso(hex($newstamp2))." < ".toiso(hex($oldstamp2))."\n");
+}
+open(STAMP, '>', "$basedir/drpmsync/timestamp.new") || die("$basedir/drpmsync/timestamp.new: $!\n");
+print STAMP "$oldstamp1$newstamp2\n";
+close STAMP;
+rename("$basedir/drpmsync/timestamp.new", "$basedir/drpmsync/timestamp");
+
+# change all directories to at least user rwx
+for (@syncfiles) {
+ next if $_->[2] !~ /^0/;
+ next if (hex(substr($_->[2], 0, 4)) & 0700) == 0700;
+ $_->[2] = sprintf("0%03x", hex(substr($_->[2], 0, 4)) | 0700).substr($_->[2], 4);
+}
+
+printf "local: ".@files." entries\n";
+printf "remote: ".@syncfiles." entries\n";
+
+rsync_adapt_filelist(\@syncfiles) if $syncproto eq 'rsync';
+
+%files = map {$_->[0] => $_} @files;
+%syncfiles = map {$_->[0] => $_} @syncfiles;
+
+# 1) create all new directories
+# 2) delete all dirs that are now files
+# 3) get all rpms and update/delete the associated files
+# 4) update all other files
+# 5) delete all files/rpms/directories
+# 6) set mode/time of directories
+
+# part 1
+for my $dir (grep {@$_ == 3} @syncfiles) {
+ my $d = $files{$dir->[0]};
+ if ($d) {
+ next if $d->[2] =~ /^0/;
+ recvlog_print("- $d->[0]");
+ unlink("$basedir/$d->[0]") || die("unlink $basedir/$d->[0]: $!\n");
+ }
+ recvlog_print("+ $dir->[0]");
+ mkdir("$basedir/$dir->[0]", 0755) || die("mkdir $basedir/$dir->[0]: $!\n");
+ fixmodetime("$basedir/$dir->[0]", $dir->[2]);
+ my @s = lstat("$basedir/$dir->[0]");
+ die("$basedir/$dir->[0]: $!\n") unless @s;
+ $files{$dir->[0]} = [ $dir->[0], "$s[9]/$s[7]/$s[1]", sprintf("0%03x%08x", ($s[2] & 07777), $s[9]) ];
+ dirchanged($dir->[0]);
+}
+
+# part 2
+@files = sort {$a->[0] cmp $b->[0]} values %files;
+for my $dir (grep {@$_ == 3} @files) {
+ my $sd = $syncfiles{$dir->[0]};
+ next if !$sd || $sd->[2] =~ /^0/;
+ next unless $files{$dir->[0]};
+ my @subf = grep {$_->[0] =~ /^\Q$dir->[0]\E\//} @files;
+ unshift @subf, $dir;
+ @subf = reverse @subf;
+ for my $subf (@subf) {
+ recvlog_print("- $subf->[0]");
+ if ($subf->[2] =~ /^0/) {
+ rmdir("$basedir/$subf->[0]") || die("rmdir $basedir/$subf->[0]: $!\n");
+ } else {
+ unlink("$basedir/$subf->[0]") || die("unlink $basedir/$subf->[0]: $!\n");
+ }
+ repo_del("$basedir/$subf->[0]", $subf) if $config_repo;
+ delete $files{$subf->[0]};
+ }
+ dirchanged($dir->[0]);
+ @files = sort {$a->[0] cmp $b->[0]} values %files;
+}
+
+# part 3
+my @syncrpms = grep {@$_ > 5} @syncfiles;
+# sort by rpm built date
+@syncrpms = sort {$a->[4] cmp $b->[4]} @syncrpms;
+for my $rpm (@syncrpms) {
+ update($basedir, $rpm);
+ # update meta file(s)
+ my $rpmname = $rpm->[0];
+ $rpmname =~ s/\.[sr]pm$//;
+ for my $afn ("$rpmname.changes", "$rpmname-MD5SUMS.meta", "$rpmname-MD5SUMS.srcdir") {
+ my $sd = $syncfiles{$afn};
+ my $d = $files{$afn};
+ next if !$d && !$sd;
+ if ($d && !$sd) {
+ next if $d->[2] =~ /^0/;
+ recvlog_print("- $d->[0]");
+ unlink("$basedir/$d->[0]") || die("unlink $basedir/$d->[0]: $!\n");
+ dirchanged($d->[0]);
+ delete $files{$d->[0]};
+ } else {
+ update($basedir, $sd);
+ }
+ }
+}
+
+# part 4
+for my $file (grep {@$_ == 4} @syncfiles) {
+ update($basedir, $file);
+}
+
+checkjob() if $runningjob;
+
+send_fin();
+
+# part 5
+@files = sort {$a->[0] cmp $b->[0]} values %files;
+for my $file (grep {!$syncfiles{$_->[0]}} reverse @files) {
+ recvlog_print("- $file->[0]");
+ if ($file->[2] =~ /^0/) {
+ rmdir("$basedir/$file->[0]") || die("rmdir $basedir/$file->[0]: $!\n");
+ } else {
+ unlink("$basedir/$file->[0]") || die("unlink $basedir/$file->[0]: $!\n");
+ repo_del("$basedir/$file->[0]", $file) if $config_repo;
+ }
+ dirchanged($file->[0]);
+ delete $files{$file->[0]};
+}
+
+# part 6
+for my $dir (grep {@$_ == 3} @syncfiles) {
+ my $d = $files{$dir->[0]};
+ next if !$d || $d->[2] eq $dir->[2];
+ fixmodetime("$basedir/$dir->[0]", $dir->[2]);
+}
+
+@files = sort {$a->[0] cmp $b->[0]} values %files;
+writecache("$basedir/drpmsync/cache");
+
+if (!$had_gone) {
+ open(STAMP, '>', "$basedir/drpmsync/timestamp.new") || die("$basedir/drpmsync/timestamp.new: $!\n");
+ print STAMP "$newstamp1$newstamp2\n";
+ close STAMP;
+ rename("$basedir/drpmsync/timestamp.new", "$basedir/drpmsync/timestamp");
+}
+
+if (defined($config_delta_max_age)) {
+ print "removing outdated deltas...\n";
+ my $nold = 0;
+ my $cut = time() - 24*60*60*$config_delta_max_age;
+ if (opendir(PACKS, "$basedir/drpmsync/deltas")) {
+ my @packs = readdir(PACKS);
+ closedir(PACKS);
+ for my $pack (@packs) {
+ next if $pack eq '.' || $pack eq '..';
+ next unless opendir(DELTAS, "$basedir/drpmsync/deltas/$pack");
+ my @deltas = readdir(DELTAS);
+ closedir(DELTAS);
+ for my $delta (@deltas) {
+ next if $delta eq '.' || $delta eq '..';
+ my @s = stat "$basedir/drpmsync/deltas/$pack/$delta";
+ next unless @s;
+ next if $s[9] >= $cut;
+ unlink("$basedir/drpmsync/deltas/$pack/$delta") || die("unlink $basedir/drpmsync/deltas/$pack/$delta: $!\n");
+ $nold++;
+ }
+ }
+ }
+ recvlog_print("removed $nold deltarpms") if $nold;
+}
+my $net_kbsec = 0;
+$net_kbsec = int($net_recv_bytes / 1024 / $net_spent_time) if $net_spent_time;
+recvlog("update finished $txbytes/$rvbytes/$sabytes $net_kbsec");
+close(RECVLOG) if $config_recvlog;
+unlink("$basedir/drpmsync/lock");
+close(LOCK);
+if ($sabytes == 0) {
+ printf "update finished, sent %.1f K, received %.1f M\n", $txbytes / 1000, $rvbytes / 1000000;
+} elsif ($sabytes < 0) {
+ printf "update finished, sent %.1f K, received %.1f M, deltarpm excess %.1f M\n", $txbytes / 1000, $rvbytes / 1000000, (-$sabytes) /1000000;
+} else {
+ printf "update finished, sent %.1f K, received %.1f M, deltarpm savings %.1f M\n", $txbytes / 1000, $rvbytes / 1000000, $sabytes /1000000;
+}
+printf "network throughput %d kbyte/sec\n", $net_kbsec if $net_spent_time;
+exit 24 if $had_gone;