blob: faa113a0cd3d35bca321d2ffff7bb8a3478e08d6 [file] [log] [blame]
#!/usr/bin/perl
# -*-perl-*-
#
# Copyright © 1996 Andy Guy <awpguy@acs.ucalgary.ca>
# Copyright © 1998 Martin Schulze <joey@infodrom.north.de>
# Copyright © 1999, 2009 Raphaël Hertzog <hertzog@debian.org>
#
# This program has been distributed under the terms of the GNU GPL.
use strict;
use warnings;
use vars qw(%config $ftp);
#use diagnostics;
use lib '/usr/lib/perl5/Debian';
use lib '/usr/share/perl5/Debian';
eval q{
use Net::FTP;
use File::Path;
use File::Basename;
use File::Find;
use Data::Dumper;
};
if ($@) {
print STDERR "Please install the 'perl' package if you want to use the\n" .
"FTP access method of dselect.\n\n";
exit 1;
}
use Dselect::Ftp;
# exit value
my $exit = 0;
# deal with arguments
my $vardir = $ARGV[0];
my $method = $ARGV[1];
my $option = $ARGV[2];
if ($option eq "manual" ) {
print "manual mode not supported yet\n";
exit 1;
}
#print "vardir: $vardir, method: $method, option: $option\n";
my $methdir = "$vardir/methods/ftp";
# get info from control file
read_config("$methdir/vars");
chdir "$methdir";
mkpath(["$methdir/$config{'dldir'}"], 0, 0755);
#Read md5sums already calculated
my %md5sums;
if (-f "$methdir/md5sums") {
local $/;
open(MD5SUMS, "$methdir/md5sums") ||
die "Couldn't read file $methdir/md5sums";
my $code = <MD5SUMS>;
close MD5SUMS;
use vars qw($VAL1);
my $res = eval $code;
if ($@) {
die "Couldn't eval $methdir/md5sums content: $@\n";
}
if (ref($res)) { %md5sums = %{$res} }
}
# get a block
# returns a ref to a hash containing flds->fld contents
# white space from the ends of lines is removed and newlines added
# (no trailing newline).
# die's if something unexpected happens
sub getblk {
my $fh = shift;
my %flds;
my $fld;
while (<$fh>) {
if ( ! /^$/ ) {
FLDLOOP: while (1) {
if ( /^(\S+):\s*(.*)\s*$/ ) {
$fld = lc($1);
$flds{$fld} = $2;
while (<$fh>) {
if ( /^$/ ) {
return %flds;
} elsif ( /^(\s.*)$/ ) {
$flds{$fld} = $flds{$fld} . "\n" . $1;
} else {
next FLDLOOP;
}
}
return %flds;
} else {
die "Expected a start of field line, but got:\n$_";
}
}
}
}
return %flds;
}
# process status file
# create curpkgs hash with version (no version implies not currently installed)
# of packages we want
print "Processing status file...\n";
my %curpkgs;
sub procstatus {
my (%flds, $fld);
open (STATUS, "$vardir/status") or die "Could not open status file";
while (%flds = getblk(\*STATUS), %flds) {
if($flds{'status'} =~ /^install ok/) {
my $cs = (split(/ /, $flds{'status'}))[2];
if(($cs eq "not-installed") ||
($cs eq "half-installed") ||
($cs eq "config-files")) {
$curpkgs{$flds{'package'}} = "";
} else {
$curpkgs{$flds{'package'}} = $flds{'version'};
}
}
}
close(STATUS);
}
procstatus();
sub dcmpvers {
my($a, $p, $b) = @_;
my ($r);
$r = system("/usr/bin/dpkg", "--compare-versions", "$a", "$p", "$b");
$r = $r/256;
if ($r == 0) {
return 1;
} elsif ($r == 1) {
return 0;
}
die "dpkg --compare-versions $a $p $b - failed with $r";
}
# process package files, looking for packages to install
# create a hash of these packages pkgname => version, filenames...
# filename => md5sum, size
# for all packages
my %pkgs;
my %pkgfiles;
sub procpkgfile {
my $fn = shift;
my $site = shift;
my $dist = shift;
my(@files,@sizes,@md5sums,$pkg,$ver,$fl,$nfs,$fld);
my(%flds);
open(PKGFILE, "$fn") or die "Could not open package file $fn";
while(%flds = getblk(\*PKGFILE), %flds) {
$pkg = $flds{'package'};
$ver = $curpkgs{$pkg};
@files = split(/[\s\n]+/, $flds{'filename'});
@sizes = split(/[\s\n]+/, $flds{'size'});
@md5sums = split(/[\s\n]+/, $flds{'md5sum'});
if ( defined($ver) && ( ($ver eq "") || dcmpvers( $ver, "lt", $flds{'version'} ) )) {
$pkgs{$pkg} = [ $flds{'version'}, [ @files ], $site ];
$curpkgs{$pkg} = $flds{'version'};
}
$nfs = scalar(@files);
if(($nfs != scalar(@sizes)) || ($nfs != scalar(@md5sums)) ) {
print "Different number of filenames, sizes and md5sums for $flds{'package'}\n";
} else {
my $i = 0;
foreach $fl (@files) {
$pkgfiles{$fl} = [ $md5sums[$i], $sizes[$i], $site, $dist ];
$i++;
}
}
}
}
print "\nProcessing Package files...\n";
my ($dist,$site,$fn,$i,$j);
$i = 0;
foreach $site (@{$config{'site'}}) {
$j = 0;
foreach $dist (@{$site->[2]}) {
$fn = $dist;
$fn =~ tr#/#_#;
$fn = "Packages.$site->[0].$fn";
if (-f $fn) {
print " $site->[0] $dist...\n";
procpkgfile($fn,$i,$j);
} else {
print "Could not find packages file for $site->[0] $dist distribution (re-run Update)\n"
}
$j++;
}
$i++;
}
my $dldir = $config{'dldir'};
# md5sum
sub md5sum($) {
my $fn = shift;
my $m = `md5sum $fn`;
$m = (split(" ", $m))[0];
$md5sums{"$dldir/$fn"} = $m;
return $m;
}
# construct list of files to get
# hash of filenames => size of downloaded part
# query user for each paritial file
print "\nConstructing list of files to get...\n";
my %downloads;
my ($pkg, $dir, @info, @files, $csize, $size);
my $totsize = 0;
foreach $pkg (keys(%pkgs)) {
@files = @{$pkgs{$pkg}[1]};
foreach $fn (@files) {
#Look for a partial file
if (-f "$dldir/$fn.partial") {
rename "$dldir/$fn.partial", "$dldir/$fn";
}
$dir = dirname($fn);
if(! -d "$dldir/$dir") {
mkpath(["$dldir/$dir"], 0, 0755);
}
@info = @{$pkgfiles{$fn}};
$csize = int($info[1]/1024)+1;
if(-f "$dldir/$fn") {
$size = -s "$dldir/$fn";
if($info[1] > $size) {
# partial download
if(yesno("y", "continue file: $fn (" . nb($size) ."/" .
nb($info[1]). ")")) {
$downloads{$fn} = $size;
$totsize += $csize - int($size/1024);
} else {
$downloads{$fn} = 0;
$totsize += $csize;
}
} else {
# check md5sum
if (! exists $md5sums{"$dldir/$fn"}) {
$md5sums{"$dldir/$fn"} = md5sum("$dldir/$fn");
}
if ($md5sums{"$dldir/$fn"} eq $info[0]) {
print "already got: $fn\n";
} else {
print "corrupted: $fn\n";
$downloads{$fn} = 0;
}
}
} else {
my $ffn = $fn;
$ffn =~ s/binary-[^\/]+/.../;
print "want: " .
$config{'site'}[$pkgfiles{$fn}[2]][0] . " $ffn (${csize}k)\n";
$downloads{$fn} = 0;
$totsize += $csize;
}
}
}
my $avsp = `df -Pk $dldir| awk '{ print \$4}' | tail -n 1`;
chomp $avsp;
print "\nApproximate total space required: ${totsize}k\n";
print "Available space in $dldir: ${avsp}k\n";
#$avsp = `df -k $::dldir| paste -s | awk '{ print \$11}'`;
#chomp $avsp;
if($totsize == 0) {
print "Nothing to get.";
} else {
if($totsize > $avsp) {
print "Space required is greater than available space,\n";
print "you will need to select which items to get.\n";
}
# ask user which files to get
if(($totsize > $avsp) || yesno("n", "Do you want to select the files to get")) {
$totsize = 0;
my @files = sort(keys(%downloads));
my $fn;
my $def = "y";
foreach $fn (@files) {
my @info = @{$pkgfiles{$fn}};
my $csize = int($info[1] / 1024) + 1;
my $rsize = int(($info[1] - $downloads{$fn}) / 1024) + 1;
if ($rsize + $totsize > $avsp) {
print "no room for: $fn\n";
delete $downloads{$fn};
} else {
if(yesno($def, $downloads{$fn}
? "download: $fn ${rsize}k/${csize}k (total = ${totsize}k)"
: "download: $fn ${rsize}k (total = ${totsize}k)")) {
$def = "y";
$totsize += $rsize;
} else {
$def = "n";
delete $downloads{$fn};
}
}
}
}
}
sub download() {
my $i = 0;
my ($site, $ftp);
foreach $site (@{$config{'site'}}) {
my @getfiles = grep { $pkgfiles{$_}[2] == $i } keys %downloads;
my @pre_dist = (); # Directory to add before $fn
#Scan distributions for looking at "(../)+/dir/dir"
my ($n,$cp);
$cp = -1;
foreach (@{$site->[2]}) {
$cp++;
$pre_dist[$cp] = "";
$n = (s#\.\./#../#g);
next if (! $n);
if (m#^((?:\.\./){$n}(?:[^/]+/){$n})#) {
$pre_dist[$cp] = $1;
}
}
if (! @getfiles) { $i++; next; }
$ftp = do_connect ($site->[0], #$::ftpsite,
$site->[4], #$::username,
$site->[5], #$::password,
$site->[1], #$::ftpdir,
$site->[3], #$::passive,
$config{'use_auth_proxy'},
$config{'proxyhost'},
$config{'proxylogname'},
$config{'proxypassword'});
$::ftp = $ftp;
local $SIG{'INT'} = sub { die "Interrupted !\n"; };
my ($fn,$rsize,$res,$pre);
foreach $fn (@getfiles) {
$pre = $pre_dist[$pkgfiles{$fn}[3]] || "";
if ($downloads{$fn}) {
$rsize = ${pkgfiles{$fn}}[1] - $downloads{$fn};
print "getting: $pre$fn (". nb($rsize) . "/" .
nb($pkgfiles{$fn}[1]) . ")\n";
} else {
print "getting: $pre$fn (". nb($pkgfiles{$fn}[1]) . ")\n";
}
$res = $ftp->get("$pre$fn", "$dldir/$fn", $downloads{$fn});
if(! $res) {
my $r = $ftp->code();
print $ftp->message() . "\n";
if (!($r == 550 || $r == 450)) {
return 1;
} else {
#Try to find another file or this package
print "Looking for another version of the package...\n";
my ($dir,$package) = ($fn =~ m#^(.*)/([^/]+)_[^/]+.deb$#);
my $protected = $package;
$protected =~ s/\+/\\\+/g;
my $list = $ftp->ls("$pre$dir");
if ($ftp->ok() && ref($list)) {
foreach (@{$list}) {
if (m/($dir\/${protected}_[^\/]+.deb)/i) {
print "Package found : $_\n";
print "getting: $_ (size not known)\n";
$res = $ftp->get($_, "$dldir/$1");
if (! $res) {
$r = $ftp->code();
print $ftp->message() . "\n";
return 1 if ($r != 550 and $r != 450);
}
}
}
}
}
}
# fully got, remove it from list in case we have to re-download
delete $downloads{$fn};
}
$ftp->quit();
$i++;
}
return 0;
}
# download stuff (protect from ^C)
if($totsize != 0) {
if(yesno("y", "\nDo you want to download the required files")) {
DOWNLOAD_TRY: while (1) {
print "Downloading files... use ^C to stop\n";
eval {
if ((download() == 1) && yesno("y", "\nDo you want to retry downloading at once")) {
next DOWNLOAD_TRY;
}
};
if($@ =~ /Interrupted|Timeout/i ) {
# close the FTP connection if needed
if ((ref($::ftp) =~ /Net::FTP/) and ($@ =~ /Interrupted/i)) {
$::ftp->abort();
$::ftp->quit();
undef $::ftp;
}
print "FTP ERROR\n";
if (yesno("y", "\nDo you want to retry downloading at once")) {
# get the first $fn that foreach would give:
# this is the one that got interrupted.
my $ffn;
MY_ITER: foreach $ffn (keys(%downloads)) {
$fn = $ffn;
last MY_ITER;
}
my $size = -s "$dldir/$fn";
# partial download
if(yesno("y", "continue file: $fn (at $size)")) {
$downloads{$fn} = $size;
} else {
$downloads{$fn} = 0;
}
next DOWNLOAD_TRY;
} else {
$exit = 1;
last DOWNLOAD_TRY;
}
} elsif ($@) {
print "An error occurred ($@) : stopping download\n";
}
last DOWNLOAD_TRY;
}
}
}
# remove duplicate packages (keep latest versions)
# move half downloaded files out of the way
# delete corrupted files
print "\nProcessing downloaded files...(for corrupt/old/partial)\n";
my %vers; # package => version
my %files; # package-version => files...
# check a deb or split deb file
# return 1 if it a deb file, 2 if it is a split deb file
# else 0
sub chkdeb($) {
my ($fn) = @_;
# check to see if it is a .deb file
if(!system("dpkg-deb --info $fn 2>&1 >/dev/null && dpkg-deb --contents $fn 2>&1 >/dev/null")) {
return 1;
} elsif(!system("dpkg-split --info $fn 2>&1 >/dev/null")) {
return 2;
}
return 0;
}
sub getdebinfo($) {
my ($fn) = @_;
my $type = chkdeb($fn);
my ($pkg, $ver);
if($type == 1) {
open(PKGFILE, "dpkg-deb --field $fn |");
my %fields = getblk(\*PKGFILE);
close(PKGFILE);
$pkg = $fields{'package'};
$ver = $fields{'version'};
if($fields{'package_revision'}) { $ver .= '-' . $fields{'package_revision'}; }
return $pkg, $ver;
} elsif ( $type == 2) {
open(PKGFILE, "dpkg-split --info $fn|");
while(<PKGFILE>) {
/Part of package:\s*(\S+)/ and $pkg = $+;
/\.\.\. version:\s*(\S+)/ and $ver = $+;
}
close(PKGFILE);
return $pkg, $ver;
}
print "could not figure out type of $fn\n";
return $pkg, $ver;
}
# process deb file to make sure we only keep latest versions
sub prcdeb($$) {
my ($dir, $fn) = @_;
my ($pkg, $ver) = getdebinfo($fn);
if(!defined($pkg) || !defined($ver)) {
print "could not get package info from file\n";
return 0;
}
if($vers{$pkg}) {
if(dcmpvers($vers{$pkg}, "eq", $ver)) {
$files{$pkg . $ver} = [ $files{$pkg . $ver }, "$dir/$fn" ];
} elsif (dcmpvers($vers{$pkg}, "gt", $ver)) {
print "old version\n";
unlink $fn;
} else { # else $ver is gt current version
my ($c);
foreach $c (@{$files{$pkg . $vers{$pkg}}}) {
print "replaces: $c\n";
unlink "$vardir/methods/ftp/$dldir/$c";
}
$vers{$pkg} = $ver;
$files{$pkg . $ver} = [ "$dir/$fn" ];
}
} else {
$vers{$pkg} = $ver;
$files{$pkg . $ver} = [ "$dir/$fn" ];
}
}
sub prcfile() {
my ($fn) = $_;
if (-f $fn and $fn ne '.') {
my $dir = ".";
if (length($File::Find::dir) > length($dldir)) {
$dir = substr($File::Find::dir, length($dldir)+1);
}
print "$dir/$fn\n";
if(defined($pkgfiles{"$dir/$fn"})) {
my @info = @{$pkgfiles{"$dir/$fn"}};
my $size = -s $fn;
if($size == 0) {
print "zero length file\n";
unlink $fn;
} elsif($size < $info[1]) {
print "partial file\n";
rename $fn, "$fn.partial";
} elsif(( (exists $md5sums{"$dldir/$fn"})
and ($md5sums{"$dldir/$fn"} ne $info[0]) )
or
(md5sum($fn) ne $info[0])) {
print "corrupt file\n";
unlink $fn;
} else {
prcdeb($dir, $fn);
}
} elsif($fn =~ /.deb$/) {
if(chkdeb($fn)) {
prcdeb($dir, $fn);
} else {
print "corrupt file\n";
unlink $fn;
}
} else {
print "non-debian file\n";
}
}
}
find(\&prcfile, "$dldir/");
# install .debs
if(yesno("y", "\nDo you want to install the files fetched")) {
print "Installing files...\n";
#Installing pre-dependent package before !
my (@flds, $package, @filename, $r);
while (@flds = `dpkg --predep-package`, $? == 0) {
foreach (@flds) {
s/\s*\n//;
$package= $_ if s/^Package: //i;
@filename= split(/ +/,$_) if s/^Filename: //i;
}
@filename = map { "$dldir/$_" } @filename;
next if (! @filename);
$r = system('dpkg', '-iB', '--', @filename);
if ($r) { print "DPKG ERROR\n"; $exit = 1; }
}
#Installing other packages after
$r = system("dpkg", "-iGREOB", $dldir);
if($r) {
print "DPKG ERROR\n";
$exit = 1;
}
}
sub removeinstalled {
my $fn = $_;
if (-f $fn and $fn ne '.') {
my $dir = ".";
if (length($File::Find::dir) > length($dldir)) {
$dir = substr($File::Find::dir, length($dldir)+1);
}
if($fn =~ /.deb$/) {
my($pkg, $ver) = getdebinfo($fn);
if(!defined($pkg) || !defined($ver)) {
print "Could not get info for: $dir/$fn\n";
} else {
if($curpkgs{$pkg} and dcmpvers($ver, "le", $curpkgs{$pkg})) {
print "deleting: $dir/$fn\n";
unlink $fn;
} else {
print "leaving: $dir/$fn\n";
}
}
} else {
print "non-debian: $dir/$fn\n";
}
}
}
# remove .debs that have been installed (query user)
# first need to reprocess status file
if(yesno("y", "\nDo you wish to delete the installed package (.deb) files?")) {
print "Removing installed files...\n";
%curpkgs = ();
procstatus();
find(\&removeinstalled, "$dldir/");
}
# remove whole ./debian directory if user wants to
if(yesno("n", "\nDo you want to remove $dldir directory?")) {
rmtree("$dldir");
}
#Store useful md5sums
foreach (keys %md5sums) {
next if (-f $_);
delete $md5sums{$_};
}
open(MD5SUMS, ">$methdir/md5sums") ||
die "Can't open $methdir/md5sums in write mode : $!\n";
print MD5SUMS Dumper(\%md5sums);
close MD5SUMS;
exit $exit;