File Coverage

blib/lib/PAR/Repository/Client/DBM.pm
Criterion Covered Total %
statement 119 158 75.3
branch 41 76 53.9
condition 13 30 43.3
subroutine 14 14 100.0
pod 7 7 100.0
total 194 285 68.0


line stmt bran cond sub pod time code
1             package PAR::Repository::Client::DBM;
2              
3 9     9   169 use 5.006;
  9         27  
  9         358  
4 9     9   51 use strict;
  9         17  
  9         304  
5 9     9   41 use warnings;
  9         19  
  9         412  
6              
7             our $VERSION = '0.24';
8              
9 9     9   47 use Carp qw/croak/;
  9         15  
  9         19124  
10              
11             =head1 NAME
12              
13             PAR::Repository::Client::DBM - Contains all the DBM access functions
14              
15             =head1 SYNOPSIS
16              
17             use PAR::Repository::Client;
18              
19             =head1 DESCRIPTION
20              
21             This module implements access to the underlying DBMs.
22              
23             All of the methods described here shouldn't be used frivolously in user
24             code even if some of them are part of the API and are guaranteed not
25             to change.
26              
27             =cut
28              
29              
30             =head2 need_dbm_update
31              
32             Takes one or no arguments. Without arguments, all DBM files are
33             checked. With an argument, only the specified DBM file will be checked.
34              
35             Returns true if either one of the following conditions match:
36              
37             =over 2
38              
39             =item
40              
41             The repository does not support checksums.
42              
43             =item
44              
45             The checksums (and thus also the DBM files) haven't been
46             downloaded yet.
47              
48             =item
49              
50             The local copies of the checksums do not match those of the repository.
51              
52             =back
53              
54             In cases two and three above, the return value is actually the hash
55             reference of checksums that was fetched from the repository.
56              
57             Returns the empty list if the local checksums match those of the
58             repository exactly.
59              
60             You don't usually need to call this directly. By default, DBM files
61             are only fetched from the repository if necessary.
62              
63             =cut
64              
65             sub need_dbm_update {
66 45     45 1 99 my $self = shift;
67 45         86 $self->{error} = undef;
68              
69 45         88 my $check_file = shift;
70 45 100 66     348 $check_file .= '.zip' if defined $check_file and not $check_file =~ /\.zip$/;
71              
72 45         93 my $support = $self->{supports_checksums};
73 45 50 66     252 if (defined $support and not $support) {
74 0         0 return 1;
75             }
76              
77 45         227 my $checksums = $self->_dbm_checksums();
78 45 100       163 $self->{last_checksums_refresh} = time() if $self->{checksums_timeout};
79              
80 45 50       112 if (not defined $checksums) {
81 0         0 $self->{supports_checksums} = 0;
82 0         0 return 1;
83             }
84             else {
85 45         101 $self->{supports_checksums} = 1;
86             }
87              
88 45 100 100     163 if (not defined $self->{checksums} or keys %{$self->{checksums}} == 0) {
  36         176  
89             # never fetched checksums before.
90 13         44 return $checksums;
91             }
92             else {
93             # we fetched checksums earlier, match them
94 32         83 my $local_checksums = $self->{checksums};
95 32 100       90 if (not defined $check_file) {
96 3 50       33 return $checksums if keys(%$local_checksums) != keys(%$checksums);
97 0         0 foreach my $file (keys %$checksums) {
98 0 0 0     0 return $checksums
99             if not exists $local_checksums->{$file}
100             or not $local_checksums->{$file} eq $checksums->{$file};
101             }
102             }
103             else {
104 29 50 66     253 return $checksums
      33        
105             if not exists $local_checksums->{$check_file}
106             or not exists $checksums->{$check_file} # shouldn't happen
107             or not $local_checksums->{$check_file} eq $checksums->{$check_file};
108             }
109 22         125 return();
110             }
111             }
112              
113              
114             =head2 modules_dbm
115              
116             Fetches the C database from the repository,
117             ties it to a L object and returns a tied hash
118             reference or the empty list on failure. Second return
119             value is the name of the local temporary file.
120              
121             In case of failure, an error message is available via
122             the C method.
123              
124             The method uses the C<_fetch_dbm_file()> method which must be
125             implemented in a subclass such as L.
126              
127             =cut
128              
129             sub modules_dbm {
130 25     25 1 89 my $self = shift;
131 25         146 return( $self->_get_a_dbm('modules', PAR::Repository::Client::MODULES_DBM_FILE()) );
132             }
133              
134              
135             =head2 scripts_dbm
136              
137             Fetches the C database from the repository,
138             ties it to a L object and returns a tied hash
139             reference or the empty list on failure. Second return
140             value is the name of the local temporary file.
141              
142             In case of failure, an error message is available via
143             the C method.
144              
145             The method uses the C<_fetch_dbm_file()> method which must be
146             implemented in a subclass such as L.
147              
148             =cut
149              
150             sub scripts_dbm {
151 2     2 1 6 my $self = shift;
152 2         9 return( $self->_get_a_dbm('scripts', PAR::Repository::Client::SCRIPTS_DBM_FILE()) );
153             }
154              
155              
156             =head2 dependencies_dbm
157              
158             Fetches the C database from the repository,
159             ties it to a L object and returns a tied hash
160             reference or the empty list on failure. Second return
161             value is the name of the local temporary file.
162              
163             In case of failure, an error message is available via
164             the C method.
165              
166             The method uses the C<_fetch_dbm_file()> method which must be
167             implemented in a subclass such as L.
168              
169             =cut
170              
171             sub dependencies_dbm {
172 3     3 1 5 my $self = shift;
173 3         17 return( $self->_get_a_dbm('dependencies', PAR::Repository::Client::DEPENDENCIES_DBM_FILE()) );
174             }
175              
176              
177             =head2 close_modules_dbm
178              
179             Closes the C file and does all necessary
180             cleaning up.
181              
182             This is called when the object is destroyed.
183              
184             =cut
185              
186             sub close_modules_dbm {
187 11     11 1 65 my $self = shift;
188 11         44 my $hash = $self->{modules_dbm_hash};
189 11 100       73 return if not defined $hash;
190              
191 9         36 my $obj = tied($hash);
192 9         27 $self->{modules_dbm_hash} = undef;
193 9         26 undef $hash;
194 9         118 undef $obj;
195              
196 9         3137 unlink $self->{modules_dbm_temp_file};
197 9         32 $self->{modules_dbm_temp_file} = undef;
198 9 50       94 if ($self->{checksums}) {
199 9         60 delete $self->{checksums}{PAR::Repository::Client::MODULES_DBM_FILE().".zip"};
200             }
201              
202 9         39 return 1;
203             }
204              
205              
206             =head2 close_scripts_dbm
207              
208             Closes the C file and does all necessary
209             cleaning up.
210              
211             This is called when the object is destroyed.
212              
213             =cut
214              
215             sub close_scripts_dbm {
216 11     11 1 31 my $self = shift;
217 11         35 my $hash = $self->{scripts_dbm_hash};
218 11 100       63 return if not defined $hash;
219              
220 2         7 my $obj = tied($hash);
221 2         5 $self->{scripts_dbm_hash} = undef;
222 2         5 undef $hash;
223 2         11 undef $obj;
224              
225 2         2531 unlink $self->{scripts_dbm_temp_file};
226 2         12 $self->{scripts_dbm_temp_file} = undef;
227 2 50       16 if ($self->{checksums}) {
228 2         10 delete $self->{checksums}{PAR::Repository::Client::SCRIPTS_DBM_FILE().".zip"};
229             }
230              
231 2         8 return 1;
232             }
233              
234              
235             =head2 close_dependencies_dbm
236              
237             Closes the C file and does all necessary
238             cleaning up.
239              
240             This is called when the object is destroyed.
241              
242             =cut
243              
244             sub close_dependencies_dbm {
245 11     11 1 28 my $self = shift;
246 11         32 my $hash = $self->{dependencies_dbm_hash};
247 11 100       59 return if not defined $hash;
248              
249 2         5 my $obj = tied($hash);
250 2         5 $self->{dependencies_dbm_hash} = undef;
251 2         5 undef $hash;
252 2         10 undef $obj;
253              
254 2         241 unlink $self->{dependencies_dbm_temp_file};
255 2         8 $self->{dependencies_dbm_temp_file} = undef;
256 2 50       12 if ($self->{checksums}) {
257 2         7 delete $self->{checksums}{PAR::Repository::Client::DEPENDENCIES_DBM_FILE().".zip"};
258             }
259              
260 2         7 return 1;
261             }
262              
263              
264             =head1 PRIVATE METHODS
265              
266             These private methods should not be relied upon from the outside of
267             the module.
268              
269             =head2 _get_a_dbm
270              
271             This is a private method.
272              
273             Generic method returning a dbm.
274             Requires two arguments. The type of the DBM (C,
275             C, C), and the name of the remote
276             DBM file. The latter should be taken from one of the package
277             constants.
278              
279             =cut
280              
281             sub _get_a_dbm {
282 30     30   57 my $self = shift;
283 30         69 $self->{error} = undef;
284              
285 30         51 my $dbm_type = shift;
286 30         64 my $dbm_remotefile = shift;
287              
288 30         87 my $dbm_hashkey = $dbm_type . "_dbm_hash";
289 30         77 my $tempfile_hashkey = $dbm_type . "_dbm_temp_file";
290 30         63 my $dbm_remotefile_zip = $dbm_remotefile . ".zip";
291              
292 30         163 my $checksums = $self->need_dbm_update($dbm_remotefile);
293              
294 30 100       161 if ($self->{$dbm_hashkey}) {
295             # need new dbm file?
296 17 50       184 return($self->{$dbm_hashkey}, $self->{$tempfile_hashkey})
297             if not $checksums;
298              
299             # does this particular dbm need to be updated?
300 0 0       0 if ($self->{checksums}) {
301 0         0 my $local_checksum = $self->{checksums}{$dbm_remotefile_zip};
302 0         0 my $remote_checksum = $checksums->{$dbm_remotefile_zip};
303 0 0 0     0 return($self->{$dbm_hashkey}, $self->{$tempfile_hashkey})
      0        
304             if defined $local_checksum and defined $remote_checksum
305             and $local_checksum eq $remote_checksum;
306             }
307              
308             # just to make sure
309 0         0 my $method = 'close_' . $dbm_type . "_dbm";
310 0         0 $self->$method;
311             }
312              
313 13         27 my $file;
314 13 50       35 if ($checksums) {
315 13         157 $file = $self->_fetch_dbm_file($dbm_remotefile_zip);
316             # (error set by _fetch_dbm_file)
317 13 50       71 return() if not defined $file; # or not -f $file; # <--- _fetch_dbm_file should do the stat!
318             }
319             else {
320             # cached!
321 0         0 $file = File::Spec->catfile($self->{cache_dir}, $dbm_remotefile_zip);
322 0 0       0 $self->{error} = "Cache miss error: Expected $file to exist, but it doesn't" if not -f $file;
323             }
324              
325 13         386 my ($tempfh, $tempfile) = File::Temp::tempfile(
326             'temporary_dbm_XXXXX',
327             UNLINK => 0,
328             DIR => File::Spec->tmpdir(),
329             EXLOCK => 0, # FIXME no exclusive locking or else we block on BSD. What's the right solution?
330             );
331              
332 13 50       101079 if (not $self->_unzip_file($file, $tempfile, $dbm_remotefile)) {
333 0         0 $self->{error} = "Could not unzip dbm file '$file' to '$tempfile'";
334 0         0 unlink($tempfile);
335 0         0 return();
336             }
337              
338 13         54 $self->{$tempfile_hashkey} = $tempfile;
339              
340 13         31 my %hash;
341 13         180 my $obj = tie %hash, "DBM::Deep", {
342             file => $tempfile,
343             locking => 1,
344             autoflush => 0,
345             };
346              
347 13         333695 $self->{$dbm_hashkey} = \%hash;
348              
349             # save this dbm file checksum
350 13 50       84 if (ref($checksums)) {
351 13 100       76 if (not $self->{checksums}) {
352 9         30 $self->{checksums} = {};
353             }
354 13         68 $self->{checksums}{$dbm_remotefile_zip} = $checksums->{$dbm_remotefile_zip};
355             }
356              
357 13         336 return (\%hash, $tempfile);
358             }
359              
360              
361             =head2 _parse_dbm_checksums
362              
363             This is a private method.
364              
365             Given a reference to a file handle, a reference to a string
366             or a file name, this method parses a checksum file
367             and returns a hash reference associating file names
368             with their base64 encoded MD5 hashes.
369              
370             If passed a ref to a string, the contents of the string will
371             be assumed to contain the checksum data.
372              
373             =cut
374              
375             sub _parse_dbm_checksums {
376 45     45   102 my $self = shift;
377 45         104 $self->{error} = undef;
378              
379 45         76 my $file_or_fh = shift;
380 45         77 my $is_string = 0;
381 45         57 my $fh;
382 45 50       205 if (ref($file_or_fh) eq 'GLOB') {
    50          
383 0         0 $fh = $file_or_fh;
384             }
385             elsif (ref($file_or_fh) eq 'SCALAR') {
386 0         0 $is_string = 1;
387             }
388             else {
389 45 50       2301 open $fh, '<', $file_or_fh
390             or die "Could not open file '$file_or_fh' for reading: $!";
391             }
392              
393 45         100 my $hashes = {};
394 45         72 my @lines;
395 45 50       125 @lines = split /\n/, $$file_or_fh if $is_string;
396              
397 45         70 while (1) {
398 405 50       2308 local $_ = $is_string ? shift @lines : <$fh>;
399 405 100       945 last if not defined $_;
400 360 100 66     2239 next if /^\s*$/ or /^\s*#/;
401 180         581 my ($file, $hash) = split /\t/, $_;
402 180 50 33     922 if (not defined $file or not defined $hash) {
403 0         0 $self->{error} = "Error reading repository checksums.";
404 0         0 return();
405             }
406 180         791 $hash =~ s/\s+$//;
407 180         888 $hashes->{$file} = $hash;
408             }
409              
410 45         803 return $hashes;
411             }
412              
413              
414             =head2 _calculate_cache_local_checksums
415              
416             This is a private method.
417              
418             Calculates the checksums of the DBMs in the local cache directory.
419             If the repository client isn't using a private cache directory, this
420             B and does not actually try to calculate
421             any checksums of potentially modified files.
422              
423             Returns the checksums hash just like the checksum fetching
424             routine.
425              
426             Maintainer note: Essentially the same code lives in
427             PAR::Repository's DBM code for calculating the repository checksums
428             in the first place.
429              
430             =cut
431              
432             sub _calculate_cache_local_checksums {
433 9     9   22 my $self = shift;
434              
435             # only support inter-run cache summing if we're in a private cache dir!
436 9 50       41 if (!$self->{private_cache_dir}) {
437 9         38 return();
438             }
439              
440             # find a working base64 MD5 implementation
441 0           my $md5_function;
442 0           eval { require Digest::MD5; $md5_function = \&Digest::MD5::md5_base64; };
  0            
  0            
443 0 0         eval { require Digest::Perl::MD5; $md5_function = \&Digest::Perl::MD5::md5_base64; } if $@;
  0            
  0            
444 0 0         if ($@) {
445 0           return();
446             }
447            
448 0           my $hashes = {};
449             # calculate local hashes
450 0           foreach my $dbmfile (
451             PAR::Repository::Client::MODULES_DBM_FILE(),
452             PAR::Repository::Client::SCRIPTS_DBM_FILE(),
453             PAR::Repository::Client::SYMLINKS_DBM_FILE(),
454             PAR::Repository::Client::DEPENDENCIES_DBM_FILE(),
455             ) {
456 0           my $filepath = File::Spec->catfile($self->{cache_dir}, $dbmfile.'.zip');
457 0 0         next unless -f $filepath;
458 0 0         open my $fh, '<', $filepath
459             or die "Could not open DBM file '$filepath' for reading: $!";
460 0           local $/ = undef;
461 0           my $hash = $md5_function->(<$fh>);
462 0           close $fh;
463 0           $hashes->{$dbmfile.'.zip'} = $hash;
464             } # end foreach dbm files
465              
466 0           return $hashes;
467             }
468              
469             1;
470              
471             __END__