File Coverage

blib/lib/Perl/Download/FTP.pm
Criterion Covered Total %
statement 34 201 16.9
branch 3 108 2.7
condition 1 14 7.1
subroutine 9 18 50.0
pod 6 6 100.0
total 53 347 15.2


line stmt bran cond sub pod time code
1             package Perl::Download::FTP;
2 5     5   2430 use strict;
  5         25  
  5         114  
3 5     5   20 use warnings;
  5         6  
  5         94  
4 5     5   67 use 5.10.1;
  5         15  
5 5     5   29 use Carp;
  5         7  
  5         394  
6 5     5   2636 use Net::FTP;
  5         463118  
  5         299  
7 5     5   2538 use File::Copy;
  5         18937  
  5         308  
8 5     5   34 use Cwd;
  5         10  
  5         246  
9 5     5   31 use File::Spec;
  5         9  
  5         11138  
10             our $VERSION = '0.05';
11              
12             =head1 NAME
13              
14             Perl::Download::FTP - Identify Perl releases and download the most recent via FTP
15              
16             =head1 SYNOPSIS
17              
18             use Perl::Download::FTP;
19              
20             $self = Perl::Download::FTP->new( {
21             host => 'ftp.cpan.org',
22             dir => '/pub/CPAN/src/5.0',
23             verbose => 1,
24             } );
25              
26             @all_releases = $self->ls();
27              
28             $classified_releases = $self->classify_releases();
29              
30             @releases = $self->list_releases( {
31             type => 'production',
32             compression => 'gz',
33             } );
34              
35             $latest_release = $self->get_latest_release( {
36             compression => 'gz',
37             type => 'dev',
38             path => '/path/to/download',
39             verbose => 1,
40             } );
41              
42             $specific_release = $self->get_specific_release( {
43             release => 'perl-5.27.2.tar.xz',
44             path => '/path/to/download',
45             } );
46              
47             =head1 DESCRIPTION
48              
49             This library provides (a) methods for obtaining a list of all Perl 5 releases
50             which are available for FTP download; and (b) a method for obtaining the most
51             recent release.
52              
53             =head2 Compression Formats
54              
55             Perl releases have, over time, used three different compression formats:
56             C, C and C. C is the one that has been used in every
57             production, development and release candidate release, so that is the default
58             value used herein. All three compression formats are available for use herein.
59              
60             =head2 Testing
61              
62             This library can only be truly tested by attempting live FTP connections and
63             downloads of Perl 5 source code tarballs. Since testing over the internet
64             can be problematic when being conducted in an automatic manner or when the
65             user is behind a firewall, the test files under F will only be run live
66             when you say:
67              
68             export PERL_ALLOW_NETWORK_TESTING=1 && make test
69              
70             Each test file further attempts to confirm the possibility of making an FTP
71             connection by using CPAN library Test::RequiresInternet.
72              
73             =head1 METHODS
74              
75             =head2 C
76              
77             =over 4
78              
79             =item * Purpose
80              
81             Perl::Download::FTP constructor.
82              
83             =item * Arguments
84              
85             $self = Perl::Download::FTP->new();
86              
87             $self = Perl::Download::FTP->new( {
88             host => 'ftp.cpan.org',
89             dir => '/pub/CPAN/src/5.0',
90             verbose => 1,
91             } );
92              
93             $self = Perl::Download::FTP->new( {
94             host => 'ftp.cpan.org',
95             dir => '/pub/CPAN/src/5.0',
96             Timeout => 5,
97             } );
98              
99             Takes a hash reference with, typically, two elements: C and C.
100             Any options which can be passed to F may also be passed as
101             key-value pairs. When no argument is provided, the values shown above for
102             C and C will be used. You may enter values for any CPAN mirror
103             which provides FTP access. (See L and
104             L.) You may also pass C for more
105             descriptive output; by default, this is off.
106              
107             =item * Return Value
108              
109             Perl::Download::FTP object.
110              
111             =item * Comment
112              
113             The method establishes an FTP connection to , logs you in as an
114             anonymous user, and changes directory to C.
115              
116             Wrapper around Net::FTP object. You will get Net::FTP error messages at any
117             point of failure. Uses FTP C mode.
118              
119             =back
120              
121             =cut
122              
123             sub new {
124 2     2 1 936 my ($class, $args) = @_;
125 2   50     8 $args //= {};
126 2 100       209 croak "Argument to constructor must be hashref"
127             unless ref($args) eq 'HASH';
128              
129 1         7 my %default_args = (
130             host => 'ftp.cpan.org',
131             dir => '/pub/CPAN/src/5.0',
132             verbose => 0,
133             );
134 1         7 my $default_args_string = join('|' => keys %default_args);
135 1         60 my %netftp_options = (
136             Firewall => undef,
137             FirewallType => undef,
138             BlockSize => 10240,
139             Port => undef,
140             SSL => undef,
141             Timeout => 120,
142             Debug => 0,
143             Passive => 1,
144             Hash => undef,
145             LocalAddr => undef,
146             Domain => undef,
147             );
148 1         9 my %permitted_args = map {$_ => 1} (
  14         33  
149             keys %default_args,
150             keys %netftp_options,
151             );
152              
153 1         4 for my $k (keys %{$args}) {
  1         6  
154             croak "Argument '$k' not permitted in constructor"
155 1 50       123 unless $permitted_args{$k};
156             }
157              
158 0           my $data;
159             # Populate object starting with default host and directory
160 0           while (my ($k,$v) = each %default_args) {
161 0           $data->{$k} = $v;
162             }
163             # Then add Net::FTP plausible defaults
164 0           while (my ($k,$v) = each %netftp_options) {
165 0           $data->{$k} = $v;
166             }
167             # Then override with key-value pairs passed to new()
168 0           while (my ($k,$v) = each %{$args}) {
  0            
169 0           $data->{$k} = $v;
170             }
171              
172             # For the Net::FTP constructor, we don't need 'dir' and 'host'
173 0           my %passed_netftp_options;
174 0           for my $k (keys %{$data}) {
  0            
175 0 0         $passed_netftp_options{$k} = $data->{$k}
176             unless ($k =~ m/^($default_args_string)$/);
177             }
178              
179 0 0         my $ftp = Net::FTP->new($data->{host}, %passed_netftp_options)
180             or croak "Cannot connect to $data->{host}: $@";
181              
182 0 0         $ftp->login("anonymous",'-anonymous@')
183             or croak "Cannot login ", $ftp->message;
184              
185             $ftp->cwd($data->{dir})
186 0 0         or croak "Cannot change to working directory $data->{dir}", $ftp->message;
187              
188 0           $data->{ftp} = $ftp;
189              
190 0           my @compressions = (qw| gz bz2 xz |);
191 0           $data->{eligible_compressions} = { map { $_ => 1 } @compressions };
  0            
192 0           $data->{compression_string} = join('|' => @compressions);
193             $data->{eligible_types} = {
194 0           production => 'prod',
195             prod => 'prod',
196             development => 'dev',
197             dev => 'dev',
198             rc => 'rc',
199             dev_or_rc => 'dev_or_rc',
200             };
201              
202 0           return bless $data, $class;
203             }
204              
205             =head2 C
206              
207             =over 4
208              
209             =item * Purpose
210              
211             Identify all Perl releases.
212              
213             =item * Arguments
214              
215             @all_releases = $self->ls();
216              
217             Returns list of all Perl core tarballs on the FTP host.
218              
219             @all_gzipped_releases = $self->ls('gz');
220              
221             Returns list of only those all tarballs on the FTP host which are compressed
222             in C<.gz> format. Also available (in separate calls): C, C.
223              
224             =item * Return Value
225              
226             List of strings like:
227              
228             "perl-5.10.0-RC2.tar.gz",
229             "perl-5.10.0.tar.gz",
230             "perl-5.26.0.tar.gz",
231             "perl-5.26.1-RC1.tar.gz",
232             "perl-5.27.0.tar.gz",
233             "perl-5.6.0.tar.gz",
234             "perl-5.6.1-TRIAL1.tar.gz",
235             "perl-5.6.1-TRIAL2.tar.gz",
236             "perl-5.6.1-TRIAL3.tar.gz",
237             "perl5.003_07.tar.gz",
238             "perl5.004.tar.gz",
239             "perl5.004_01.tar.gz",
240             "perl5.005.tar.gz",
241             "perl5.005_01.tar.gz",
242              
243             "perl-5.10.1.tar.bz2",
244             "perl-5.12.2-RC1.tar.bz2",
245             "perl-5.26.1-RC1.tar.bz2",
246             "perl-5.27.0.tar.bz2",
247             "perl-5.8.9.tar.bz2",
248              
249             "perl-5.21.10.tar.xz",
250             "perl-5.21.6.tar.xz",
251             "perl-5.22.0-RC1.tar.xz",
252             "perl-5.22.0.tar.xz",
253             "perl-5.22.1-RC4.tar.xz",
254             "perl-5.26.1.tar.xz",
255             "perl-5.27.2.tar.xz",
256              
257             =back
258              
259             =cut
260              
261             sub ls {
262 0     0 1   my ($self, $compression) = @_;
263 0 0         if (! defined $compression) {
264 0           $compression = $self->{compression_string};
265             }
266             else {
267             croak "ls(): Bad compression format: $compression"
268 0 0         unless $self->{eligible_compressions}{$compression};
269             }
270             my @all_releases = grep {
271 0           /^perl
272             (?:
273             -5\.\d+\.\d+ # 5.6.0 and above
274             |
275             5\.00\d(_\d{2})? # 5.003_007 thru 5.005
276             )
277             .*? # Account for RC and TRIAL
278             \.tar # We only want tarballs
279             \.(?:${compression}) # Compression format
280             $/x
281             } $self->{ftp}->ls()
282 0 0         or croak "Unable to perform FTP 'get' call to host: $!";
283 0           $self->{all_releases} = \@all_releases;
284 0           my $location = "ftp://$self->{host}$self->{dir}";
285             say "Identified ",
286             scalar(@all_releases),
287             " perl releases at $location"
288 0 0         if $self->{verbose};
289 0           return @all_releases;
290             }
291              
292             =head2 C
293              
294             =over 4
295              
296             =item * Purpose
297              
298             Categorize releases as production, development or RC (release candidate).
299              
300             =item * Arguments
301              
302             None. Works on data stored in object by C.
303              
304             =item * Return Value
305              
306             Hash reference.
307              
308             =back
309              
310             =cut
311              
312             sub classify_releases {
313 0     0 1   my $self = shift;
314              
315 0           my %versions;
316 0           for my $tb (@{$self->{all_releases}}) {
  0            
317 0           my ($major, $minor, $rc);
318 0 0         if ($tb =~ m/^
    0          
319             perl-5\.(\d+)
320             \.(\d+)
321             (?:-((?:TRIAL|RC)\d+))?
322             \.tar\.(?:gz|bz2|xz)
323             $/x) {
324 0           ($major, $minor, $rc) = ($1,$2,$3);
325 0 0         if ($major % 2 == 0) {
326 0 0         unless (defined $rc) {
327 0           $versions{prod}{$tb} = {
328             tarball => $tb,
329             major => $major,
330             minor => $minor,
331             }
332             }
333             else {
334 0           $versions{rc}{$tb} = {
335             tarball => $tb,
336             major => $major,
337             minor => $minor,
338             rc => $rc,
339             }
340             }
341             }
342             else {
343 0           $versions{dev}{$tb} = {
344             tarball => $tb,
345             major => $major,
346             minor => $minor,
347             }
348             }
349             }
350             elsif ($tb =~ m/^
351             perl5\.
352             (00\d)
353             (?:_(\d{2}))? # 5.003_007 thru 5.005; account for RC and TRIAL
354             .*?
355             \.tar # We only want tarballs
356             \.gz # Compression format
357             $/x
358             ) {
359 0           my $early_dev;
360 0           ($major, $early_dev) = ($1,$2);
361 0   0       $early_dev //= '';
362 0 0         if (! $early_dev) {
363 0           $versions{prod}{$tb} = {
364             tarball => $tb,
365             major => $major,
366             minor => '',
367             }
368             }
369             else {
370 0           $versions{dev}{$tb} = {
371             tarball => $tb,
372             major => $major,
373             minor => $early_dev,
374             }
375             }
376             }
377             }
378 0           $self->{versions} = \%versions;
379 0           return \%versions;
380             }
381              
382             sub _compression_check {
383 0     0     my ($self, $compression) = @_;
384 0 0         if (! defined $compression) {
385 0           return 'gz';
386             }
387             else {
388             croak "ls(): Bad compression format: $compression"
389 0 0         unless $self->{eligible_compressions}{$compression};
390 0           return $compression;
391             }
392             }
393              
394             sub _prepare_list {
395 0     0     my ($self, $compression) = @_;
396 0           $compression = $self->_compression_check($compression);
397              
398 0 0         unless (exists $self->{versions}) {
399 0           $self->classify_releases();
400             }
401 0           return $compression;
402             }
403              
404             =head2 C
405              
406             =over 4
407              
408             =item * Purpose
409              
410             List all releases for a specified compression format and release type, sorted
411             in reverse logical order.
412              
413             =item * Arguments
414              
415             @releases = $self->list_releases( {
416             type => 'production',
417             compression => 'gz',
418             } );
419              
420             Takes a hash reference with, typically two elements:
421              
422             =over 4
423              
424             =item * C
425              
426             Available values:
427              
428             gz bz2 xz
429              
430             Defaults to C.
431              
432             =item * C
433              
434             Available values:
435              
436             production prod
437             development dev
438             rc
439             dev_or_rc
440              
441             Defaults to C. Selecting C will return any release which is
442             either C or C.
443              
444             =back
445              
446             =item * Return Value
447              
448             List of strings naming Perl release tarballs for the specified compression
449             format and type. The list is sorted in reverse logical order, I the
450             newest production release will be the first item in the list and the oldest
451             will be the last. So, for instance, the list of development releases in C
452             format will start with something like:
453              
454             perl-5.27.5.tar.gz
455             perl-5.27.4.tar.gz
456             perl-5.27.3.tar.gz
457              
458             and end with:
459              
460             perl5.004_02.tar.gz
461             perl5.004_01.tar.gz
462             perl5.003_07.tar.gz
463              
464             =back
465              
466             =cut
467              
468             sub list_releases {
469 0     0 1   my ($self, $args) = @_;
470 0   0       $args ||= {};
471 0 0         croak "Argument to method must be hashref"
472             unless ref($args) eq 'HASH';
473 0           my $type;
474 0 0         if (defined $args->{type}) {
475             croak "Bad value for 'type': $args->{type}"
476 0 0         unless $self->{eligible_types}->{$args->{type}};
477 0           $type = $self->{eligible_types}->{$args->{type}};
478             }
479             else {
480 0           $type = 'dev';
481             }
482              
483 0           my $compression = 'gz';
484 0 0         if (exists $args->{compression}) {
485 0           $compression = $self->_compression_check($args->{compression});
486             }
487 0           $compression = $self->_prepare_list($compression);
488              
489             say "Preparing list of '$type' releases with '$compression' compression"
490 0 0         if $self->{verbose};
491 0           my @these_releases;
492 0 0         if ($type eq 'prod') {
    0          
    0          
493 0           @these_releases = $self->_get_prod_or_dev($compression, $type);
494 0           $self->{"${compression}_${type}_releases"} = \@these_releases;
495 0           return @these_releases;
496             }
497             elsif ($type eq 'dev') {
498 0           @these_releases = $self->_get_prod_or_dev($compression, $type);
499 0           $self->{"${compression}_${type}_releases"} = \@these_releases;
500 0           return @these_releases;
501             }
502             elsif ($type eq 'rc') {
503 0           @these_releases = $self->_get_rc($compression, $type);
504 0           $self->{"${compression}_${type}_releases"} = \@these_releases;
505 0           return @these_releases;
506             }
507             else { # $type eq dev_or_rc
508 0           my @dev_releases = grep { /\.${compression}$/ }
509 0           keys %{$self->{versions}->{dev}};
  0            
510 0           my @rc_releases = grep { /\.${compression}$/ }
511 0           keys %{$self->{versions}->{rc}};
  0            
512 0           my %in;
513 0           for my $r (@dev_releases, @rc_releases) {
514 0           my ($stem) = $r =~ m/^(.*?)\.tar/;
515 0           my ($core) = $stem =~ m/^perl-?5\.(.*)/;
516 0 0         if ($core =~ m/^00(\d)_(\d+)$/) {
    0          
517 0           my ($minor, $patch) = ($1,$2);
518 0           ($in{$r}{minor}, $in{$r}{patch}) = ($minor, $patch + 0);
519             }
520             elsif ($core =~ m/^(\d+)\.(.*?)(?:-((?:TRIAL|RC)\d+))?$/) {
521 0           my ($minor, $patch, $rc) = ($1,$2, $3);
522 0   0       ($in{$r}{minor}, $in{$r}{patch}, $in{$r}{rc}) = ($minor, $patch, $rc || '');
523             }
524             }
525             @these_releases = sort {
526 0           $in{$b}{minor} <=> $in{$a}{minor} ||
527             $in{$b}{patch} <=> $in{$a}{patch} ||
528             $in{$b}{rc} cmp $in{$a}{rc}
529 0 0 0       } keys %in;
530              
531 0           $self->{"${compression}_${type}_releases"} = \@these_releases;
532 0           return @these_releases;
533             }
534             }
535              
536             sub _get_prod_or_dev {
537 0     0     my ($self, $compression, $type) = @_;
538             my @these_releases =
539 0           grep { /\.${compression}$/ } sort {
540             $self->{versions}->{$type}{$b}{major} <=> $self->{versions}->{$type}{$a}{major} ||
541             $self->{versions}->{$type}{$b}{minor} <=> $self->{versions}->{$type}{$a}{minor}
542 0 0         } keys %{$self->{versions}->{$type}};
  0            
  0            
543 0           return @these_releases;
544             }
545              
546             sub _get_rc {
547 0     0     my ($self, $compression, $type) = @_;
548             my @these_releases =
549 0           grep { /\.${compression}$/ } sort {
550             $self->{versions}->{$type}{$b}{major} <=> $self->{versions}->{$type}{$a}{major} ||
551             $self->{versions}->{$type}{$b}{minor} <=> $self->{versions}->{$type}{$a}{minor} ||
552             $self->{versions}->{$type}{$b}{rc} cmp $self->{versions}->{$type}{$a}{rc}
553 0 0 0       } keys %{$self->{versions}->{$type}};
  0            
  0            
554 0           return @these_releases;
555             }
556              
557             =head2 C
558              
559             =over 4
560              
561             =item * Purpose
562              
563             Download the latest release via FTP.
564              
565             =item * Arguments
566              
567             $latest_release = $self->get_latest_release( {
568             compression => 'gz',
569             type => 'dev',
570             path => '/path/to/download',
571             verbose => 1,
572             } );
573              
574             Possible values for C and C are the same as for C.
575              
576             =item * Return Value
577              
578             Scalar holding path to download of tarball.
579              
580             =back
581              
582             =cut
583              
584             sub get_latest_release {
585 0     0 1   my ($self, $args) = @_;
586 0 0         croak "Argument to method must be hashref"
587             unless ref($args) eq 'HASH';
588 0           my $type;
589 0 0         if (defined $args->{type}) {
590             croak "Bad value for 'type': $args->{type}"
591 0 0         unless $self->{eligible_types}->{$args->{type}};
592 0           $type = $self->{eligible_types}->{$args->{type}};
593             }
594             else {
595 0           $type = 'dev';
596             }
597              
598 0           my $compression = 'gz';
599 0 0         if (exists $args->{compression}) {
600 0           $compression = $self->_compression_check($args->{compression});
601             }
602 0           my $cache = "${compression}_${type}_releases";
603              
604 0           my $path = cwd();
605 0 0         if (exists $args->{path}) {
606 0 0         croak "Value for 'path' not found" unless (-d $args->{path});
607 0           $path = $args->{path};
608             }
609 0           my $latest;
610 0 0         if (exists $self->{$cache}) {
611 0 0         say "Identifying latest $type release from cache" if $self->{verbose};
612 0           $latest = $self->{$cache}->[0];
613             }
614             else {
615 0 0         say "Identifying latest $type release" if $self->{verbose};
616 0           my @releases = $self->list_releases( {
617             compression => $compression,
618             type => $type,
619             } );
620 0           $latest = $releases[0];
621             }
622 0 0         say "Performing FTP 'get' call for: $latest" if $self->{verbose};
623 0           my $starttime = time();
624 0 0         $self->{ftp}->get($latest)
625             or croak "Unable to perform FTP get call: $!";
626 0           my $endtime = time();
627             say "Elapsed time for FTP 'get' call: ", $endtime - $starttime, " seconds"
628 0 0         if $self->{verbose};
629 0           my $rv = File::Spec->catfile($path, $latest);
630 0 0         move $latest, $rv or croak "Unable to move $latest to $path";
631 0 0         say "See: $rv" if $self->{verbose};
632 0           return $rv;
633             }
634              
635             =head2 C
636              
637             =over 4
638              
639             =item * Purpose
640              
641             Download a specific release via FTP.
642              
643             =item * Arguments
644              
645             $specific_release = $self->get_specific_release( {
646             release => 'perl-5.27.2.tar.xz',
647             path => '/path/to/download',
648             } );
649              
650             =item * Return Value
651              
652             Scalar holding path to download of tarball.
653              
654             =back
655              
656             =cut
657              
658             sub get_specific_release {
659 0     0 1   my ($self, $args) = @_;
660 0 0         croak "Argument to method must be hashref"
661             unless ref($args) eq 'HASH';
662              
663 0           my $path = cwd();
664 0 0         if (exists $args->{path}) {
665 0 0         croak "Value for 'path' not found" unless (-d $args->{path});
666 0           $path = $args->{path};
667             }
668              
669 0           my @all_releases = $self->ls;
670 0           my %all_releases = map {$_ => 1} @all_releases;
  0            
671             croak "$args->{release} not found among releases at ftp://$self->{host}$self->{dir}"
672 0 0         unless $all_releases{$args->{release}};
673              
674 0 0         say "Performing FTP 'get' call for: $args->{release}" if $self->{verbose};
675 0           my $starttime = time();
676             $self->{ftp}->get($args->{release})
677 0 0         or croak "Unable to perform FTP get call: $!";
678 0           my $endtime = time();
679             say "Elapsed time for FTP 'get' call: ", $endtime - $starttime, " seconds"
680 0 0         if $self->{verbose};
681 0           my $rv = File::Spec->catfile($path, $args->{release});
682 0 0         move $args->{release}, $rv
683             or croak "Unable to move $args->{release} to $path";
684 0 0         say "See: $rv" if $self->{verbose};
685 0           return $rv;
686             }
687              
688             =head1 BUGS AND SUPPORT
689              
690             Please report any bugs by mail to C
691             or through the web interface at L.
692              
693             =head1 ACKNOWLEDGEMENTS
694              
695             Thanks for feedback from Chad Granum, Kent Fredric and David Golden
696             in the perl.cpan.workers newsgroup.
697              
698             =head1 AUTHOR
699              
700             James E Keenan
701             CPAN ID: JKEENAN
702             jkeenan@cpan.org
703             http://thenceforward.net/perl
704              
705             =head1 COPYRIGHT
706              
707             This program is free software; you can redistribute
708             it and/or modify it under the same terms as Perl itself.
709              
710             The full text of the license can be found in the
711             LICENSE file included with this module.
712              
713             Copyright James E Keenan 2018. All rights reserved.
714              
715             =head1 SEE ALSO
716              
717             perl(1). Net::FTP(3). Test::RequiresInternet(3).
718              
719             =cut
720              
721             1;