install 17 KB

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