File Coverage

blib/lib/OrePAN2/Injector.pm
Criterion Covered Total %
statement 98 148 66.2
branch 16 44 36.3
condition 7 15 46.6
subroutine 22 26 84.6
pod 3 8 37.5
total 146 241 60.5


line stmt bran cond sub pod time code
1             package OrePAN2::Injector;
2              
3 5     5   107591 use strict;
  5         25  
  5         170  
4 5     5   27 use warnings;
  5         12  
  5         148  
5 5     5   30 use utf8;
  5         13  
  5         37  
6              
7 5     5   836 use Archive::Extract;
  5         200587  
  5         172  
8 5     5   3876 use Archive::Tar;
  5         200561  
  5         435  
9 5     5   824 use CPAN::Meta;
  5         33523  
  5         172  
10 5     5   34 use File::Basename qw(dirname basename);
  5         13  
  5         326  
11 5     5   1879 use File::Copy qw(copy);
  5         7711  
  5         311  
12 5     5   36 use File::Find qw(find);
  5         14  
  5         224  
13 5     5   31 use File::Path qw(mkpath);
  5         12  
  5         245  
14 5     5   29 use File::Spec;
  5         10  
  5         145  
15 5     5   29 use File::Temp qw(tempdir);
  5         10  
  5         211  
16 5     5   729 use File::pushd;
  5         1357  
  5         312  
17 5     5   859 use HTTP::Tiny;
  5         33815  
  5         180  
18 5     5   746 use MetaCPAN::Client;
  5         347917  
  5         9510  
