File Coverage

blib/lib/CPAN/InGit/Server.pm
Criterion Covered Total %
statement 119 132 90.1
branch 23 38 60.5
condition 15 39 38.4
subroutine 24 24 100.0
pod 10 10 100.0
total 191 243 78.6


line stmt bran cond sub pod time code
1             package CPAN::InGit::Server;
2             our $VERSION = '0.003'; # VERSION
3             # ABSTRACT: A Mojolicious::Controller that serves the ArchiveTrees from a git repo
4              
5              
6 1     1   922 use Carp;
  1         5  
  1         103  
7 1     1   7 use Scalar::Util 'refaddr', 'blessed';
  1         2  
  1         64  
8 1     1   7 use JSON::PP;
  1         2  
  1         74  
9 1     1   7 use Time::Piece;
  1         21  
  1         12  
10 1     1   114 use Log::Any '$log';
  1         3  
  1         12  
11 1     1   315 use Archive::Tar;
  1         3  
  1         76  
12 1     1   7 use IO::Compress::Gzip qw( gzip $GzipError );
  1         1  
  1         247  
13 1     1   9 use CPAN::InGit;
  1         2  
  1         45  
14 1     1   5 use Mojo::Base 'Mojolicious::Controller';
  1         3  
  1         12  
15 1     1   15323 use v5.36;
  1         5  
16              
17              
18 8     8 1 15 sub cpan_repo($c) { $c->stash('cpan_repo') }
  8         14  
  8         14  
  8         24  
19              
20 15     15 1 27 sub archive_tree($c) {
  15         26  
  15         23  
21 15   66     124 $c->stash->{archive_tree} //= do {
22 6         102 my $branch_name= $c->branch_name;
23 6         93 my $cache= $c->branch_cache;
24 6         70 my $atree= $cache->{$branch_name};
25 6 100       26 if ($atree) {
26             # check whether branch has updated since cached
27 5         21 my $current_tree= $c->cpan_repo->lookup_tree($branch_name);
28 5 100 66     104 unless (defined $current_tree and $atree->tree->id eq $current_tree->id) {
29 1         4 delete $cache->{$branch_name};
30 1         37 $atree= undef;
31             }
32             }
33 6 100 66     36 if (!$atree && ($atree= $c->cpan_repo->get_archive_tree($branch_name))) {
34 2 50 33     11 if ($c->branch_head_only && !defined $atree->branch) {
35 0         0 $c->log->debug("Branch '$branch_name' is not a branch HEAD");
36 0         0 $atree= undef;
37             } else {
38 2         41 $cache->{$branch_name}= $atree;
39             }
40             }
41 6         35 $atree;
42             };
43             }
44              
45 6     6 1 41 sub branch_name($c) { $c->stash('branch_name') }
  6         12  
  6         11  
  6         19  
46              
47 6     6 1 11 sub branch_cache($c) {
  6         10  
  6         11  
48 6   33     18 $c->stash->{branch_cache} //= do {
49 0         0 warn "no branch_cache, creating temporary";
50 0         0 $c->_new_cache
51             }
52             }
53              
54 2     2 1 4 sub branch_head_only($c) { $c->stash('branch_head_only') }
  2         4  
  2         3  
  2         10  
