#!/usr/bin/perl # vim: set ts=4 : use strict; use warnings FATAL => 'all'; use feature qw(switch say state); use version 0.77; our $VERSION = version->declare(sprintf "v%s", q$Revision: 1.4 $ =~ /(\d[.\d]+)/); use File::Basename qw(basename dirname); # # Download files in parallel, both internally (multiple connections per file) # and multiple files at once. See comments in the FastDownloader.pm library. # # Author: Jim Avera (jim.avera AT gmail dot com) # http://abhweb.org/jima my $USAGE = "USAGE: ${\basename($0)} [GENERAL OPTIONS] file... Each may be any of these forms: A single url string (same as -u thestring) -letters arg arg ... where indicates what kind of args follow, and may be any of the following in any order: u - url to download from s - expected file size c - hex checksum (md5 by default; may be ALG:sum) o - outpath filename. May be absolute or relative to destdir (default: current directory) For example, to download to /tmp/myfile.pdf: ${\basename($0)} -d /tmp -uo http://srvr/x/foo.pdf myfile.pdf To download /cache/dir/foo.deb and /cache/dir/bar.deb, checking the size of the first and the md5 sum of the second, and downloading a third file to an absolute path checking both size an md5: ${\basename($0)} -d /cache/dir \\ -us http://host1/x/y/foo.deb 1234 \\ -uc http://host2/p/q/bar.deb 401b30e3b8b5d629635a5c613cdb7919 \\ -uosc http://host3/a/b/mum.ble /path/to/mymum.ble 1234 401b3... GENERAL OPTIONS -d,--destdir default-destination-directory -n,--dry-run -j,--num-threads \$destdir, "n|dry-run" => \$dryrun, "q|quiet" => \$quiet, "s|silent" => \$silent, "debug" => \$debug, "j|jobs|num-threads:i" => \$num_threads, "h|help" => \$help, ) or die $USAGE."(bad args)\n"; if ($help) { print $USAGE; exit 0; } $num_threads //= 2; die "Invalid number of threads: $num_threads\n" unless $num_threads =~ /^[1-9]\d*$/; $destdir //= "."; die "westdir '$destdir' is not a directory\n" unless -d $destdir; $quiet = $silent = 0 if $debug; $quiet //= $silent; #------------------------------------------------------ # Parse the special-form args giving the files to download etc. #------------------------------------------------------ my @items; while (@ARGV) { if ($ARGV[0] =~ /^-([a-z]+)$/) { my $letters = $1; shift @ARGV; my %args; foreach (split //, $letters) { die "Unrecognized letter '$_' in -$letters\n" unless /^[usco]$/; die "Not enough args following '-$letters'\n" if @ARGV==0; $args{$_} = shift @ARGV; } die "URL must be specified ('-$letters' is not valid)\n" unless $args{u}; push @items, [ @args{qw/u s c o/} ]; } elsif ($ARGV[0] =~ /^\w+:\/\//) { push @items, [$ARGV[0],undef,undef,undef]; shift @ARGV; } else { die "Invalid special argument '$ARGV[0]'\n"; } } foreach (@items) { my ($u, $s, $c, $o) = @$_; die "Invalid URL '$u'\n" if $u !~ m#^\w+://[^/]+/#; die "Invalid size '$s'\n" if defined($s) && $s !~ /^\d+$/; die "Invalid digest checksum'$c'\n" if defined($c) && $c !~ /^(\w+:)?[0-9a-fA-F]+$/; } #------------------------------------------------------ # Do the downloads #------------------------------------------------------ my @fspecs; foreach (@items) { my ($u, $s, $c, $o) = @$_; push @fspecs, { url => $u, local_file => $o, destdir => $destdir, callback => "filecheck_callback", # These are for us, used by the callback: ExpectedSize => $s, Csum => $c, }; } my $obj = FastDownloader->new( filespecs => \@fspecs, num_threads => $num_threads, dryrun => $dryrun, quiet => $quiet, debug => $debug, ); $obj->wait(); my $stats = $obj->get_stats(); #------------------------------------------------------ # Print statistics #------------------------------------------------------ unless ($silent) { if (! $stats->{failed_file_count} && $stats->{file_count} == 0) { warn "> No files were downloaded.\n"; } else { printf STDERR "> Downloaded $stats->{file_count} files (%.1f MiB", ($stats->{total_bytes} / $MiB); if ($stats->{elapsed_secs} != 0) { printf STDERR ' @ %.1f MiB/sec', ($stats->{total_bytes} / $MiB)/$stats->{elapsed_secs}; } else { print STDERR " in less than one second"; } print STDERR ") using $num_threads threads\n"; if ($stats->{failed_file_count}) { warn "> $stats->{failed_file_count} FAILED to download successfully\n"; } } } exit 0; sub filecheck_callback($$$) { my ($fspec, $temp_path, $final_path) = @_; # Check the file size if (defined $fspec->{ExpectedSize}) { my $actual_size = (-s $temp_path); if ($actual_size != $fspec->{ExpectedSize}) { warn "File-size mis-match on $temp_path\n", " Expected $fspec->{ExpectedSize} bytes, got $actual_size bytes\n"; return 0; } } # Check the hash. The Csum prefix is normally "MD5Sum:" # but could specify another algorithm. if (defined $fspec->{Csum}) { my ($hashname, $expected_digest) = ($fspec->{Csum} =~ /(\w+)Sum:(.*)/); my $hasher = Digest->new($hashname); # e.g "MD5" open(my $fhandle, "<$temp_path") or die "$temp_path : $!"; binmode $fhandle; $hasher->addfile($fhandle); my $actual_digest = $hasher->hexdigest(); if ($actual_digest ne $expected_digest) { warn "$hashname mis-match on $temp_path\n", " expected: $expected_digest\n", " actual: $actual_digest\n"; return 0; } } return 1; }