File Coverage

blib/lib/Perl/Download/FTP.pm
Criterion Covered Total %
statement 34 160 21.2
branch 3 84 3.5
condition 1 9 11.1
subroutine 9 15 60.0
pod 5 5 100.0
total 52 273 19.0


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