19              
20             sub new {
21 16     16 1 1453322 my $class = shift;
22 16 50       129 my %args = @_ == 1 ? %{ $_[0] } : @_;
  0         0  
23 16 50       89 unless ( exists $args{directory} ) {
24 0         0 Carp::croak("Missing directory");
25             }
26             bless {
27 16         148 author => 'DUMMY',
28             %args
29             }, $class;
30             }
31              
32 32     32 1 1927 sub directory { shift->{directory} }
33              
34             sub inject {
35 16     16 1 131 my ( $self, $source, $opts ) = @_;
36             local $self->{author}
37 16   50     197 = $opts->{author} || $self->{author} || 'DUMMY';
38 16   100     128 local $self->{author_subdir} = $opts->{author_subdir} || '';
39              
40 16         40 my $tarpath;
41 16 50       575 if ( $source =~ /(?:^git(?:\+\w+)?:|\.git(?:@.+)?$)/ )
    100          
    100          
    50          
42             { # steal from App::cpanminus::script
43             # git URL has to end with .git when you need to use pin @ commit/tag/branch
44 0         0 my ( $uri, $commitish ) = split /(?<=\.git)@/i, $source, 2;
45              
46             # git CLI doesn't support git+http:// etc.
47 0         0 $uri =~ s/^git\+//;
48 0         0 $tarpath = $self->inject_from_git( $uri, $commitish );
49             }
50             elsif ( $source =~ m{\Ahttps?://} ) {
51 3         18 $tarpath = $self->inject_from_http($source);
52             }
53             elsif ( -f $source ) {
54 8         78 $tarpath = $self->inject_from_file($source);
55             }
56             elsif ( $source =~ m/^[\w_][\w0-9:_]+$/ ) {
57              
58 5   50     157 my $c = MetaCPAN::Client->new( version => 'v1' )
59             || die "Could not get MetaCPAN::Client";
60              
61 5   50     1754 my $mod = $c->module($source)
62             || die "Could not find $source";
63              
64 5   50     847010 my $rel = $c->release( $mod->distribution )
65             || die "Could not find distribution for $source";
66              
67 5   50     471893 my $url = $rel->download_url
68             || die "Could not find url for $source";
69              
70 5         138 $tarpath = $self->inject_from_http($url);
71             }
72             else {
73 0         0 die "Unknown source: $source\n";
74             }
75              
76 16         1873 return File::Spec->abs2rel(
77             File::Spec->rel2abs($tarpath),
78             $self->directory
79             );
80             }
81              
82             sub tarpath {
83 16     16 0 70 my ( $self, $basename ) = @_;
84              
85 16         57 my $author = uc( $self->{author} );
86             my $tarpath = File::Spec->catfile(
87             $self->directory, 'authors', 'id',
88             substr( $author, 0, 1 ),
89             substr( $author, 0, 2 ),
90             $author,
91             $self->{author_subdir},
92 16         105 $basename
93             );
94 16         26282 mkpath( dirname($tarpath) );
95              
96 16         112 return $tarpath;
97             }
98              
99             sub _detect_author {
100 2     2   9 my ( $self, $source, $archive ) = @_;
101 2         15 my $tmpdir = tempdir( CLEANUP => 1 );
102 2         916 my $ae = Archive::Extract->new( archive => $archive );
103 2         570 $ae->extract( to => $tmpdir );
104 2         669575 my $guard = pushd( glob("$tmpdir/*") );
105 2         429 $self->{author}->($source);
106             }
107              
108             sub inject_from_file {
109 8     8 0 32 my ( $self, $file ) = @_;
110              
111             local $self->{author} = $self->_detect_author( $file, $file )
112 8 100       53 if ref $self->{author} eq "CODE";
113 8         16753 my $basename = basename($file);
114 8         51 my $tarpath = $self->tarpath($basename);
115              
116 8 50       91 copy( $file, $tarpath )
117             or die "Copy failed $file $tarpath: $!\n";
118              
119 8         3639 return $tarpath;
120             }
121              
122             sub inject_from_http {
123 8     8 0 28 my ( $self, $url ) = @_;
124              
125             # If $self->{author} is not a code reference,
126             # then $tarpath is fixed before http request
127             # and HTTP::Tiny->mirror works correctly.
128             # So we treat that case first.
129 8 100       47 if ( ref $self->{author} ne "CODE" ) {
130 7         452 my $basename = basename($url);
131 7         43 my $tarpath = $self->tarpath($basename);
132 7         81 my $response = HTTP::Tiny->new->mirror( $url, $tarpath );
133 7 50       1736877 unless ( $response->{success} ) {
134 0         0 die
135             "Cannot fetch $url($response->{status} $response->{reason})\n";
136             }
137 7         3528 return $tarpath;
138             }
139              
140 1         4 my $tmpdir = tempdir( CLEANUP => 1 );
141 1         354 my $tmpfile = "$tmpdir/tmp.tar.gz";
142 1         10 my $response = HTTP::Tiny->new->mirror( $url, $tmpfile );
143 1 50       80283 unless ( $response->{success} ) {
144 0         0 die "Cannot fetch $url($response->{status} $response->{reason})\n";
145             }
146              
147 1         419 my $basename = basename($url);
148 1         8 local $self->{author} = $self->_detect_author( $url, $tmpfile );
149 1         328 my $tarpath = $self->tarpath($basename);
150 1 50       27 copy( $tmpfile, $tarpath )
151             or die "Copy failed $tmpfile $tarpath: $!\n";
152              
153 1         491 my $mtime = ( stat $tmpfile )[9];
154 1         23 utime $mtime, $mtime, $tarpath;
155              
156 1         122 return $tarpath;
157             }
158              
159             sub inject_from_git {
160 0     0 0   my ( $self, $repository, $branch ) = @_;
161              
162 0           my $tmpdir = tempdir( CLEANUP => 1 );
163              
164 0           my ( $basename, $tar, $author ) = do {
165 0           my $guard = pushd($tmpdir);
166              
167 0           _run("git clone $repository");
168              
169 0 0         if ($branch) {
170 0           my $guard2 = pushd( [<*>]->[0] );
171 0           _run("git checkout $branch");
172             }
173              
174 0           my $author;
175 0 0         if ( ref $self->{author} eq "CODE" ) {
176 0           my $guard2 = pushd( [<*>]->[0] );
177 0           $author = $self->{author}->($repository);
178             }
179              
180             # The repository needs to contains META.json in repository.
181 0           my $metafname = File::Spec->catfile( [<*>]->[0], 'META.json' );
182 0 0         unless ( -f $metafname ) {
183 0           die "$repository does not have a META.json\n";
184             }
185              
186 0           my $meta = CPAN::Meta->load_file($metafname);
187              
188 0           my $name = $meta->{name};
189 0           my $version = $meta->{version};
190              
191 0 0         rename( [<*>]->[0], "$name-$version" )
192             or die $!;
193              
194 0           my $tmp_path = File::Spec->catfile(
195             $tmpdir,
196             );
197              
198 0           my $tar = Archive::Tar->new();
199 0           my @files = $self->list_files($tmpdir);
200 0           $tar->add_files(@files);
201              
202 0           ( "$name-$version.tar.gz", $tar, $author );
203             };
204              
205 0 0         local $self->{author} = $author if $author;
206 0           my $tarpath = $self->tarpath($basename);
207              
208             # Must be same partition.
209 0           my $tmp_tarpath = File::Temp::mktemp("${tarpath}.XXXXXX");
210 0           $tar->write( $tmp_tarpath, COMPRESS_GZIP );
211 0 0         unlink $tarpath if -f $tarpath;
212 0 0         rename( $tmp_tarpath => $tarpath )
213             or die $!;
214              
215 0           return $tarpath;
216             }
217              
218             sub list_files {
219 0     0 0   my ( $self, $dir ) = @_;
220              
221 0           my @files;
222             find(
223             {
224             wanted => sub {
225 0     0     my $rel = File::Spec->abs2rel( $_, $dir );
226 0           my $top = [ File::Spec->splitdir($rel) ]->[1];
227 0 0 0       return if $top && $top eq '.git';
228 0 0         return unless -f $_;
229 0           push @files, $rel;
230             },
231 0           no_chdir => 1,
232             },
233             $dir,
234             );
235 0           return @files;
236             }
237              
238             sub _run {
239 0     0     print "% @_\n";
240              
241 0 0         system(@_) == 0 or die "ABORT\n";
242             }
243              
244             1;
245              
246             __END__
247              
248             =encoding utf-8
249              
250             =for stopwords DarkPAN orepan2-inject orepan2-indexer darkpan OrePAN1 OrePAN
251              
252             =head1 NAME
253              
254             OrePAN2::Injector - Inject a distribution to your DarkPAN
255              
256             =head1 SYNOPSIS
257              
258             use OrePAN2::Injector;
259              
260             my $injector = OrePAN2::Injector->new(directory => '/path/to/darkpan');
261              
262             $injector->inject(
263             'http://cpan.metacpan.org/authors/id/M/MA/MAHITO/Acme-Hoge-0.03.tar.gz',
264             { author => 'MAHITO' },
265             );
266              
267             =head1 DESCRIPTION
268              
269             OrePAN2::Injector allows you to inject a distribution into your DarkPAN.
270              
271             =head1 METHODS
272              
273             =head3 C<< my $injector = OrePAN2::Injector->new(%attr) >>
274              
275             Constructor. Here C<%attr> might be:
276              
277             =over 4
278              
279             =item * directory
280              
281             Your DarkPAN directory path. This is required.
282              
283             =item * author
284              
285             Default author of distributions.
286             If you omit this, then C<DUMMY> will be used.
287              
288             B<BETA>: As of OrePAN2 0.37,
289             the author attribute accepts a code reference, so that
290             you can calculate author whenever injecting distributions:
291              
292             my $author_cb = sub {
293             my $source = shift;
294             $source =~ m{authors/id/./../([^/]+)} ? $1 : "DUMMY";
295             };
296              
297             my $injector = OrePAN2::Injector->new(
298             directory => '/path/to/darkpan',
299             author => $author_cb,
300             );
301              
302             $injector->inject(
303             'http://cpan.metacpan.org/authors/id/M/MA/MAHITO/Acme-Hoge-0.03.tar.gz'
304             );
305             #=> Acme-Hoge-0.03 will be indexed with author MAHITO
306              
307             Note that the code reference C<$author_cb> will be executed
308             under the following circumstances:
309              
310             * the first argument is the $source argument to the inject method
311             * the working directory of it is the top level of the distribution in question
312              
313             =item * author_subdir
314              
315             This is an optional attribute. If present it means that directory elements
316             will be created following the author. This can be useful, for instance,
317             if you want to make your DarkPAN have paths that exactly match the paths
318             in CPAN. Sometimes CPAN paths look something like the following:
319              
320             authors/id/<author>/modules/...
321              
322             In the above case you can pass 'modules' as the value for author_subdir so
323             that the path OrePAN2 creates looks like the above path.
324              
325             =back
326              
327             =head3 C<< $injector->inject($source, \%option) >>
328              
329             Inject C<$source> to your DarkPAN. Here C<$source> is one of the following:
330              
331             =over 4
332              
333             =item * local archive file
334              
335             eg: /path/to/Text-TestBase-0.10.tar.gz
336              
337             =item * HTTP url
338              
339             eg: http://cpan.metacpan.org/authors/id/T/TO/TOKUHIROM/Text-TestBase-0.10.tar.gz
340              
341             =item * git repository
342              
343             eg: git://github.com/tokuhirom/Text-TestBase.git@master
344              
345             Note that you need to set up git repository as a installable git repo,
346             that is, you need to put a META.json in your repository.
347              
348             If you are using L<Minilla> or L<Milla>, your repository is already ready to install.
349              
350             Supports the following URL types:
351              
352             git+file://path/to/repo.git
353             git://github.com/plack/Plack.git@1.0000 # tag
354             git://github.com/plack/Plack.git@devel # branch
355              
356             They are compatible with L<cpanm>.
357              
358             =item * module name
359              
360             eg: Data::Dumper
361              
362             =back
363              
364             C<\%option> might be:
365              
366             =over 4
367              
368             =item * author
369              
370             Author of the distribution. This overrides C<new>'s author attribute.
371              
372             =back
373              
374             =head1 SEE ALSO
375              
376             L<orepan2-inject>
377              
378             =head1 LICENSE
379              
380             Copyright (C) tokuhirom.
381              
382             This library is free software; you can redistribute it and/or modify
383             it under the same terms as Perl itself.
384              
385             =head1 AUTHOR
386              
387             tokuhirom E<lt>tokuhirom@gmail.comE<gt>
388              
389             =cut