55              
56             sub _new_cache {
57             # the 'recent_limit' feature was added in 0.20
58 1     1   423 state $have_tree_rb_xs= eval 'use Tree::RB::XS 0.20';
  0     1   0  
  0         0  
  1         164  
59 1         10 my %hash;
60 1 50       4 tie %hash, 'Tree::RB::XS', track_recent => 1, recent_limit => 20
61             if $have_tree_rb_xs;
62 1         7 \%hash;
63             }
64              
65              
66 1     1 1 85380 sub mount($class, $base_route, $cpan_repo, %options) {
  1         4  
  1         2  
  1         3  
  1         2  
  1         3  
67 1         3 my $atree= $options{archive_tree};
68 1 50       4 if ($atree) {
69 0 0 0     0 croak "Not an ArchiveTree"
70             unless blessed($atree) && $atree->can('get_path');
71             } else {
72             # Ensure there is a cache if serving all branches
73 1   33     11 $options{branch_cache} //= $class->_new_cache;
74             }
75 1         10 $base_route= $base_route->to(namespace => '', controller => $class, cpan_repo => $cpan_repo, %options);
76 1 50       58 my $tree_route= $atree? $base_route
77             : $base_route->any('/:branch_name');
78 1         543 $tree_route->get('/modules/02packages.details', [ format => ['txt','txt.gz'] ])
79             ->to(action => 'serve_package_details');
80 1         574 $tree_route->get('/authors/id/*author_path')->to(action => 'serve_author_file');
81             }
82              
83              
84 6     6 1 15 sub check_branch_exists($c) {
  6         12  
  6         12  
85 6 50       21 unless ($c->archive_tree) {
86 0         0 $c->render(text => 'Git branch does not exist', status => 404);
87 0         0 return undef;
88             }
89 6         26 return 1;
90             }
91              
92              
93 2     2 1 44839 sub serve_package_details($c) {
  2         6  
  2         5  
94 2 50       19 return undef unless $c->check_branch_exists;
95 2         7 my $blob= $c->archive_tree->package_details_blob;
96             $c->respond_to(
97 1     1   1018 txt => sub { $c->render(text => $blob->content) },
98 1     1   859 'txt.gz' => sub { $c->render_gzipped($blob->content, '02packages.details.txt.gz') },
99 2         35 );
100             }
101              
102              
103 4     4 1 50313 sub serve_author_file($c) {
  4         11  
  4         7  
104 4 50       37 return undef unless $c->check_branch_exists;
105 4         19 my $path= 'authors/id/'.$c->stash->{author_path};
106             # cpanm adds extra '/' when the path name doesn't match the expected A/AU/AUTHOR format
107 4         62 $path =~ s,//+,/,g;
108 4         48 my ($basename)= ($path =~ m,([^/]+)\z,);
109             # Does the exact file exist?
110 4         16 my $ent= $c->archive_tree->get_path($path);
111 4 50       44 $c->log->debug("Path $path is ".($ent? ($ent->[0]->is_blob? 'blob ':'tree ').$ent->[0]->id : ''));
    100          
112 4 100 33     308 if ($ent) {
    50 33        
    50 33        
      33        
      33        
      33        
113 3 50       30 if ($ent->[0]->is_blob) {
114 3         15 $c->res->headers->content_disposition(qq{attachment; filename="$basename"});
115 3         133 $c->render(data => $ent->[0]->content);
116             } else {
117 0         0 $c->render(status => 403, text => 'not a file');
118             }
119             }
120             elsif ($path =~ /(.*?)\.gz\z/
121             && ($ent= $c->archive_tree->get_path($1)) && $ent->[0]->is_blob
122             ) {
123 0         0 $c->log->debug("Path $1 is ".$ent->[0]->id.', will gzip it');
124 0         0 $c->render_gzipped($ent->[0]->content, $basename);
125             }
126             elsif ($path =~ /(.*?)\.tar\.gz\z/
127             # Don't auto-tar trees that don't look like distributions
128             && ($ent= $c->archive_tree->get_path("$1.meta")) && $ent->[0]->is_blob
129             && ($ent= $c->archive_tree->get_path($1)) && $ent->[0]->is_tree
130             ) {
131 1         7 $c->log->debug("Path $1 is ".$ent->[0]->id.', will tar+gzip it');
132 1         57 my $tar= Archive::Tar->new;
133 1         19 $c->cpan_repo->add_git_tree_to_tar($tar, substr($basename, 0, -7), $ent->[0]);
134 1         414 $c->render_gzipped($tar->write, $basename);
135             }
136             else {
137 0         0 $c->render(status => 404, text => 'No such path in branch');
138             }
139             }
140              
141              
142 2     2 1 712 sub render_gzipped($c, $data, $filename='') {
  2         5  
  2         5  
  2         5  
  2         4  
143 2         4 my $gzipped;
144 2 50       16 gzip \$data => \$gzipped
145             or return $c->reply->exception("gzip failed: $GzipError");
146 2         5202 $c->res->headers->content_type('application/gzip');
147 2 50       85 $c->res->headers->content_disposition(qq{attachment; filename="$filename"})
148             if length $filename;
149 2         57 return $c->render(data => $gzipped);
150             }
151              
152             1;
153              
154             __END__