blob: faa113a0cd3d35bca321d2ffff7bb8a3478e08d6 [file] [log] [blame]
Igor Sarkisov980c0122020-10-06 12:16:43 -07001#!/usr/bin/perl
2# -*-perl-*-
3#
4# Copyright © 1996 Andy Guy <awpguy@acs.ucalgary.ca>
5# Copyright © 1998 Martin Schulze <joey@infodrom.north.de>
6# Copyright © 1999, 2009 Raphaël Hertzog <hertzog@debian.org>
7#
8# This program has been distributed under the terms of the GNU GPL.
9
10use strict;
11use warnings;
12
13use vars qw(%config $ftp);
14#use diagnostics;
15
16use lib '/usr/lib/perl5/Debian';
17use lib '/usr/share/perl5/Debian';
18
19eval q{
20 use Net::FTP;
21 use File::Path;
22 use File::Basename;
23 use File::Find;
24 use Data::Dumper;
25};
26if ($@) {
27 print STDERR "Please install the 'perl' package if you want to use the\n" .
28 "FTP access method of dselect.\n\n";
29 exit 1;
30}
31
32use Dselect::Ftp;
33
34# exit value
35my $exit = 0;
36
37# deal with arguments
38my $vardir = $ARGV[0];
39my $method = $ARGV[1];
40my $option = $ARGV[2];
41
42if ($option eq "manual" ) {
43 print "manual mode not supported yet\n";
44 exit 1;
45}
46#print "vardir: $vardir, method: $method, option: $option\n";
47
48my $methdir = "$vardir/methods/ftp";
49
50# get info from control file
51read_config("$methdir/vars");
52
53chdir "$methdir";
54mkpath(["$methdir/$config{'dldir'}"], 0, 0755);
55
56
57#Read md5sums already calculated
58my %md5sums;
59if (-f "$methdir/md5sums") {
60 local $/;
61 open(MD5SUMS, "$methdir/md5sums") ||
62 die "Couldn't read file $methdir/md5sums";
63 my $code = <MD5SUMS>;
64 close MD5SUMS;
65 use vars qw($VAL1);
66 my $res = eval $code;
67 if ($@) {
68 die "Couldn't eval $methdir/md5sums content: $@\n";
69 }
70 if (ref($res)) { %md5sums = %{$res} }
71}
72
73# get a block
74# returns a ref to a hash containing flds->fld contents
75# white space from the ends of lines is removed and newlines added
76# (no trailing newline).
77# die's if something unexpected happens
78sub getblk {
79 my $fh = shift;
80 my %flds;
81 my $fld;
82 while (<$fh>) {
83 if ( ! /^$/ ) {
84 FLDLOOP: while (1) {
85 if ( /^(\S+):\s*(.*)\s*$/ ) {
86 $fld = lc($1);
87 $flds{$fld} = $2;
88 while (<$fh>) {
89 if ( /^$/ ) {
90 return %flds;
91 } elsif ( /^(\s.*)$/ ) {
92 $flds{$fld} = $flds{$fld} . "\n" . $1;
93 } else {
94 next FLDLOOP;
95 }
96 }
97 return %flds;
98 } else {
99 die "Expected a start of field line, but got:\n$_";
100 }
101 }
102 }
103 }
104 return %flds;
105}
106
107# process status file
108# create curpkgs hash with version (no version implies not currently installed)
109# of packages we want
110print "Processing status file...\n";
111my %curpkgs;
112sub procstatus {
113 my (%flds, $fld);
114 open (STATUS, "$vardir/status") or die "Could not open status file";
115 while (%flds = getblk(\*STATUS), %flds) {
116 if($flds{'status'} =~ /^install ok/) {
117 my $cs = (split(/ /, $flds{'status'}))[2];
118 if(($cs eq "not-installed") ||
119 ($cs eq "half-installed") ||
120 ($cs eq "config-files")) {
121 $curpkgs{$flds{'package'}} = "";
122 } else {
123 $curpkgs{$flds{'package'}} = $flds{'version'};
124 }
125 }
126 }
127 close(STATUS);
128}
129procstatus();
130
131sub dcmpvers {
132 my($a, $p, $b) = @_;
133 my ($r);
134 $r = system("/usr/bin/dpkg", "--compare-versions", "$a", "$p", "$b");
135 $r = $r/256;
136 if ($r == 0) {
137 return 1;
138 } elsif ($r == 1) {
139 return 0;
140 }
141 die "dpkg --compare-versions $a $p $b - failed with $r";
142}
143
144# process package files, looking for packages to install
145# create a hash of these packages pkgname => version, filenames...
146# filename => md5sum, size
147# for all packages
148my %pkgs;
149my %pkgfiles;
150sub procpkgfile {
151 my $fn = shift;
152 my $site = shift;
153 my $dist = shift;
154 my(@files,@sizes,@md5sums,$pkg,$ver,$fl,$nfs,$fld);
155 my(%flds);
156 open(PKGFILE, "$fn") or die "Could not open package file $fn";
157 while(%flds = getblk(\*PKGFILE), %flds) {
158 $pkg = $flds{'package'};
159 $ver = $curpkgs{$pkg};
160 @files = split(/[\s\n]+/, $flds{'filename'});
161 @sizes = split(/[\s\n]+/, $flds{'size'});
162 @md5sums = split(/[\s\n]+/, $flds{'md5sum'});
163 if ( defined($ver) && ( ($ver eq "") || dcmpvers( $ver, "lt", $flds{'version'} ) )) {
164 $pkgs{$pkg} = [ $flds{'version'}, [ @files ], $site ];
165 $curpkgs{$pkg} = $flds{'version'};
166 }
167 $nfs = scalar(@files);
168 if(($nfs != scalar(@sizes)) || ($nfs != scalar(@md5sums)) ) {
169 print "Different number of filenames, sizes and md5sums for $flds{'package'}\n";
170 } else {
171 my $i = 0;
172 foreach $fl (@files) {
173 $pkgfiles{$fl} = [ $md5sums[$i], $sizes[$i], $site, $dist ];
174 $i++;
175 }
176 }
177 }
178}
179
180print "\nProcessing Package files...\n";
181my ($dist,$site,$fn,$i,$j);
182$i = 0;
183foreach $site (@{$config{'site'}}) {
184 $j = 0;
185 foreach $dist (@{$site->[2]}) {
186 $fn = $dist;
187 $fn =~ tr#/#_#;
188 $fn = "Packages.$site->[0].$fn";
189 if (-f $fn) {
190 print " $site->[0] $dist...\n";
191 procpkgfile($fn,$i,$j);
192 } else {
193 print "Could not find packages file for $site->[0] $dist distribution (re-run Update)\n"
194 }
195 $j++;
196 }
197 $i++;
198}
199
200my $dldir = $config{'dldir'};
201# md5sum
202sub md5sum($) {
203 my $fn = shift;
204 my $m = `md5sum $fn`;
205 $m = (split(" ", $m))[0];
206 $md5sums{"$dldir/$fn"} = $m;
207 return $m;
208}
209
210# construct list of files to get
211# hash of filenames => size of downloaded part
212# query user for each paritial file
213print "\nConstructing list of files to get...\n";
214my %downloads;
215my ($pkg, $dir, @info, @files, $csize, $size);
216my $totsize = 0;
217foreach $pkg (keys(%pkgs)) {
218 @files = @{$pkgs{$pkg}[1]};
219 foreach $fn (@files) {
220 #Look for a partial file
221 if (-f "$dldir/$fn.partial") {
222 rename "$dldir/$fn.partial", "$dldir/$fn";
223 }
224 $dir = dirname($fn);
225 if(! -d "$dldir/$dir") {
226 mkpath(["$dldir/$dir"], 0, 0755);
227 }
228 @info = @{$pkgfiles{$fn}};
229 $csize = int($info[1]/1024)+1;
230 if(-f "$dldir/$fn") {
231 $size = -s "$dldir/$fn";
232 if($info[1] > $size) {
233 # partial download
234 if(yesno("y", "continue file: $fn (" . nb($size) ."/" .
235 nb($info[1]). ")")) {
236 $downloads{$fn} = $size;
237 $totsize += $csize - int($size/1024);
238 } else {
239 $downloads{$fn} = 0;
240 $totsize += $csize;
241 }
242 } else {
243 # check md5sum
244 if (! exists $md5sums{"$dldir/$fn"}) {
245 $md5sums{"$dldir/$fn"} = md5sum("$dldir/$fn");
246 }
247 if ($md5sums{"$dldir/$fn"} eq $info[0]) {
248 print "already got: $fn\n";
249 } else {
250 print "corrupted: $fn\n";
251 $downloads{$fn} = 0;
252 }
253 }
254 } else {
255 my $ffn = $fn;
256 $ffn =~ s/binary-[^\/]+/.../;
257 print "want: " .
258 $config{'site'}[$pkgfiles{$fn}[2]][0] . " $ffn (${csize}k)\n";
259 $downloads{$fn} = 0;
260 $totsize += $csize;
261 }
262 }
263}
264
265my $avsp = `df -Pk $dldir| awk '{ print \$4}' | tail -n 1`;
266chomp $avsp;
267
268print "\nApproximate total space required: ${totsize}k\n";
269print "Available space in $dldir: ${avsp}k\n";
270
271#$avsp = `df -k $::dldir| paste -s | awk '{ print \$11}'`;
272#chomp $avsp;
273
274if($totsize == 0) {
275 print "Nothing to get.";
276} else {
277 if($totsize > $avsp) {
278 print "Space required is greater than available space,\n";
279 print "you will need to select which items to get.\n";
280 }
281# ask user which files to get
282 if(($totsize > $avsp) || yesno("n", "Do you want to select the files to get")) {
283 $totsize = 0;
284 my @files = sort(keys(%downloads));
285 my $fn;
286 my $def = "y";
287 foreach $fn (@files) {
288 my @info = @{$pkgfiles{$fn}};
289 my $csize = int($info[1] / 1024) + 1;
290 my $rsize = int(($info[1] - $downloads{$fn}) / 1024) + 1;
291 if ($rsize + $totsize > $avsp) {
292 print "no room for: $fn\n";
293 delete $downloads{$fn};
294 } else {
295 if(yesno($def, $downloads{$fn}
296 ? "download: $fn ${rsize}k/${csize}k (total = ${totsize}k)"
297 : "download: $fn ${rsize}k (total = ${totsize}k)")) {
298 $def = "y";
299 $totsize += $rsize;
300 } else {
301 $def = "n";
302 delete $downloads{$fn};
303 }
304 }
305 }
306 }
307}
308
309sub download() {
310
311 my $i = 0;
312 my ($site, $ftp);
313
314 foreach $site (@{$config{'site'}}) {
315
316 my @getfiles = grep { $pkgfiles{$_}[2] == $i } keys %downloads;
317 my @pre_dist = (); # Directory to add before $fn
318
319 #Scan distributions for looking at "(../)+/dir/dir"
320 my ($n,$cp);
321 $cp = -1;
322 foreach (@{$site->[2]}) {
323 $cp++;
324 $pre_dist[$cp] = "";
325 $n = (s#\.\./#../#g);
326 next if (! $n);
327 if (m#^((?:\.\./){$n}(?:[^/]+/){$n})#) {
328 $pre_dist[$cp] = $1;
329 }
330 }
331
332 if (! @getfiles) { $i++; next; }
333
334 $ftp = do_connect ($site->[0], #$::ftpsite,
335 $site->[4], #$::username,
336 $site->[5], #$::password,
337 $site->[1], #$::ftpdir,
338 $site->[3], #$::passive,
339 $config{'use_auth_proxy'},
340 $config{'proxyhost'},
341 $config{'proxylogname'},
342 $config{'proxypassword'});
343
344 $::ftp = $ftp;
345 local $SIG{'INT'} = sub { die "Interrupted !\n"; };
346
347 my ($fn,$rsize,$res,$pre);
348 foreach $fn (@getfiles) {
349 $pre = $pre_dist[$pkgfiles{$fn}[3]] || "";
350 if ($downloads{$fn}) {
351 $rsize = ${pkgfiles{$fn}}[1] - $downloads{$fn};
352 print "getting: $pre$fn (". nb($rsize) . "/" .
353 nb($pkgfiles{$fn}[1]) . ")\n";
354 } else {
355 print "getting: $pre$fn (". nb($pkgfiles{$fn}[1]) . ")\n";
356 }
357 $res = $ftp->get("$pre$fn", "$dldir/$fn", $downloads{$fn});
358 if(! $res) {
359 my $r = $ftp->code();
360 print $ftp->message() . "\n";
361 if (!($r == 550 || $r == 450)) {
362 return 1;
363 } else {
364 #Try to find another file or this package
365 print "Looking for another version of the package...\n";
366 my ($dir,$package) = ($fn =~ m#^(.*)/([^/]+)_[^/]+.deb$#);
367 my $protected = $package;
368 $protected =~ s/\+/\\\+/g;
369 my $list = $ftp->ls("$pre$dir");
370 if ($ftp->ok() && ref($list)) {
371 foreach (@{$list}) {
372 if (m/($dir\/${protected}_[^\/]+.deb)/i) {
373 print "Package found : $_\n";
374 print "getting: $_ (size not known)\n";
375 $res = $ftp->get($_, "$dldir/$1");
376 if (! $res) {
377 $r = $ftp->code();
378 print $ftp->message() . "\n";
379 return 1 if ($r != 550 and $r != 450);
380 }
381 }
382 }
383 }
384 }
385 }
386 # fully got, remove it from list in case we have to re-download
387 delete $downloads{$fn};
388 }
389 $ftp->quit();
390 $i++;
391 }
392 return 0;
393}
394
395# download stuff (protect from ^C)
396if($totsize != 0) {
397 if(yesno("y", "\nDo you want to download the required files")) {
398 DOWNLOAD_TRY: while (1) {
399 print "Downloading files... use ^C to stop\n";
400 eval {
401 if ((download() == 1) && yesno("y", "\nDo you want to retry downloading at once")) {
402 next DOWNLOAD_TRY;
403 }
404 };
405 if($@ =~ /Interrupted|Timeout/i ) {
406 # close the FTP connection if needed
407 if ((ref($::ftp) =~ /Net::FTP/) and ($@ =~ /Interrupted/i)) {
408 $::ftp->abort();
409 $::ftp->quit();
410 undef $::ftp;
411 }
412 print "FTP ERROR\n";
413 if (yesno("y", "\nDo you want to retry downloading at once")) {
414 # get the first $fn that foreach would give:
415 # this is the one that got interrupted.
416 my $ffn;
417 MY_ITER: foreach $ffn (keys(%downloads)) {
418 $fn = $ffn;
419 last MY_ITER;
420 }
421 my $size = -s "$dldir/$fn";
422 # partial download
423 if(yesno("y", "continue file: $fn (at $size)")) {
424 $downloads{$fn} = $size;
425 } else {
426 $downloads{$fn} = 0;
427 }
428 next DOWNLOAD_TRY;
429 } else {
430 $exit = 1;
431 last DOWNLOAD_TRY;
432 }
433 } elsif ($@) {
434 print "An error occurred ($@) : stopping download\n";
435 }
436 last DOWNLOAD_TRY;
437 }
438 }
439}
440
441# remove duplicate packages (keep latest versions)
442# move half downloaded files out of the way
443# delete corrupted files
444print "\nProcessing downloaded files...(for corrupt/old/partial)\n";
445my %vers; # package => version
446my %files; # package-version => files...
447
448# check a deb or split deb file
449# return 1 if it a deb file, 2 if it is a split deb file
450# else 0
451sub chkdeb($) {
452 my ($fn) = @_;
453 # check to see if it is a .deb file
454 if(!system("dpkg-deb --info $fn 2>&1 >/dev/null && dpkg-deb --contents $fn 2>&1 >/dev/null")) {
455 return 1;
456 } elsif(!system("dpkg-split --info $fn 2>&1 >/dev/null")) {
457 return 2;
458 }
459 return 0;
460}
461sub getdebinfo($) {
462 my ($fn) = @_;
463 my $type = chkdeb($fn);
464 my ($pkg, $ver);
465 if($type == 1) {
466 open(PKGFILE, "dpkg-deb --field $fn |");
467 my %fields = getblk(\*PKGFILE);
468 close(PKGFILE);
469 $pkg = $fields{'package'};
470 $ver = $fields{'version'};
471 if($fields{'package_revision'}) { $ver .= '-' . $fields{'package_revision'}; }
472 return $pkg, $ver;
473 } elsif ( $type == 2) {
474 open(PKGFILE, "dpkg-split --info $fn|");
475 while(<PKGFILE>) {
476 /Part of package:\s*(\S+)/ and $pkg = $+;
477 /\.\.\. version:\s*(\S+)/ and $ver = $+;
478 }
479 close(PKGFILE);
480 return $pkg, $ver;
481 }
482 print "could not figure out type of $fn\n";
483 return $pkg, $ver;
484}
485
486# process deb file to make sure we only keep latest versions
487sub prcdeb($$) {
488 my ($dir, $fn) = @_;
489 my ($pkg, $ver) = getdebinfo($fn);
490 if(!defined($pkg) || !defined($ver)) {
491 print "could not get package info from file\n";
492 return 0;
493 }
494 if($vers{$pkg}) {
495 if(dcmpvers($vers{$pkg}, "eq", $ver)) {
496 $files{$pkg . $ver} = [ $files{$pkg . $ver }, "$dir/$fn" ];
497 } elsif (dcmpvers($vers{$pkg}, "gt", $ver)) {
498 print "old version\n";
499 unlink $fn;
500 } else { # else $ver is gt current version
501 my ($c);
502 foreach $c (@{$files{$pkg . $vers{$pkg}}}) {
503 print "replaces: $c\n";
504 unlink "$vardir/methods/ftp/$dldir/$c";
505 }
506 $vers{$pkg} = $ver;
507 $files{$pkg . $ver} = [ "$dir/$fn" ];
508 }
509 } else {
510 $vers{$pkg} = $ver;
511 $files{$pkg . $ver} = [ "$dir/$fn" ];
512 }
513}
514
515sub prcfile() {
516 my ($fn) = $_;
517 if (-f $fn and $fn ne '.') {
518 my $dir = ".";
519 if (length($File::Find::dir) > length($dldir)) {
520 $dir = substr($File::Find::dir, length($dldir)+1);
521 }
522 print "$dir/$fn\n";
523 if(defined($pkgfiles{"$dir/$fn"})) {
524 my @info = @{$pkgfiles{"$dir/$fn"}};
525 my $size = -s $fn;
526 if($size == 0) {
527 print "zero length file\n";
528 unlink $fn;
529 } elsif($size < $info[1]) {
530 print "partial file\n";
531 rename $fn, "$fn.partial";
532 } elsif(( (exists $md5sums{"$dldir/$fn"})
533 and ($md5sums{"$dldir/$fn"} ne $info[0]) )
534 or
535 (md5sum($fn) ne $info[0])) {
536 print "corrupt file\n";
537 unlink $fn;
538 } else {
539 prcdeb($dir, $fn);
540 }
541 } elsif($fn =~ /.deb$/) {
542 if(chkdeb($fn)) {
543 prcdeb($dir, $fn);
544 } else {
545 print "corrupt file\n";
546 unlink $fn;
547 }
548 } else {
549 print "non-debian file\n";
550 }
551 }
552}
553find(\&prcfile, "$dldir/");
554
555# install .debs
556if(yesno("y", "\nDo you want to install the files fetched")) {
557 print "Installing files...\n";
558 #Installing pre-dependent package before !
559 my (@flds, $package, @filename, $r);
560 while (@flds = `dpkg --predep-package`, $? == 0) {
561 foreach (@flds) {
562 s/\s*\n//;
563 $package= $_ if s/^Package: //i;
564 @filename= split(/ +/,$_) if s/^Filename: //i;
565 }
566 @filename = map { "$dldir/$_" } @filename;
567 next if (! @filename);
568 $r = system('dpkg', '-iB', '--', @filename);
569 if ($r) { print "DPKG ERROR\n"; $exit = 1; }
570 }
571 #Installing other packages after
572 $r = system("dpkg", "-iGREOB", $dldir);
573 if($r) {
574 print "DPKG ERROR\n";
575 $exit = 1;
576 }
577}
578
579sub removeinstalled {
580 my $fn = $_;
581 if (-f $fn and $fn ne '.') {
582 my $dir = ".";
583 if (length($File::Find::dir) > length($dldir)) {
584 $dir = substr($File::Find::dir, length($dldir)+1);
585 }
586 if($fn =~ /.deb$/) {
587 my($pkg, $ver) = getdebinfo($fn);
588 if(!defined($pkg) || !defined($ver)) {
589 print "Could not get info for: $dir/$fn\n";
590 } else {
591 if($curpkgs{$pkg} and dcmpvers($ver, "le", $curpkgs{$pkg})) {
592 print "deleting: $dir/$fn\n";
593 unlink $fn;
594 } else {
595 print "leaving: $dir/$fn\n";
596 }
597 }
598 } else {
599 print "non-debian: $dir/$fn\n";
600 }
601 }
602}
603
604# remove .debs that have been installed (query user)
605# first need to reprocess status file
606if(yesno("y", "\nDo you wish to delete the installed package (.deb) files?")) {
607 print "Removing installed files...\n";
608 %curpkgs = ();
609 procstatus();
610 find(\&removeinstalled, "$dldir/");
611}
612
613# remove whole ./debian directory if user wants to
614if(yesno("n", "\nDo you want to remove $dldir directory?")) {
615 rmtree("$dldir");
616}
617
618#Store useful md5sums
619foreach (keys %md5sums) {
620 next if (-f $_);
621 delete $md5sums{$_};
622}
623open(MD5SUMS, ">$methdir/md5sums") ||
624 die "Can't open $methdir/md5sums in write mode : $!\n";
625print MD5SUMS Dumper(\%md5sums);
626close MD5SUMS;
627
628exit $exit;