File Coverage

blib/lib/Archive/Tar/Merge.pm
Criterion Covered Total %
statement 115 125 92.0
branch 30 42 71.4
condition 3 3 100.0
subroutine 16 16 100.0
pod 0 4 0.0
total 164 190 86.3


line stmt bran cond sub pod time code
1             ###########################################
2             package Archive::Tar::Merge;
3             ###########################################
4              
5 5     5   105762 use strict;
  5         13  
  5         189  
6 5     5   24 use warnings;
  5         11  
  5         135  
7 5     5   4646 use Archive::Tar::Wrapper;
  5         756178  
  5         96  
8 5     5   206 use File::Temp qw(tempdir);
  5         12  
  5         322  
9 5     5   29 use Log::Log4perl qw(:easy);
  5         9  
  5         51  
10 5     5   3338 use File::Spec::Functions qw(abs2rel);
  5         8  
  5         377  
11 5     5   28 use File::Spec;
  5         9  
  5         55  
12 5     5   141 use File::Find;
  5         10  
  5         297  
13 5     5   33 use Digest::MD5;
  5         8  
  5         166  
14 5     5   84 use File::Basename;
  5         9  
  5         358  
15 5     5   5641 use Sysadm::Install qw(mkd);
  5         73414  
  5         41  
16              
17             our $VERSION = "0.01";
18              
19             ###########################################
20             sub new {
21             ###########################################
22 9     9 0 142614 my($class, %options) = @_;
23              
24 9         95 my $self = {
25             dest_tarball => undef,
26             source_tarballs => [],
27             hook => undef,
28             %options,
29             };
30              
31 9 50       32 if(@{ $self->{source_tarballs} } == 0) {
  9         52  
32 0         0 LOGDIE "Need at least one tarball to merge";
33             }
34              
35 9         20 for my $tarball (@{ $self->{source_tarballs} }) {
  9         42  
36 18 50       360 if(! -f $tarball) {
37 0         0 LOGDIE "Tarball not found: $tarball";
38             }
39             }
40              
41 9         40 bless $self, $class;
42              
43 9         66 $self->unpack_sources();
44 9         475 return $self;
45             }
46              
47             ###########################################
48             sub unpack_sources {
49             ###########################################
50 9     9 0 168 my($self) = @_;
51              
52 9         17 for my $source_tarball (@{ $self->{source_tarballs} }) {
  9         63  
53 18         255 my($tmpdir) = tempdir(CLEANUP => 1);
54              
55 18         14518 my %source = ();
56              
57 18         89 $source{dir} = $tmpdir;
58              
59 18         303 my $arch = Archive::Tar::Wrapper->new(tmpdir => $tmpdir);
60 18         28327 $arch->read($source_tarball);
61              
62 18         491277 $source{archive} = $arch;
63 18         187 $source{tarball} = $source_tarball;
64              
65 18         51 push @{ $self->{sources} }, \%source;
  18         676  
66             }
67             }
68              
69             ######################################
70             sub merge {
71             ######################################
72 9     9 0 14172 my($self) = @_;
73              
74 9         665 my $out_dir = tempdir(CLEANUP => 1);
75 9         14723 my $out_tar = Archive::Tar::Wrapper->new(tmpdir => $out_dir);
76              
77 9         153 DEBUG "Merging tarballs ",
78 9         14305 join(', ', @{ $self->{source_tarballs} }), ".";
79              
80 9         166 my $paths = {};
81              
82             # Build the following data structure:
83             # rel/path1:
84             # digests => digest1 => abs/path1
85             # digest2 => abs/path2
86             # paths => [abs/path1, abs/path2]
87             # rel/path3:
88             # digests => digest3 => abs/path3
89             # paths => [abs/path3]
90             # ...
91 9         36 for my $source (@{ $self->{sources} }) {
  9         72  
92 18         104 my $dir = $source->{dir};
93             find(sub {
94 89 100   89   8871 return unless -f;
95 35         553 my $rel = abs2rel($File::Find::name, $dir);
96             # Two down
97 35         4688 $rel =~ s#.*?/.*?/##;
98              
99 35         168 my $digest = file_hash($File::Find::name);
100            
101             # avoid autovivification
102 35 100 100     309 if(!exists $paths->{$rel} or
103             !exists $paths->{$rel}->{digests}->{$digest}) {
104             # create the data structure shown above
105 33         323 $paths->{$rel}->{digests}->{$digest} = $File::Find::name;
106 33         55 push @{$paths->{$rel}->{paths}}, $File::Find::name;
  33         1383  
107             }
108 18         2778 }, $dir);
109             }
110              
111             # Traverse and figure out conflicts
112 9         154 for my $relpath (keys %$paths) {
113              
114 26         10493 my $dst_dir = File::Spec->catfile($out_dir, dirname($relpath));
115 26         76 my @digests = keys %{$paths->{$relpath}->{digests}};
  26         142  
116            
117 26         2137 my $dst_entry = File::Spec->catfile($dst_dir,
118             basename($relpath));
119 26         69 my $dst_content;
120              
121 26 50       587 mkd $dst_dir unless -d $dst_dir;
122              
123 26         79 my $src_entry = $paths->{$relpath}->{paths}->[0];
124              
125 26 50       1164 if(-l $src_entry) {
126 0         0 DEBUG "Symlinking $src_entry to $dst_entry";
127 0 0       0 symlink(readlink($src_entry), $dst_entry) or
128             LOGDIE("symlinking $dst_entry failed: $!");
129 0         0 next;
130             }
131              
132 26 100       90 if(@digests == 1) {
133              
134             # A unique file. Take it as-is, but call the
135             # he hook if there's one defined.
136 19 100       73 if(defined $self->{hook}) {
137 4         25 my $hook = $self->{hook}->(
138             $relpath,
139             $paths->{$relpath}->{paths},
140             $out_tar,
141             );
142              
143 4 100       92 if(0) {
    100          
144 0         0 } elsif(defined $hook->{action}) {
145 1 50       90 if($hook->{action} eq "ignore") {
146 1         10 DEBUG "Ignoring $relpath per hook";
147 1         11 next;
148             } else {
149 0         0 LOGDIE "Unknown action from hook: ",
150             "$hook->{action}";
151             }
152             } elsif(defined $hook->{content}) {
153 2         9 $dst_content = $hook->{content};
154             } else {
155             # No action from hook, leave the file unmodified
156             }
157             }
158              
159             } else {
160              
161             # Several different versions of the file, call the
162             # decider to pick one
163 7 100       36 if(defined $self->{decider}) {
164 6         54 my $decision = $self->{decider}->(
165             $relpath,
166             $paths->{$relpath}->{paths},
167             $out_tar,
168             );
169              
170 6 100       1639 if(0) {
    100          
    50          
171 0         0 } elsif(defined $decision->{action}) {
172 2 50       13 if($decision->{action} eq "ignore") {
173 2         22 DEBUG "Ignoring $relpath per decider";
174 2         27 next;
175             } else {
176 0         0 LOGDIE "Unknown action from decider: ",
177             "$decision->{action}";
178             }
179             } elsif(defined $decision->{index}) {
180 2         12 $src_entry =
181             $paths->{$relpath}->{paths}->[ $decision->{index} ];
182              
183             } elsif(defined $decision->{content}) {
184 2         11 $dst_content = $decision->{content};
185             } else {
186 0         0 LOGDIE "Decider failed to return decision";
187             }
188             } else {
189 1         9 LOGDIE "Conflict: $relpath (and no decider defined)";
190             }
191             }
192              
193 22 100       69 if(defined $dst_content) {
194 4         44 $out_tar->add($relpath, \$dst_content);
195             } else {
196 18         135 $out_tar->add($relpath, $src_entry);
197             }
198             }
199              
200 8 50       5223 if(defined $self->{dest_tarball}) {
201 8         20 my $compress = 0;
202 8 50       103 if($self->{dest_tarball} =~ /gz$/) {
203 8         23 $compress = 1;
204             }
205            
206 8         160 $out_tar->write($self->{dest_tarball}, $compress);
207             }
208              
209 8         214849 return $out_tar;
210             }
211              
212             ######################################
213             sub file_hash {
214             ######################################
215 35     35 0 68 my($filename) = @_;
216              
217 35 50       2183 open FH, "<$filename" or LOGDIE "Cannot open $filename";
218              
219 35         364 my $ctx = Digest::MD5->new();
220 35         872 $ctx->addfile(*FH);
221 35         310 my $digest = $ctx->hexdigest;
222              
223 35         541 close FH;
224              
225 35         236 return $digest;
226             }
227              
228             1;
229              
230             __END__