File Coverage

blib/lib/urpm/download.pm
Criterion Covered Total %
statement 4 6 66.6
branch n/a
condition n/a
subroutine 2 2 100.0
pod n/a
total 6 8 75.0


line stmt bran cond sub pod time code
1             package urpm::download;
2              
3              
4 2     2   3496 use strict;
  2         4  
  2         47  
5 2     2   319 use urpm::msg;
  0            
  0            
6             use urpm::util qw(cat_ basename dirname file_size max member output_safe reduce_pathname);
7             use bytes ();
8             use Cwd;
9             use Exporter;
10             # perl_checker: require urpm
11              
12             # help perl_checker:
13             sub getcwd { goto &Cwd::getcwd }
14              
15             our @ISA = 'Exporter';
16             our @EXPORT = qw(get_proxy
17             propagate_sync_callback
18             sync_file sync_rsync sync_ssh
19             set_proxy_config dump_proxy_config
20             );
21              
22             #- proxy config file.
23             our $PROXY_CFG = '/etc/urpmi/proxy.cfg';
24             my $proxy_config;
25              
26             #- Timeout for curl connection and wget operations
27             our $CONNECT_TIMEOUT = 60; #- (in seconds)
28              
29              
30             =head1 NAME
31              
32             urpm::download - download routines for the urpm* tools
33              
34             =head1 SYNOPSIS
35              
36             =head1 DESCRIPTION
37              
38             =over
39              
40             =cut
41              
42              
43             sub ftp_http_downloaders() { qw(curl wget prozilla aria2) }
44              
45             sub available_ftp_http_downloaders() {
46             my %binaries = (
47             curl => 'curl',
48             wget => 'wget',
49             prozilla => 'proz',
50             aria2 => 'aria2c',
51             );
52             grep { -x "/usr/bin/$binaries{$_}" || -x "/bin/$binaries{$_}" } ftp_http_downloaders();
53             }
54              
55             sub metalink_downloaders() { qw(aria2) }
56              
57             sub available_metalink_downloaders() {
58             my %binaries = (
59             aria2 => 'aria2c',
60             );
61             grep { -x "/usr/bin/$binaries{$_}" || -x "/bin/$binaries{$_}" } metalink_downloaders();
62             }
63              
64             sub use_metalink {
65             my ($urpm, $medium) = @_;
66              
67             $medium->{allow_metalink} //= do {
68             my $use_metalink = 1;
69             preferred_downloader($urpm, $medium, \$use_metalink);
70             $use_metalink;
71             };
72             }
73              
74             my %warned;
75             sub preferred_downloader {
76             my ($urpm, $medium, $use_metalink) = @_;
77              
78             my @available = urpm::download::available_ftp_http_downloaders();
79             my @metalink_downloaders = urpm::download::available_metalink_downloaders();
80             my $metalink_disabled = !$$use_metalink && $medium->{disable_metalink};
81              
82             if ($$use_metalink && !$metalink_disabled) {
83             #- If metalink is used, only aria2 is available as other downloaders doesn't support metalink
84             unshift @available, @metalink_downloaders;
85             }
86            
87             #- first downloader of @available is the default one
88             my $preferred = $available[0];
89             my $requested_downloader = requested_ftp_http_downloader($urpm, $medium);
90             if ($requested_downloader) {
91             if (member($requested_downloader, @available)) {
92             #- use user default downloader if provided and available
93             $preferred = $requested_downloader;
94             } elsif ($warned{webfetch_not_available}++ == 0) {
95             $urpm->{log}(N("%s is not available, falling back on %s", $requested_downloader, $preferred));
96             }
97             }
98              
99             if ($$use_metalink && !member($preferred, @metalink_downloaders)) {
100             $warned{not_using_metalink}++ or
101             $urpm->{log}($requested_downloader eq $preferred ?
102             "not using metalink since requested downloader does not handle it" :
103             "not using metalink since no downloaders handling metalink are available");
104             $$use_metalink = 0;
105             }
106             $preferred;
107             }
108              
109             sub parse_http_proxy {
110             $_[0] =~ m!^(?:http://)?([^:/]+(:\d+)?)/*$!;
111             }
112              
113             #- parses proxy.cfg (private)
114             sub load_proxy_config () {
115             return if defined $proxy_config;
116             $proxy_config = {};
117             foreach (cat_($PROXY_CFG)) {
118             chomp; s/#.*$//; s/^\s*//; s/\s*$//;
119             if (/^(?:(.*):\s*)?(ftp_proxy|http_proxy)\s*=\s*(.*)$/) {
120             $proxy_config->{$1 || ''}{$2} = $3;
121             next;
122             }
123             if (/^(?:(.*):\s*)?proxy_user\s*=\s*([^:]*)(?::(.*))?$/) {
124             $proxy_config->{$1 || ''}{user} = $2;
125             $proxy_config->{$1 || ''}{pwd} = $3 if defined $3;
126             next;
127             }
128             if (/^(?:(.*):\s*)?proxy_user_ask/) {
129             $proxy_config->{$1 || ''}{ask} = 1;
130             next;
131             }
132             }
133             }
134              
135             #- writes proxy.cfg
136             sub dump_proxy_config () {
137             $proxy_config or return 0; #- hasn't been read yet
138              
139             my $has_password;
140              
141             open my $f, '>', $PROXY_CFG or return 0;
142             foreach ('', sort grep { !/^(|cmd_line)$/ } keys %$proxy_config) {
143             my $m = $_ eq '' ? '' : "$_:";
144             my $p = $proxy_config->{$_};
145             foreach (qw(http_proxy ftp_proxy)) {
146             if (defined $p->{$_} && $p->{$_} ne '') {
147             print $f "$m$_=$p->{$_}\n";
148             $has_password ||= hide_password($p->{$_}) ne $p->{$_};
149             }
150             }
151             if ($p->{ask}) {
152             print $f "${m}proxy_user_ask\n";
153             } elsif (defined $p->{user} && $p->{user} ne '') {
154             print $f "${m}proxy_user=$p->{user}:$p->{pwd}\n";
155             $has_password ||= $p->{pwd};
156             }
157             }
158             close $f;
159             chmod 0600, $PROXY_CFG if $has_password;
160             return 1;
161             }
162              
163             #- deletes the proxy configuration for the specified media
164             sub remove_proxy_media {
165             defined $proxy_config and delete $proxy_config->{$_[0] || ''};
166             }
167              
168             sub get_proxy_ {
169             my ($urpm) = @_;
170              
171             -e $PROXY_CFG && !-r $PROXY_CFG and $urpm->{error}(N("can not read proxy settings (not enough rights to read %s)", $PROXY_CFG));
172              
173             get_proxy($urpm);
174             }
175              
176             =item get_proxy($media)
177              
178             Reads and loads the proxy.cfg file ;
179             Returns the global proxy settings (without arguments) or the
180             proxy settings for the specified media (with a media name as argument)
181              
182             =cut
183              
184             sub get_proxy (;$) {
185             my ($o_media) = @_; $o_media ||= '';
186             load_proxy_config();
187             my $p = $proxy_config->{cmd_line}
188             || $proxy_config->{$o_media}
189             || $proxy_config->{''}
190             || {
191             http_proxy => undef,
192             ftp_proxy => undef,
193             user => undef,
194             pwd => undef,
195             };
196             if ($p->{ask} && ($p->{http_proxy} || $p->{ftp_proxy}) && !$p->{user}) {
197             our $PROMPT_PROXY;
198             unless (defined $PROMPT_PROXY) {
199             require urpm::prompt;
200             $PROMPT_PROXY = new urpm::prompt(
201             N("Please enter your credentials for accessing proxy\n"),
202             [ N("User name:"), N("Password:") ],
203             undef,
204             [ 0, 1 ],
205             );
206             }
207             ($p->{user}, $p->{pwd}) = $PROMPT_PROXY->prompt;
208             }
209             $p;
210             }
211              
212             #- copies the settings for proxies from the command line to media named $media
213             #- and writes the proxy.cfg file (used when adding new media)
214             sub copy_cmd_line_proxy {
215             my ($media) = @_;
216             return unless $media;
217             load_proxy_config();
218             if (defined $proxy_config->{cmd_line}) {
219             $proxy_config->{$media} = $proxy_config->{cmd_line};
220             dump_proxy_config();
221             } else {
222             #- use default if available
223             $proxy_config->{$media} = $proxy_config->{''};
224             }
225             }
226              
227             =item set_cmdline_proxy(%h)
228              
229             Overrides the config file proxy settings with values passed via command-line
230              
231             =cut
232              
233             sub set_cmdline_proxy {
234             my (%h) = @_;
235             load_proxy_config();
236             $proxy_config->{cmd_line} ||= {
237             http_proxy => undef,
238             ftp_proxy => undef,
239             user => undef,
240             pwd => undef,
241             };
242             $proxy_config->{cmd_line}{$_} = $h{$_} foreach keys %h;
243             }
244              
245             =item set_proxy_config($key, $value, $o_media)
246              
247             Changes permanently the proxy settings
248              
249             =cut
250              
251             sub set_proxy_config {
252             my ($key, $value, $o_media) = @_;
253             $proxy_config->{$o_media || ''}{$key} = $value;
254             }
255              
256             #- set up the environment for proxy usage for the appropriate tool.
257             #- returns an array of command-line arguments for wget or curl.
258             sub set_proxy {
259             my ($proxy) = @_;
260              
261             my $p = $proxy->{proxy};
262             defined $p->{http_proxy} || defined $p->{ftp_proxy} or return;
263              
264             my @res;
265             if ($proxy->{type} =~ /\bwget\b/) {
266             if (defined $p->{http_proxy}) {
267             $ENV{http_proxy} = $p->{http_proxy} =~ /^http:/
268             ? $p->{http_proxy} : "http://$p->{http_proxy}";
269             }
270             $ENV{ftp_proxy} = $p->{ftp_proxy} if defined $p->{ftp_proxy};
271             @res = ("--proxy-user=$p->{user}", "--proxy-passwd=$p->{pwd}")
272             if defined $p->{user} && defined $p->{pwd};
273             } elsif ($proxy->{type} =~ /\bcurl\b/) {
274             push @res, ('-x', $p->{http_proxy}) if defined $p->{http_proxy};
275             push @res, ('-x', $p->{ftp_proxy}) if defined $p->{ftp_proxy};
276             push @res, ('-U', "$p->{user}:$p->{pwd}")
277             if defined $p->{user} && defined $p->{pwd};
278             push @res, '-H', 'Pragma:' if @res;
279             } elsif ($proxy->{type} =~ /\baria2\b/) {
280             if (my ($http_proxy) = $p->{http_proxy} && parse_http_proxy($p->{http_proxy})) {
281             my $allproxy = $p->{user};
282             $allproxy .= ":" . $p->{pwd} if $p->{pwd};
283             $allproxy .= "@" if $p->{user};
284             $allproxy .= $http_proxy;
285             @res = ("--all-proxy=http://$allproxy");
286             }
287             } else {
288             die N("Unknown webfetch `%s' !!!\n", $proxy->{type});
289             }
290             @res;
291             }
292              
293             sub _error_msg {
294             my ($name) = @_;
295              
296             my $msg = $? & 127 ? N("%s failed: exited with signal %d", $name, $? & 127) :
297             N("%s failed: exited with %d", $name, $? >> 8);
298             "$msg\n";
299             }
300              
301             sub _error {
302             my ($name) = @_;
303             die _error_msg($name);
304             }
305              
306             sub hide_password {
307             my ($url) = @_;
308             $url =~ s|([^:]*://[^/:\@]*:)[^/:\@]*(\@.*)|$1xxxx$2|; #- if needed...
309             $url;
310             }
311              
312             sub propagate_sync_callback {
313             my $options = shift;
314             if (ref($options) && $options->{callback}) {
315             my $mode = shift;
316             if ($mode =~ /^(?:start|progress|end)$/) {
317             my $file = shift;
318             return $options->{callback}($mode, hide_password($file), @_);
319             } else {
320             return $options->{callback}($mode, @_);
321             }
322             }
323             }
324              
325             sub sync_file {
326             my $options = shift;
327             foreach (@_) {
328             propagate_sync_callback($options, 'start', $_);
329             require urpm::util;
330             urpm::util::copy($_, ref($options) ? $options->{dir} : $options)
331             or die N("copy failed");
332             propagate_sync_callback($options, 'end', $_);
333             }
334             }
335              
336             sub sync_wget {
337             -x "/usr/bin/wget" or die N("wget is missing\n");
338             my $options = shift;
339             $options = { dir => $options } if !ref $options;
340             #- force download to be done in cachedir to avoid polluting cwd.
341             (my $cwd) = getcwd() =~ /(.*)/;
342             chdir $options->{dir};
343             my ($buf, $total, $file) = ('', undef, undef);
344             my $wget_command = join(" ", map { "'$_'" }
345             #- construction of the wget command-line
346             "/usr/bin/wget",
347             ($options->{'limit-rate'} ? "--limit-rate=$options->{'limit-rate'}" : @{[]}),
348             ($options->{resume} ? "--continue" : "--force-clobber"),
349             ($options->{proxy} ? set_proxy({ type => "wget", proxy => $options->{proxy} }) : @{[]}),
350             ($options->{retry} ? ('-t', $options->{retry}) : @{[]}),
351             ($options->{callback} ? ("--progress=bar:force", "-o", "-") :
352             $options->{quiet} ? "-q" : @{[]}),
353             "--retr-symlinks",
354             ($options->{"no-certificate-check"} ? "--no-check-certificate" : @{[]}),
355             "--timeout=$CONNECT_TIMEOUT",
356             (defined $options->{'wget-options'} ? split /\s+/, $options->{'wget-options'} : @{[]}),
357             '-P', $options->{dir},
358             @_
359             ) . " |";
360             $options->{debug} and $options->{debug}($wget_command);
361             local $ENV{LC_ALL} = 'C';
362             my $wget_pid = open(my $wget, $wget_command);
363             local $/ = \1; #- read input by only one char, this is slow but very nice (and it works!).
364             local $_;
365             while (<$wget>) {
366             $buf .= $_;
367             if ($_ eq "\r" || $_ eq "\n") {
368             if ($options->{callback}) {
369             if ($buf =~ /^--(\d\d\d\d-\d\d-\d\d )?\d\d:\d\d:\d\d--\s+(\S.*)\n/ms) {
370             my $file_ = $2;
371             if ($file && $file ne $file_) {
372             propagate_sync_callback($options, 'end', $file);
373             undef $file;
374             }
375             ! defined $file and propagate_sync_callback($options, 'start', $file = $file_);
376             } elsif (defined $file && ! defined $total && ($buf =~ /==>\s+RETR/ || $buf =~ /200 OK$/)) {
377             $total = '';
378             } elsif ($buf =~ /^Length:\s*(\d\S*)/) {
379             $total = $1;
380             } elsif (defined $total && $buf =~ m!^\s*(\d+)%.*\s+(\S+/s)\s+((ETA|eta)\s+(.*?)\s*)?[\r\n]$!ms) {
381             my ($percent, $speed, $eta) = ($1, $2, $5);
382             if (propagate_sync_callback($options, 'progress', $file, $percent, $total, $eta, $speed) eq 'canceled') {
383             kill 15, $wget_pid;
384             close $wget;
385             return;
386             }
387             if ($_ eq "\n") {
388             propagate_sync_callback($options, 'end', $file);
389             ($total, $file) = (undef, undef);
390             }
391             }
392             } else {
393             $options->{quiet} or print STDERR $buf;
394             }
395             $buf = '';
396             }
397             }
398             $file and propagate_sync_callback($options, 'end', $file);
399             chdir $cwd;
400             close $wget or _error('wget');
401             }
402              
403             sub sync_curl {
404             -x "/usr/bin/curl" or die N("curl is missing\n");
405             my $options = shift;
406             $options = { dir => $options } if !ref $options;
407             if (defined $options->{'limit-rate'} && $options->{'limit-rate'} =~ /\d$/) {
408             #- use bytes by default
409             $options->{'limit-rate'} .= 'B';
410             }
411             #- force download to be done in cachedir to avoid polluting cwd,
412             #- however for curl, this is mandatory.
413             (my $cwd) = getcwd() =~ /(.*)/;
414             chdir($options->{dir});
415             my (@ftp_files, @other_files);
416             foreach (@_) {
417             my ($proto, $nick, $rest) = m,^(http|ftp)://([^:/]+):(.*),,;
418             if ($nick) { #- escape @ in user names
419             $nick =~ s/@/%40/;
420             $_ = "$proto://$nick:$rest";
421             }
422             if (m|^ftp://.*/([^/]*)$| && file_size($1) > 8192) { #- manage time stamp for large file only
423             push @ftp_files, $_;
424             } else {
425             push @other_files, $_;
426             }
427             }
428             if (@ftp_files) {
429             my ($cur_ftp_file, %ftp_files_info);
430             local $_;
431              
432             eval { require Date::Manip };
433              
434             #- prepare to get back size and time stamp of each file.
435             my $cmd = join(" ", map { "'$_'" } "/usr/bin/curl",
436             "-q", # don't read .curlrc; some toggle options might interfer
437             ($options->{'limit-rate'} ? ("--limit-rate", $options->{'limit-rate'}) : @{[]}),
438             ($options->{proxy} ? set_proxy({ type => "curl", proxy => $options->{proxy} }) : @{[]}),
439             ($options->{retry} ? ('--retry', $options->{retry}) : @{[]}),
440             "--stderr", "-", # redirect everything to stdout
441             "--disable-epsv",
442             "--connect-timeout", $CONNECT_TIMEOUT,
443             "-s", "-I",
444             "--anyauth",
445             (defined $options->{'curl-options'} ? split /\s+/, $options->{'curl-options'} : @{[]}),
446             @ftp_files);
447             $options->{debug} and $options->{debug}($cmd);
448             open my $curl, "$cmd |";
449             while (<$curl>) {
450             if (/Content-Length:\s*(\d+)/) {
451             !$cur_ftp_file || exists($ftp_files_info{$cur_ftp_file}{size})
452             and $cur_ftp_file = shift @ftp_files;
453             $ftp_files_info{$cur_ftp_file}{size} = $1;
454             }
455             if (/Last-Modified:\s*(.*)/) {
456             !$cur_ftp_file || exists($ftp_files_info{$cur_ftp_file}{time})
457             and $cur_ftp_file = shift @ftp_files;
458             eval {
459             $ftp_files_info{$cur_ftp_file}{time} = Date::Manip::ParseDate($1);
460             };
461             }
462             }
463             close $curl or _error('curl');
464              
465             #- now analyse size and time stamp according to what already exists here.
466             if (@ftp_files) {
467             #- re-insert back shifted element of ftp_files, because curl output above
468             #- has not been parsed correctly, so in doubt download them all.
469             push @ftp_files, keys %ftp_files_info;
470             } else {
471             #- for that, it should be clear ftp_files is empty...
472             #- elsewhere, the above work was useless.
473             foreach (keys %ftp_files_info) {
474             my ($lfile) = m|/([^/]*)$| or next; #- strange if we can't parse it correctly.
475             my $ltime = eval { Date::Manip::ParseDate(scalar gmtime((stat $1)[9])) };
476             $ltime && -s $lfile == $ftp_files_info{$_}{size} && $ftp_files_info{$_}{time} eq $ltime
477             or push @ftp_files, $_;
478             }
479             }
480             }
481             # Indicates whether this option is available in our curl
482             our $location_trusted;
483             if (!defined $location_trusted) {
484             $location_trusted = `/usr/bin/curl -h` =~ /location-trusted/ ? 1 : 0;
485             }
486             #- http files (and other files) are correctly managed by curl wrt conditional download.
487             #- options for ftp files, -R (-O )*
488             #- options for http files, -R (-O )*
489             my $result;
490             if (my @all_files = (
491             (map { ("-O", $_) } @ftp_files),
492             (map { m|/| ? ("-O", $_) : @{[]} } @other_files)))
493             {
494             my @l = (@ftp_files, @other_files);
495             my $cmd = join(" ", map { "'$_'" } "/usr/bin/curl",
496             "-q", # don't read .curlrc; some toggle options might interfer
497             ($options->{'limit-rate'} ? ("--limit-rate", $options->{'limit-rate'}) : @{[]}),
498             ($options->{resume} ? ("--continue-at", "-") : @{[]}),
499             ($options->{proxy} ? set_proxy({ type => "curl", proxy => $options->{proxy} }) : @{[]}),
500             ($options->{retry} ? ('--retry', $options->{retry}) : @{[]}),
501             ($options->{quiet} ? "-s" : @{[]}),
502             ($options->{"no-certificate-check"} ? "-k" : @{[]}),
503             $location_trusted ? "--location-trusted" : @{[]},
504             "-R",
505             "-f",
506             "--disable-epsv",
507             "--connect-timeout", $CONNECT_TIMEOUT,
508             "--anyauth",
509             (defined $options->{'curl-options'} ? split /\s+/, $options->{'curl-options'} : @{[]}),
510             "--stderr", "-", # redirect everything to stdout
511             @all_files);
512             $options->{debug} and $options->{debug}($cmd);
513             $result = _curl_action($cmd, $options, @l);
514             }
515             chdir $cwd;
516             $result;
517             }
518              
519             sub _curl_action {
520             my ($cmd, $options, @l) = @_;
521            
522             my ($buf, $file); $buf = '';
523             my $curl_pid = open(my $curl, "$cmd |");
524             local $/ = \1; #- read input by only one char, this is slow but very nice (and it works!).
525             local $_;
526             while (<$curl>) {
527             $buf .= $_;
528             if ($_ eq "\r" || $_ eq "\n") {
529             if ($options->{callback}) {
530             unless (defined $file) {
531             $file = shift @l;
532             propagate_sync_callback($options, 'start', $file);
533             }
534             if (my ($percent, $total, $eta, $speed) = $buf =~ /^\s*(\d+)\s+(\S+)[^\r\n]*\s+(\S+)\s+(\S+)\s*[\r\n]$/ms) {
535             $speed =~ s/^-//;
536             if (propagate_sync_callback($options, 'progress', $file, $percent, $total, $eta, $speed) eq 'canceled') {
537             kill 15, $curl_pid;
538             close $curl;
539             die N("curl failed: download canceled\n");
540             }
541             #- this checks that download has actually started
542             if ($_ eq "\n"
543             && !($speed == 0 && $percent == 100 && index($eta, '--') >= 0) #- work around bug 13685
544             ) {
545             propagate_sync_callback($options, 'end', $file);
546             $file = undef;
547             }
548             } elsif ($buf =~ /^curl:/) { #- likely to be an error reported by curl
549             local $/ = "\n";
550             chomp $buf;
551             propagate_sync_callback($options, 'error', $file, $buf);
552             }
553             } else {
554             $options->{quiet} or print STDERR $buf;
555             }
556             $buf = '';
557             }
558             }
559             close $curl or _error('curl');
560             }
561              
562             sub _calc_limit_rate {
563             my $limit_rate = $_[0];
564             for ($limit_rate) {
565             /^(\d+)$/ and $limit_rate = int $1/1024, last;
566             /^(\d+)[kK]$/ and $limit_rate = $1, last;
567             /^(\d+)[mM]$/ and $limit_rate = 1024*$1, last;
568             /^(\d+)[gG]$/ and $limit_rate = 1024*1024*$1, last;
569             }
570             $limit_rate;
571             }
572              
573             sub sync_rsync {
574             -x "/usr/bin/rsync" or die N("rsync is missing\n");
575             my $options = shift;
576             $options = { dir => $options } if !ref $options;
577             #- force download to be done in cachedir to avoid polluting cwd.
578             (my $cwd) = getcwd() =~ /(.*)/;
579             chdir($options->{dir});
580             my $limit_rate = _calc_limit_rate($options->{'limit-rate'});
581             foreach (@_) {
582             my $count = 10; #- retry count on error (if file exists).
583             my $basename = basename($_);
584             my $file = m!^rsync://([^/]*::.*)! ? $1 : $_;
585             propagate_sync_callback($options, 'start', $file);
586             do {
587             local $_;
588             my $buf = '';
589             my $cmd = join(" ", "/usr/bin/rsync",
590             ($limit_rate ? "--bwlimit=$limit_rate" : @{[]}),
591             ($options->{quiet} ? qw(-q) : qw(--progress -v --no-human-readable)),
592             ($options->{compress} ? qw(-z) : @{[]}),
593             ($options->{ssh} ? qq(-e $options->{ssh}) :
594             ("--timeout=$CONNECT_TIMEOUT",
595             "--contimeout=$CONNECT_TIMEOUT")),
596             qw(--partial --no-whole-file --no-motd --copy-links),
597             (defined $options->{'rsync-options'} ? split /\s+/, $options->{'rsync-options'} : @{[]}),
598             "'$file' '$options->{dir}' 2>&1");
599             $options->{debug} and $options->{debug}($cmd);
600             open(my $rsync, "$cmd |");
601             local $/ = \1; #- read input by only one char, this is slow but very nice (and it works!).
602             local $_;
603             while (<$rsync>) {
604             $buf .= $_;
605             if ($_ eq "\r" || $_ eq "\n") {
606             if ($options->{callback}) {
607             if (my ($percent, $speed) = $buf =~ /^\s*\d+\s+(\d+)%\s+(\S+)\s+/) {
608             propagate_sync_callback($options, 'progress', $file, $percent, undef, undef, $speed);
609             } else {
610             $options->{debug} and $options->{debug}($buf);
611             }
612             } else {
613             $options->{quiet} or print STDERR $buf;
614             $options->{debug} and $options->{debug}($buf);
615             }
616             $buf = '';
617             }
618             }
619             close $rsync;
620             } while ($? != 0 && --$count > 0 && -e $options->{dir} . "/$basename");
621             propagate_sync_callback($options, 'end', $file);
622             }
623             chdir $cwd;
624             $? == 0 or _error('rsync');
625             }
626              
627             our $SSH_PATH;
628             sub _init_ssh_path() {
629             foreach (qw(/usr/bin/ssh /bin/ssh)) {
630             -x $_ and $SSH_PATH = $_;
631             next;
632             }
633             }
634              
635             #- Don't generate a tmp dir name, so when we restart urpmi, the old ssh
636             #- connection can be reused
637             our $SSH_CONTROL_DIR = $ENV{TMP} || $ENV{TMPDIR} || '/tmp';
638             our $SSH_CONTROL_OPTION;
639              
640             sub sync_ssh {
641             $SSH_PATH or _init_ssh_path();
642             $SSH_PATH or die N("ssh is missing\n");
643             my $options = shift;
644             $options = { dir => $options } if !ref $options;
645             unless ($options->{'rsync-options'} =~ /(?:-e|--rsh)\b/) {
646             my ($server, $user) = ('', getpwuid($<));
647             $_[0] =~ /((?:\w|\.)*):/ and $server = $1;
648             $_[0] =~ /((?:\w|-)*)@/ and $user = $1;
649             $SSH_CONTROL_OPTION = "-o 'ControlPath $SSH_CONTROL_DIR/ssh-urpmi-$$-%h_%p_%r' -o 'ControlMaster auto'";
650             if (start_ssh_master($server, $user)) {
651             $options->{ssh} = qq("$SSH_PATH $SSH_CONTROL_OPTION");
652             } else {
653             #- can't start master, use single connection
654             $options->{ssh} = $SSH_PATH;
655             }
656             }
657             sync_rsync($options, @_);
658             }
659              
660             sub sync_prozilla {
661             -x "/usr/bin/proz" or die N("prozilla is missing\n");
662             my $options = shift;
663             $options = { dir => $options } if !ref $options;
664             #- force download to be done in cachedir to avoid polluting cwd.
665             (my $cwd) = getcwd() =~ /(.*)/;
666             chdir $options->{dir};
667             my $proz_command = join(" ", map { "'$_'" }
668             "/usr/bin/proz",
669             "--no-curses",
670             (defined $options->{'prozilla-options'} ? split /\s+/, $options->{'prozilla-options'} : @{[]}),
671             @_
672             );
673             my $ret = system($proz_command);
674             chdir $cwd;
675             if ($ret) {
676             if ($? == -1) {
677             die N("Couldn't execute prozilla\n");
678             } else {
679             _error('prozilla');
680             }
681             }
682             }
683              
684             sub sync_aria2 {
685             my ($urpm, $medium, $rel_files, $options) = @_;
686              
687             -x "/usr/bin/aria2c" or die N("aria2 is missing\n");
688              
689             #- force download to be done in cachedir to avoid polluting cwd.
690             (my $cwd) = getcwd() =~ /(.*)/;
691             chdir $options->{dir};
692              
693             my $stat_file = ($< ? $ENV{HOME} : '/root') . '/.aria2-adaptive-stats';
694              
695             my $aria2c_command = join(" ", map { "'$_'" }
696             "/usr/bin/aria2c", $options->{debug} ? ('--log', "$options->{dir}/.aria2.log") : @{[]},
697             '--auto-file-renaming=false',
698             '--ftp-pasv',
699             '--summary-interval=1',
700             '--follow-metalink=mem',
701             $medium->{mirrorlist} ? (
702             '--metalink-enable-unique-protocol=true', # do not try to connect to the same server using the same protocol
703             '--metalink-preferred-protocol=http', # try http as first protocol as they're stateless and
704             # will put less strain on ie. the ftp servers which connections
705             # are statefull for, causing unhappy mirror admins complaining
706             # about increase of connections, increasing resource usage.
707             '--max-tries=5', # nb: not using $options->{retry}
708             '--lowest-speed-limit=20K', "--timeout", 3,
709             '--split=3', # maximum number of servers to use for one download
710             '--uri-selector=adaptive', "--server-stat-if=$stat_file", "--server-stat-of=$stat_file",
711             $options->{is_versioned} ? @{[]} : '--max-file-not-found=9', # number of not found errors on different servers before aborting file download
712             '--connect-timeout=6', # $CONNECT_TIMEOUT,
713             ) : @{[]},
714             '-Z', '-j1',
715             ($options->{'limit-rate'} ? "--max-download-limit=" . $options->{'limit-rate'} : @{[]}),
716             ($options->{resume} ? "--continue" : "--allow-overwrite=true"),
717             ($options->{proxy} ? set_proxy({ type => "aria2", proxy => $options->{proxy} }) : @{[]}),
718             ($options->{"no-certificate-check"} ? "--check-certificate=false" : @{[]}),
719             (defined $options->{'aria2-options'} ? split /\s+/, $options->{'aria2-options'} : @{[]}),
720             _create_metalink_($urpm, $medium, $rel_files, $options));
721              
722             $options->{debug} and $options->{debug}($aria2c_command);
723              
724             local $ENV{LC_ALL} = 'C';
725             my $aria2_pid = open(my $aria2, "$aria2c_command |");
726              
727             _parse_aria2_output($options, $aria2, $aria2_pid, $medium, $rel_files);
728              
729             chdir $cwd;
730             if (!close $aria2) {
731             my $raw_msg = _error_msg('aria2');
732             my $want_retry;
733             if (!$options->{is_retry} & $options->{is_versioned}) {
734             $want_retry = 1;
735             } else {
736             my $msg = N("Failed to download %s", $rel_files->[0]);
737             $want_retry = $options->{ask_retry} && $options->{ask_retry}($raw_msg, $msg);
738             }
739             if ($want_retry) {
740             $options->{is_retry}++;
741             $options->{debug} and $options->{debug}("retrying ($options->{is_retry})");
742             goto &sync_aria2;
743             }
744             die $raw_msg;
745             }
746             }
747              
748             sub _parse_aria2_output {
749             my ($options, $aria2, $aria2_pid, $medium, $rel_files) = @_;
750              
751             my ($buf, $_total, $file) = ('', undef, undef);
752              
753             local $/ = \1; #- read input by only one char, this is slow but very nice (and it works!).
754             local $_;
755              
756             while (<$aria2>) {
757             if ($_ eq "\r" || $_ eq "\n") {
758             $options->{debug}("aria2c: $buf") if $options->{debug};
759             if ($options->{callback}) {
760             if (!defined($file) && @$rel_files) {
761             $file = $medium->{mirrorlist} ?
762             $medium->{mirrorlist} . ': ' . $medium->{'with-dir'} . "/$rel_files->[0]" :
763             "$medium->{url}/$rel_files->[0]";
764             propagate_sync_callback($options, 'start', $file)
765             if !$options->{is_retry};
766             }
767              
768             # aria2c 1.16 and beyond:
769             # parses aria2c: [#2c8dae 496KiB/830KiB(59%) CN:1 DL:84KiB ETA:3s]
770             #
771             # using multiline mode and comments for better readability:
772             #
773             if ($buf =~ m!
774             ^\[\#[\dA-Fa-f]+ # match #2c8dae
775             \s+
776             ([\d\.]+\w*) # Match 496KiB
777             /
778             ([\d\.]+\w*) # Match 830KiB
779             \s* \( (\d+) % \) # Match (59%)
780             \s+
781             CN:(\S+) # Match CN:1
782             \s+
783             DL:(\S+) # Match DL:84KiB
784             \s+
785             ETA:(\w+)
786             \]$
787             !msx
788             )
789             {
790             my ($total, $percent, $speed, $eta) = ($2, $3, $5, $6);
791             #- $1 = current downloaded size, $4 = connections
792             if (propagate_sync_callback($options, 'progress', $file, $percent, $total, $eta, $speed) eq 'canceled') {
793             kill 15, $aria2_pid;
794             close $aria2;
795             return;
796             }
797             }
798             if ($buf =~ m!Download\scomplete:\s/!) {
799             propagate_sync_callback($options, 'end', $file);
800             shift @$rel_files;
801             delete $options->{is_retry};
802             $file = undef;
803             } elsif ($buf =~ /ERR\|(.*)/) {
804             propagate_sync_callback($options, 'error', $file, $1);
805             }
806             } else {
807             $options->{quiet} or print STDERR "$buf\n";
808             }
809             $buf = '';
810             } else {
811             $buf .= $_;
812             }
813             }
814             }
815              
816             sub start_ssh_master {
817             my ($server, $user) = @_;
818             $server or return 0;
819             if (!check_ssh_master($server, $user)) {
820             system(qq($SSH_PATH -f -N $SSH_CONTROL_OPTION -M $user\@$server));
821             return ! $?;
822             }
823             return 1;
824             }
825              
826             sub check_ssh_master {
827             my ($server, $user) = @_;
828             system(qq($SSH_PATH -q -f -N $SSH_CONTROL_OPTION $user\@$server -O check));
829             return ! $?;
830             }
831              
832             END {
833             #- remove ssh persistent connections
834             foreach my $socket (glob "$SSH_CONTROL_DIR/ssh-urpmi-$$-*") {
835             my ($server, $login) = $socket =~ /ssh-urpmi-\d+-([^_]+)_\d+_(.*)$/ or next;
836             system($SSH_PATH, '-q', '-f', '-N', '-o', "ControlPath $socket", '-O', 'exit', "$login\@$server");
837             }
838             }
839              
840             #- get the width of the terminal
841             my $wchar = 79;
842             if (-t *STDOUT) {
843             eval {
844             require Term::ReadKey;
845             ($wchar) = Term::ReadKey::GetTerminalSize();
846             --$wchar;
847             };
848             }
849              
850             sub progress_text {
851             my ($mode, $percent, $total, $eta, $speed) = @_;
852             $mode eq 'progress' ?
853             (defined $total && defined $eta ?
854             N(" %s%% of %s completed, ETA = %s, speed = %s", $percent, $total, $eta, $speed) :
855             N(" %s%% completed, speed = %s", $percent, $speed)) : '';
856             }
857              
858             =item sync_logger($mode, $file, $percent, $_total, $_eta, $_speed)
859              
860             Default logger (callback) suitable for sync operation on STDERR only.
861              
862             =cut
863              
864             sub sync_logger {
865             my ($mode, $file, $percent, $total, $eta, $speed) = @_;
866             if ($mode eq 'start') {
867             print STDERR " $file\n";
868             } elsif ($mode eq 'progress') {
869             my $text = progress_text($mode, $percent, $total, $eta, $speed);
870             if (length($text) > $wchar) { $text = substr($text, 0, $wchar) }
871             if (bytes::length($text) < $wchar) {
872             # clearing more than needed in case the terminal is not handling utf8 and we have a utf8 string
873             print STDERR $text, " " x ($wchar - bytes::length($text)), "\r";
874             } else {
875             # clearing all the line first since we can't really know the "length" of the string
876             print STDERR " " x $wchar, "\r", $text, "\r";
877             }
878             } elsif ($mode eq 'end') {
879             print STDERR " " x $wchar, "\r";
880             } elsif ($mode eq 'error') {
881             #- error is 3rd argument, saved in $percent
882             print STDERR N("...retrieving failed: %s", $percent), "\n";
883             }
884             }
885              
886             =item requested_ftp_http_downloader($urpm, $medium)
887              
888             Return the downloader program to use (whether it pas provided on the
889             command line or in the config file).
890              
891             =cut
892              
893             sub requested_ftp_http_downloader {
894             my ($urpm, $medium) = @_;
895              
896             $urpm->{options}{downloader} || #- cmd-line switch
897             $medium && $medium->{downloader} ||
898             $urpm->{global_config}{downloader} || "";
899             }
900              
901             sub parse_url_with_login {
902             my ($url) = @_;
903             $url =~ m!([^:]*)://([^/:]*)(:([^/:\@]*))?\@([^/]*)(.*)! && $1 ne 'ssh' &&
904             { proto => $1, login => $2, password => $4, machine => $5, dir => $6 };
905             }
906             sub url_obscuring_password {
907             my ($url) = @_;
908             my $u = parse_url_with_login($url);
909             if ($u && $u->{password}) {
910             sprintf('%s://xxx:xxx@%s%s', $u->{proto}, $u->{machine}, $u->{dir});
911             } else {
912             $url;
913             }
914             }
915              
916             #- $medium can be undef
917             sub _all_options {
918             my ($urpm, $medium, $options) = @_;
919              
920             my %all_options = (
921             dir => "$urpm->{cachedir}/partial",
922             proxy => get_proxy_($urpm),
923             metalink => $medium->{mirrorlist},
924             $medium->{"disable-certificate-check"} ? "no-certificate-check" : @{[]},
925             $urpm->{debug} ? (debug => $urpm->{debug}) : @{[]},
926             %$options,
927             );
928             foreach my $cpt (qw(compress limit-rate retry wget-options curl-options rsync-options prozilla-options aria2-options metalink)) {
929             $all_options{$cpt} = $urpm->{options}{$cpt} if defined $urpm->{options}{$cpt};
930             }
931             \%all_options;
932             }
933              
934             sub sync_rel {
935             my ($urpm, $medium, $rel_files, %options) = @_;
936              
937             my @files = map { reduce_pathname("$medium->{url}/$_") } @$rel_files;
938              
939             my $files_text = join(' ', (use_metalink($urpm, $medium) ? ($medium->{mirrorlist}, $medium->{'with-dir'}) : url_obscuring_password($medium->{url})), @$rel_files);
940             $urpm->{debug} and $urpm->{debug}(N("retrieving %s", $files_text));
941              
942             my $all_options = _all_options($urpm, $medium, \%options);
943             my @result_files = map { $all_options->{dir} . '/' . basename($_) } @$rel_files;
944             unlink @result_files if $all_options->{preclean};
945              
946             (my $cwd) = getcwd() =~ /(.*)/;
947             eval { _sync_webfetch_raw($urpm, $medium, $rel_files, \@files, $all_options) };
948             my $err = $@;
949             chdir $cwd;
950             if (!$err) {
951             $urpm->{log}(N("retrieved %s", $files_text));
952             \@result_files;
953             } else {
954             $urpm->{log}("error: $err");
955             # don't leave partial download
956             unlink @result_files;
957             undef;
958             }
959             }
960              
961             sub sync_rel_one {
962             my ($urpm, $medium, $rel_file, %options) = @_;
963              
964             my $files = sync_rel($urpm, $medium, [$rel_file], %options) or return;
965             $files->[0];
966             }
967              
968             =item sync_url($urpm, $url, %options)
969              
970             Retrieve a file from the network and return the local cached file path.
971              
972             =cut
973              
974             sub sync_url {
975             my ($urpm, $url, %options) = @_;
976             sync_rel_one($urpm, { url => dirname($url), disable_metalink => $options{disable_metalink} }, basename($url), %options);
977             }
978              
979             sub sync_rel_to {
980             my ($urpm, $medium, $rel_file, $dest_file, %options) = @_;
981              
982             my $files = sync_rel($urpm, $medium, [$rel_file], %options) or return undef;
983             my $result_file = $files->[0];
984             $result_file ne $dest_file or rename($result_file, $dest_file) or return;
985             $result_file;
986             }
987              
988             =item get_content($urpm, $url, %o_options)
989              
990             Retrieve a file and return its content.
991              
992             =cut
993              
994             sub get_content {
995             my ($urpm, $url, %o_options) = @_;
996              
997             my $file = sync_url($urpm, $url, %o_options, quiet => 1, preclean => 1) or return;
998              
999             my @l = cat_($file);
1000             unlink $file;
1001              
1002             wantarray() ? @l : join('', @l);
1003             }
1004            
1005              
1006             #- syncing algorithms.
1007             #-
1008             #- nb: $files is constructed from $rel_files using $medium
1009             sub _sync_webfetch_raw {
1010             my ($urpm, $medium, $rel_files, $files, $options) = @_;
1011              
1012             #- currently ftp and http protocols are managed by curl or wget,
1013             #- ssh and rsync protocols are managed by rsync *AND* ssh.
1014             my $proto = urpm::protocol_from_url($medium->{url}) or die N("unknown protocol defined for %s", $medium->{url});
1015              
1016             if ($proto eq 'file') {
1017             my @l = map { urpm::file_from_local_url($_) } @$files;
1018             eval { sync_file($options, @l) };
1019             $urpm->{fatal}(10, $@) if $@;
1020             } elsif ($proto eq 'rsync') {
1021             sync_rsync($options, @$files);
1022             } elsif (member($proto, 'ftp', 'http', 'https') || $options->{metalink}) {
1023              
1024             my $preferred = preferred_downloader($urpm, $medium, \$options->{metalink});
1025             if ($preferred eq 'aria2') {
1026             sync_aria2($urpm, $medium, $rel_files, $options);
1027             } else {
1028             my $sync = $urpm::download::{"sync_$preferred"} or die N("no webfetch found, supported webfetch are: %s\n", join(", ", urpm::download::ftp_http_downloaders()));
1029              
1030             my @l = @$files;
1031             while (@l) {
1032             my $half_MAX_ARG = 131072 / 2;
1033             # restrict the number of elements so that it fits on cmdline of curl/wget/proz/aria2c
1034             my $n = 0;
1035             for (my $len = 0; $n < @l && $len < $half_MAX_ARG; $len += length($l[$n++])) {}
1036             $sync->($options, splice(@l, 0, $n));
1037             }
1038             }
1039             } elsif ($proto eq 'ssh') {
1040             my @ssh_files = map { m!^ssh://([^/]*)(.*)! ? "$1:$2" : @{[]} } @$files;
1041             sync_ssh($options, @ssh_files);
1042             } else {
1043             die N("unable to handle protocol: %s", $proto);
1044             }
1045             }
1046              
1047             sub _take_n_elem {
1048             my ($n, @l) = @_;
1049             @l < $n ? @l : @l[0 .. $n-1];
1050             }
1051              
1052             sub _create_one_metalink_line {
1053             my ($medium, $mirror, $rel_file, $counter) = @_;
1054              
1055             my $type = urpm::protocol_from_url($mirror->{url});
1056              
1057             # If more than 100 mirrors, give all the remaining mirrors a priority of 0
1058             my $preference = max(0, 100 - $counter);
1059              
1060             my @options = (qq(type="$type"), qq(preference="$preference"));
1061             # Not supported in metalinks
1062             #if (@$list[$i]->{bw}) {
1063             # push @options, 'bandwidth="' . @$list[$i]->{bw} . '"';
1064             # }
1065             # Supported in metalinks, but no longer used in mirror list..?
1066             if ($mirror->{connections}) {
1067             push @options, qq(maxconnections="$mirror->{connections}");
1068             }
1069             push @options, 'location="' . lc($mirror->{zone}) . '"';
1070             my $base = urpm::mirrors::_add__with_dir($mirror->{url}, $medium->{'with-dir'});
1071             sprintf('%s/%s', join(' ', @options), $base, $rel_file);
1072             }
1073              
1074             sub _create_metalink_ {
1075             my ($urpm, $medium, $rel_files, $options) = @_;
1076             # Don't create a metalink when downloading mirror list
1077             $medium or return;
1078              
1079             # only use the 8 best mirrors, then we let aria2 choose
1080             require urpm::mirrors;
1081             my @mirrors = $medium->{mirrorlist} ? (map {
1082             # aria2 doesn't handle rsync
1083             my @l = grep { urpm::protocol_from_url($_->{url}) ne 'rsync' } @$_;
1084             _take_n_elem(8, @l);
1085             } urpm::mirrors::list_urls($urpm, $medium, '')) : { url => $medium->{url} };
1086            
1087             my $metalinkfile = "$urpm->{cachedir}/$options->{media}.metalink";
1088             # Even if not required by metalink spec, this line is needed at top of
1089             # metalink file, otherwise aria2 won't be able to autodetect it..
1090             my @metalink = (
1091             '',
1092             '',
1093             '',
1094             );
1095              
1096             foreach my $rel_file (@$rel_files) {
1097             my $i = 0;
1098             my @lines = map {
1099             $i++;
1100             _create_one_metalink_line($medium, $_, $rel_file, $i);
1101             } @mirrors;
1102              
1103             push @metalink, map { "\t$_" }
1104             sprintf('', basename($rel_file)),
1105             (map { "\t$_" } @lines),
1106             '';
1107             }
1108             push @metalink, '', '';
1109            
1110             output_safe($metalinkfile, join('', map { "$_\n" } @metalink));
1111             $metalinkfile;
1112             }
1113              
1114             1;
1115              
1116              
1117             =back
1118              
1119             =head1 COPYRIGHT
1120              
1121             Copyright (C) 2000, 2001, 2002, 2003, 2004, 2005 MandrakeSoft SA
1122              
1123             Copyright (C) 2005-2010 Mandriva SA
1124              
1125             Copyright (C) 2011-2017 Mageia
1126              
1127             =cut