File Coverage

blib/lib/OrePAN2/Injector.pm
Criterion Covered Total %
statement 99 147 67.3
branch 14 40 35.0
condition 7 15 46.6
subroutine 22 26 84.6
pod 1 6 16.6
total 143 234 61.1


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