File Coverage

blib/lib/Package/Locator/Index.pm
Criterion Covered Total %
statement 1 3 33.3
branch n/a
condition n/a
subroutine 1 1 100.0
pod n/a
total 2 4 50.0


line stmt bran cond sub pod time code
1             # ABSTRACT: The package index of a repository
2              
3             package Package::Locator::Index;
4              
5 1     1   126859 use Moose;
  0            
  0            
6             use MooseX::Types::URI qw(Uri);
7             use MooseX::Types::Path::Class;
8             use MooseX::MarkAsMethods (autoclean => 1);
9              
10             use Carp;
11             use File::Temp;
12             use Path::Class;
13             use IO::Zlib;
14             use LWP::UserAgent;
15             use URI::Escape;
16             use URI;
17              
18             #------------------------------------------------------------------------
19              
20             our $VERSION = '0.010'; # VERSION
21              
22             #------------------------------------------------------------------------
23              
24              
25             has repository_url => (
26             is => 'ro',
27             isa => Uri,
28             required => 1,
29             coerce => 1,
30             );
31              
32             #------------------------------------------------------------------------
33              
34              
35             has user_agent => (
36             is => 'ro',
37             isa => 'LWP::UserAgent',
38             default => sub { LWP::UserAgent->new() },
39             );
40              
41             #------------------------------------------------------------------------
42              
43              
44             has cache_dir => (
45             is => 'ro',
46             isa => 'Path::Class::Dir',
47             default => sub { Path::Class::Dir->new( File::Temp::tempdir(CLEANUP => 1) ) },
48             coerce => 1,
49             );
50              
51             #------------------------------------------------------------------------
52              
53              
54             has force => (
55             is => 'ro',
56             isa => 'Bool',
57             default => 0,
58             );
59              
60             #------------------------------------------------------------------------
61              
62              
63             has index_file => (
64             is => 'ro',
65             isa => 'Path::Class::File',
66             init_arg => undef,
67             lazy_build => 1,
68             );
69              
70             #------------------------------------------------------------------------
71              
72              
73             has distributions => (
74             is => 'ro',
75             isa => 'HashRef',
76             init_arg => undef,
77             default => sub { $_[0]->_data->{distributions} },
78             lazy => 1,
79             );
80              
81              
82             #------------------------------------------------------------------------
83              
84              
85              
86             has packages => (
87             is => 'ro',
88             isa => 'HashRef',
89             init_arg => undef,
90             default => sub { $_[0]->_data->{packages} },
91             lazy => 1,
92             );
93              
94              
95             #------------------------------------------------------------------------
96              
97              
98             has _data => (
99             is => 'ro',
100             isa => 'HashRef',
101             init_arg => undef,
102             lazy_build => 1,
103             );
104              
105             #------------------------------------------------------------------------------
106              
107             sub _build_index_file {
108             my ($self) = @_;
109              
110             my $repos_url = $self->repository_url->canonical()->as_string();
111             $repos_url =~ s{ /*$ }{}mx; # Remove trailing slash
112             $repos_url = URI->new($repos_url); # Reconstitute as URI object
113              
114             my $cache_dir = $self->cache_dir->subdir( URI::Escape::uri_escape($repos_url) );
115             $self->__mkpath($cache_dir);
116              
117             my $destination = $cache_dir->file('02packages.details.txt.gz');
118             $destination->remove() if -e $destination and $self->force();
119              
120             my $source = URI->new( "$repos_url/modules/02packages.details.txt.gz" );
121              
122             my $response = $self->user_agent->mirror($source, $destination);
123             $self->__handle_ua_response($response, $source, $destination);
124              
125             return $destination;
126             }
127              
128             #------------------------------------------------------------------------------
129              
130             sub _build__data {
131             my ($self) = @_;
132              
133             my $file = $self->index_file->stringify;
134             my $fh = IO::Zlib->new($file, 'rb') or croak "Failed to open index file $file: $!";
135             my $index_data = $self->__read_index($fh);
136             close $fh;
137              
138             return $index_data;
139             }
140              
141             #------------------------------------------------------------------------------
142              
143             sub __read_index {
144             my ($self, $fh) = @_;
145              
146             my $inheader = 1;
147             my $packages = {};
148             my $distributions = {};
149             my $source = $self->repository_url();
150              
151             while (<$fh>) {
152              
153             if ($inheader) {
154             $inheader = 0 if not m/ \S /x;
155             next;
156             }
157              
158             chomp;
159             my ($package, $version, $dist_path) = split;
160             my $dist_struct = $distributions->{$dist_path} ||= { source => $source, path => $dist_path };
161             my $pkg_struct = {name => $package, version => $version, distribution => $dist_path};
162             push @{ $dist_struct->{packages} ||= [] }, $pkg_struct;
163             $packages->{$package} = $pkg_struct;
164              
165             }
166              
167             return { packages => $packages,
168             distributions => $distributions };
169             }
170              
171             #------------------------------------------------------------------------
172              
173             sub __handle_ua_response {
174             my ($self, $response, $source, $destination) = @_;
175              
176             return 1 if $response->is_success(); # Ok
177             return 1 if $response->code() == 304; # Not modified
178             croak sprintf 'Request to %s failed: %s', $source, $response->status_line();
179             }
180              
181             #------------------------------------------------------------------------------
182              
183             sub __mkpath {
184             my ($self, $dir) = @_;
185              
186             return 1 if -e $dir;
187             $dir = dir($dir) unless eval { $dir->isa('Path::Class::Dir') };
188             return $dir->mkpath() or croak "Failed to make directory $dir: $!";
189             }
190              
191             #------------------------------------------------------------------------
192              
193             __PACKAGE__->meta->make_immutable();
194              
195             #------------------------------------------------------------------------
196             1;
197              
198             __END__
199              
200             =pod
201              
202             =for :stopwords Jeffrey Ryan Thalhammer Imaginative Software Systems
203              
204             =head1 NAME
205              
206             Package::Locator::Index - The package index of a repository
207              
208             =head1 VERSION
209              
210             version 0.010
211              
212             =head1 SYNOPSIS
213              
214             use Package::Locator::Index;
215              
216             my $index = Package::Locator::Index->new( repository_url => 'http://somewhere' );
217             my $dist = $index->distributions->{'A/AU/AUTHOR/Foo-Bar-1.0.tar.gz'};
218             my $pkg = $index->packages->{'Foo::Bar'};
219              
220             =head1 DESCRIPTION
221              
222             B<This is a private module and there are no user-serviceable parts
223             here. The API documentation is for my own reference only.>
224              
225             L<Package::Locator::Index> is yet-another module for parsing the
226             contents of the F<02packages.details.txt> file from a CPAN-like
227             repository.
228              
229             =head1 CONSTRUCTOR
230              
231             =head2 new( %attributes )
232              
233             All the attributes listed below can be passed to the constructor, and
234             can be retrieved via accessor methods with the same name. All
235             attributes are read-only, and cannot be changed once the object is
236             constructed.
237              
238             =head1 ATTRIBUTES
239              
240             =head2 repository_url => 'http://somewhere'
241              
242             The base URL of the repository you want to get the index from. This
243             is usually a CPAN mirror, but can be any site or directory that is
244             organized in a CPAN-like structure. This attribute is required.
245              
246             =head2 user_agent => $user_agent_obj
247              
248             The L<LWP::UserAgent> object that will fetch the index file. If you
249             do not provide a user agent, then a default one will be constructed
250             for you.
251              
252             =head2 cache_dir => '/some/directory/path'
253              
254             The path (as a string or L<Path::Class::Dir> object) to a directory
255             where the index file will be cached. If the directory does not exist,
256             it will be created for you. If you do not specify a cache directory,
257             then a temporary directory will be used. The temporary directory will
258             be deleted when your application terminates.
259              
260             =head2 force => $boolean
261              
262             Causes any cached index files to be removed, thus forcing a new one to
263             be downloaded when the object is constructed. This only has effect if
264             you specified the C<cache_dir> attribute. The default is false.
265              
266             =head1 METHODS
267              
268             =head2 index_file()
269              
270             Returns the path to the local copy of the index file (as a
271             L<Path::Class::File>).
272              
273             =head2 distributions
274              
275             Returns a hashref representing the contents of the index. The keys
276             are the paths to the distributions (as they appear in the index). The
277             values are data structures that look like this:
278              
279             {
280             path => 'A/AU/AUTHOR/FooBar-1.0.tar.gz',
281             source => 'http://some.cpan.mirror'
282             packages => [ ## See package structure below ## ]
283             }
284              
285             =head2 packages
286              
287             Returns a hashref representing the contents of the index. The keys
288             are the names of packages. The values are data structures that look
289             like this:
290              
291             {
292             name => 'Foo',
293             version => '1.0',
294             distribution => 'A/AU/AUTHOR/FooBar-1.0.tar.gz'
295             }
296              
297             =head1 MOTIVATION
298              
299             There are numerous existing modules for parsing the
300             F<02packages.details.txt> file, but I wasn't completely happy with any
301             of them. Most of the existing modules transform the data into various
302             flavors of Distribution and Package objects. But I'm not ready to
303             commit to any particular API for Distributions and Packages (not even
304             one of my own). So L<Package::Locator::Index> exposes the index data
305             as simple data structures.
306              
307             =head1 AUTHOR
308              
309             Jeffrey Ryan Thalhammer <jeff@imaginative-software.com>
310              
311             =head1 COPYRIGHT AND LICENSE
312              
313             This software is copyright (c) 2011 by Imaginative Software Systems.
314              
315             This is free software; you can redistribute it and/or modify it under
316             the same terms as the Perl 5 programming language system itself.
317              
318             =cut