File Coverage

blib/lib/Perl/Download/FTP/Distribution.pm
Criterion Covered Total %
statement 36 92 39.1
branch 5 40 12.5
condition 2 7 28.5
subroutine 9 11 81.8
pod 3 3 100.0
total 55 153 35.9


line stmt bran cond sub pod time code
1             package Perl::Download::FTP::Distribution;
2 3     3   1293 use strict;
  3         17  
  3         68  
3 3     3   13 use warnings;
  3         4  
  3         63  
4 3     3   36 use 5.10.1;
  3         8  
5 3     3   14 use Carp;
  3         4  
  3         227  
6 3     3   1585 use Net::FTP;
  3         269942  
  3         148  
7 3     3   1338 use File::Copy;
  3         10853  
  3         164  
8 3     3   18 use Cwd;
  3         5  
  3         155  
9 3     3   16 use File::Spec;
  3         5  
  3         2529  
10             our $VERSION = '0.04';
11              
12             =head1 NAME
13              
14             Perl::Download::FTP::Distribution - Identify CPAN distributions and download the most recent tarball via FTP
15              
16             =head1 SYNOPSIS
17              
18             use Perl::Download::FTP::Distribution;
19              
20             $self = Perl::Download::FTP::Distribution->new( {
21             host => 'ftp.cpan.org',
22             dir => 'pub/CPAN/modules/by-module',
23             distribution => 'Test-Smoke',
24             verbose => 1,
25             } );
26              
27             @all_releases = $self->ls();
28              
29             $latest_release = $self->get_latest_release( {
30             path => '/path/to/download',
31             verbose => 1,
32             } );
33              
34             =head1 DESCRIPTION
35              
36             This library provides (a) methods for obtaining a list of all releases
37             available on CPAN for a given Perl distribution; and (b) a method for
38             downloading the most recent release or a specific release.
39              
40             This library is similar to F contained in this same CPAN
41             distribution, except that in this module our objective is to download a CPAN
42             library rather than a tarball of the Perl 5 core distribution.
43              
44             =head2 Testing
45              
46             This library can only be truly tested by attempting live FTP connections and
47             downloads of tarballs of CPAN distributions. Since testing over the internet
48             can be problematic when being conducted in an automatic manner or when the
49             user is behind a firewall, the test files under F will only be run live
50             when you say:
51              
52             export PERL_ALLOW_NETWORK_TESTING=1 && make test
53              
54             Each test file further attempts to confirm the possibility of making an FTP
55             connection by using CPAN library Test::RequiresInternet.
56              
57             =head1 METHODS
58              
59             =head2 C
60              
61             =over 4
62              
63             =item * Purpose
64              
65             Perl::Download::FTP::Distribution constructor.
66              
67             =item * Arguments
68              
69             $self = Perl::Download::FTP::Distribution->new( {
70             distribution => 'Test-Smoke',
71             } );
72              
73             $self = Perl::Download::FTP::Distribution->new( {
74             distribution => 'Test-Smoke',
75             host => 'ftp.cpan.org',
76             dir => 'pub/CPAN/modules/by-module',
77             verbose => 1,
78             } );
79              
80             $self = Perl::Download::FTP::Distribution->new( {
81             distribution => 'Test-Smoke',
82             host => 'ftp.cpan.org',
83             dir => 'pub/CPAN/modules/by-module',
84             Timeout => 5,
85             } );
86              
87             Takes a hash reference with, typically, three elements: C,
88             C and C.
89              
90             =over 4
91              
92             =item *
93              
94             The C element is mandatory; its value must be spelled with
95             hyphens (I, C, rather than with the double colons used for
96             modules (C).
97              
98             =item *
99              
100             When no argument is provided for either of C or C, the values shown
101             above for C and C will be used. You may enter values for any CPAN
102             mirror which provides FTP access. (See L and
103             L.)
104              
105             =item *
106              
107             Any options which can be passed to F may also be passed as
108             key-value pairs.
109              
110             =item *
111              
112             You may also pass C for more descriptive output; by default, this is
113             off.
114              
115             =back
116              
117             =item * Return Value
118              
119             Perl::Download::FTP::Distribution object.
120              
121             =item * Comments
122              
123             The method establishes an FTP connection to , logs you in as an
124             anonymous user, and changes directory to C.
125              
126             Wrapper around Net::FTP object. You will get Net::FTP error messages at any
127             point of failure. Uses FTP C mode.
128              
129             Note that the value for C on a given CPAN FTP mirror is different from
130             the value for C one would use in downloading a Perl 5 core distribution
131             tarball via F.
132              
133             =back
134              
135             =cut
136              
137             sub new {
138 3     3 1 1234 my ($class, $args) = @_;
139 3   100     11 $args //= {};
140 3 100       177 croak "Argument to constructor must be hashref"
141             unless ref($args) eq 'HASH';
142             croak "Must provide 'distribution' element"
143 2 100       76 unless $args->{distribution};
144              
145             # The value for 'dir' we pass to the constructor differs among FTP
146             # mirrors but is uniform within a given mirror. However, it is *not* the
147             # directory to which we will actually change down below. That's because
148             # the tarballs are stored one directory farther down, in a directory named
149             # by the "top-level" of the distribution's name. So, for example, on
150             # ftp.cpan.org, Test-Smoke-1.71.tar.gz will be found in:
151             # pub/CPAN/modules/by-module/Test/
152             # rather than in:
153             # pub/CPAN/modules/by-module/
154              
155 1         5 my ($host_subdir) = $args->{distribution} =~ m/^([^-]+)/;
156              
157 1         5 my %default_args = (
158             host => 'ftp.cpan.org',
159             dir => 'pub/CPAN/modules/by-module',
160             verbose => 0,
161             );
162 1         5 my $default_args_string = join('|' => keys %default_args);
163 1         9 my %netftp_options = (
164             Firewall => undef,
165             FirewallType => undef,
166             BlockSize => 10240,
167             Port => undef,
168             SSL => undef,
169             Timeout => 120,
170             Debug => 0,
171             Passive => 1,
172             Hash => undef,
173             LocalAddr => undef,
174             Domain => undef,
175             );
176 1         4 my %permitted_args = map {$_ => 1} (
  15         22  
177             'distribution',
178             keys %default_args,
179             keys %netftp_options,
180             );
181              
182 1         9 for my $k (keys %{$args}) {
  1         3  
183             croak "Argument '$k' not permitted in constructor"
184 1 50       78 unless $permitted_args{$k};
185             }
186              
187 0           my $data;
188             # Populate object starting with default host and directory
189 0           while (my ($k,$v) = each %default_args) {
190 0           $data->{$k} = $v;
191             }
192             # Then add Net::FTP plausible defaults
193 0           while (my ($k,$v) = each %netftp_options) {
194 0           $data->{$k} = $v;
195             }
196             # Then override with key-value pairs passed to new()
197 0           while (my ($k,$v) = each %{$args}) {
  0            
198 0           $data->{$k} = $v;
199             }
200              
201             # For the Net::FTP constructor, we don't need 'dir' and 'host'
202 0           my %passed_netftp_options;
203 0           for my $k (keys %{$data}) {
  0            
204 0 0         $passed_netftp_options{$k} = $data->{$k}
205             unless ($k =~ m/^($default_args_string)$/);
206             }
207              
208 0 0         my $ftp = Net::FTP->new($data->{host}, %passed_netftp_options)
209             or croak "Cannot connect to $data->{host}: $@";
210              
211 0 0         $ftp->login("anonymous",'-anonymous@')
212             or croak "Cannot login ", $ftp->message;
213              
214 0           $data->{subdir} = "$data->{dir}/$host_subdir";
215             $ftp->cwd($data->{subdir})
216 0 0         or croak "Cannot change to working directory $data->{subdir}", $ftp->message;
217              
218 0           $data->{ftp} = $ftp;
219              
220 0           return bless $data, $class;
221             }
222              
223             =head2 C
224              
225             =over 4
226              
227             =item * Purpose
228              
229             Identify all currently available tarballs of the CPAN distribution in question.
230              
231             =item * Arguments
232              
233             @all_releases = $self->ls();
234              
235             None; all information needed is in the object.
236              
237             =item * Return Value
238              
239             List of strings like:
240              
241             'Test-Smoke-1.53.tar.gz',
242             'Test-Smoke-1.59.tar.gz',
243             'Test-Smoke-1.6.tar.gz',
244             'Test-Smoke-1.70.tar.gz',
245             'Test-Smoke-1.71.tar.gz',
246              
247             =item * Comment
248              
249             Results do not include versions which have been moved to BackCPAN.
250              
251             =back
252              
253             =cut
254              
255             sub ls {
256 0     0 1   my ($self) = shift;
257 0           my $extensions = qr/(?:tar\.(?:g?z|bz2)|zip|tgz)/;
258             my @releases = grep
259 0           { m/^$self->{distribution}-v?\d+\.\d+(?:\.\d+)?\.$extensions$/ }
260             $self->{ftp}->ls()
261 0 0         or croak "Unable to perform FTP 'get' call to host: $!";
262 0           my %releases;
263 0           for my $r (@releases) {
264 0           my ($v, $w, $x, $y);
265 0 0         if (($v) = $r =~ m/^$self->{distribution}-v?(\d+\.\d+)\.$extensions$/) {
    0          
266 0           $releases{$r} = $v;
267             }
268             elsif (($w, $x, $y) = $r =~ m/^$self->{distribution}-v?(\d+)\.(\d+)\.(\d+)\.$extensions$/) {
269 0           my $v = sprintf("%04d%04d%04d" => ($w, $x, $y));
270 0           $releases{$r} = $v;
271             }
272             else {
273 0           croak "Unable to analyze $r";
274             }
275             }
276 0           my @sorted_releases = sort { $releases{$a} <=> $releases{$b} } keys %releases;;
  0            
277 0           $self->{cache} = [ @sorted_releases ];
278 0           return @sorted_releases;;
279             }
280              
281              
282             =head2 C
283              
284             =over 4
285              
286             =item * Purpose
287              
288             Download the latest release via FTP.
289              
290             =item * Arguments
291              
292             $latest_release = $self->get_latest_release( {
293             path => '/path/to/download',
294             verbose => 1,
295             } );
296              
297             Takes a hash reference with two possible elements: C and C.
298             The value of C should be a string holding the path to the directory to
299             which the tarball will be downloaded. If not provided, the tarball will be
300             downloaded to the current working directory.
301              
302             =item * Return Value
303              
304             Scalar holding path to download of tarball.
305              
306             =back
307              
308             =cut
309              
310             sub get_latest_release {
311 0     0 1   my ($self, $args) = @_;
312 0 0         croak "Argument to method must be hashref"
313             unless ref($args) eq 'HASH';
314              
315 0           my $path = cwd();
316 0 0         if (exists $args->{path}) {
317 0 0         croak "Value for 'path' not found" unless (-d $args->{path});
318 0           $path = $args->{path};
319             }
320 0           my $latest;
321 0 0 0       if (exists $self->{cache} and ref($self->{cache}) eq 'ARRAY' and scalar(@{$self->{cache}})) {
  0   0        
322 0           $latest = $self->{cache}->[-1];
323 0 0         say "Latest release $latest identified from cached ls() call" if $args->{verbose};
324             }
325             else {
326 0           my @releases = $self->ls();
327 0           $latest = $releases[-1];
328             }
329              
330 0 0         say "Performing FTP 'get' call for: $latest" if $self->{verbose};
331 0           my $starttime = time();
332 0 0         $self->{ftp}->get($latest)
333             or croak "Unable to perform FTP get call: $!";
334 0           my $endtime = time();
335             say "Elapsed time for FTP 'get' call: ", $endtime - $starttime, " seconds"
336 0 0         if $self->{verbose};
337 0           my $rv = File::Spec->catfile($path, $latest);
338 0 0         move $latest, $rv or croak "Unable to move $latest to $path";
339 0 0         say "See: $rv" if $self->{verbose};
340 0           return $rv;
341             }
342              
343             1;
344              
345             =head1 BUGS AND SUPPORT
346              
347             Please report any bugs by mail to C
348             or through the web interface at L.
349              
350             =head1 ACKNOWLEDGEMENTS
351              
352             Thanks for feedback from Chad Granum, Kent Fredric and David Golden
353             in the perl.cpan.workers newsgroup.
354              
355             =head1 AUTHOR
356              
357             James E Keenan
358             CPAN ID: JKEENAN
359             jkeenan@cpan.org
360             http://thenceforward.net/perl
361              
362             =head1 COPYRIGHT
363              
364             This program is free software; you can redistribute
365             it and/or modify it under the same terms as Perl itself.
366              
367             The full text of the license can be found in the
368             LICENSE file included with this module.
369              
370             Copyright James E Keenan 2018. All rights reserved.
371              
372             =head1 SEE ALSO
373              
374             perl(1). Net::FTP(3). Test::RequiresInternet(3).
375              
376             =cut
377              
378             1;
379