File Coverage

blib/lib/CPAN/Testers/Data/Uploads.pm
Criterion Covered Total %
statement 288 316 91.1
branch 86 124 69.3
condition 32 53 60.3
subroutine 40 40 100.0
pod 7 7 100.0
total 453 540 83.8


line stmt bran cond sub pod time code
1             package CPAN::Testers::Data::Uploads;
2              
3 13     13   349357 use strict;
  13         32  
  13         502  
4 13     13   73 use warnings;
  13         22  
  13         466  
5              
6 13     13   71 use vars qw($VERSION);
  13         79  
  13         1112  
7             $VERSION = '0.21';
8             $|++;
9              
10             #----------------------------------------------------------------------------
11             # Library Modules
12              
13 13     13   86 use base qw(Class::Accessor::Fast);
  13         21  
  13         26381  
14              
15 13     13   93816 use CPAN::DistnameInfo;
  13         22877  
  13         1563  
16 13     13   14804 use CPAN::Testers::Common::DBUtils;
  13         450963  
  13         139  
17 13     13   19424 use CPAN::Testers::Common::Article;
  13         306428  
  13         1409  
18 13     13   18879 use Config::IniFiles;
  13         655879  
  13         566  
19 13     13   162 use DBI;
  13         24  
  13         559  
20 13     13   197 use File::Basename;
  13         28  
  13         1370  
21 13     13   17563 use File::Find::Rule;
  13         151558  
  13         143  
22 13     13   802 use File::Path;
  13         26  
  13         945  
23 13     13   20398 use File::Slurp;
  13         257862  
  13         1287  
24 13     13   29005 use Getopt::Long;
  13         209731  
  13         89  
25 13     13   15495 use IO::AtomicFile;
  13         43334  
  13         794  
26 13     13   254 use IO::File;
  13         28  
  13         2175  
27 13     13   22193 use Net::NNTP;
  13         599718  
  13         1314  
28              
29             #----------------------------------------------------------------------------
30             # Variables
31              
32             my (%backups);
33 13     13   180 use constant LASTMAIL => '_lastmail';
  13         31  
  13         1081  
34 13     13   79 use constant LOGFILE => '_uploads.log';
  13         27  
  13         649  
35 13     13   71 use constant JOURNAL => '_journal.sql';
  13         27  
  13         62738  
36              
37             my %phrasebook = (
38             'FindAuthor' => 'SELECT * FROM ixlatest WHERE author=?',
39              
40             'FindDistVersion' => 'SELECT type FROM uploads WHERE author=? AND dist=? AND version=?',
41             'InsertDistVersion' => 'INSERT INTO uploads (type,author,dist,version,filename,released) VALUES (?,?,?,?,?,?)',
42             'UpdateDistVersion' => 'UPDATE uploads SET type=? WHERE author=? AND dist=? AND version=?',
43             'FindDistTypes' => 'SELECT * FROM uploads WHERE type=?',
44             'DeleteAll' => 'DELETE FROM uploads',
45             'SelectAll' => 'SELECT * FROM uploads',
46              
47             'DeleteAllIndex' => 'DELETE FROM ixlatest',
48             'DeleteIndex' => 'DELETE FROM ixlatest WHERE dist=? AND author=?',
49             'FindIndex' => 'SELECT * FROM ixlatest WHERE dist=? AND author=?',
50             'InsertIndex' => 'INSERT INTO ixlatest (oncpan,author,version,released,dist) VALUES (?,?,?,?,?)',
51             'AmendIndex' => 'UPDATE ixlatest SET oncpan=? WHERE author=? AND version=? AND dist=?',
52             'UpdateIndex' => 'UPDATE ixlatest SET oncpan=?,version=?,released=? WHERE dist=? AND author=?',
53             'BuildAuthorIndex' => 'SELECT x.author,x.version,x.released,x.dist,x.type FROM (SELECT dist, MAX(released) AS mv FROM uploads WHERE author=? GROUP BY dist) AS y INNER JOIN uploads AS x ON x.dist=y.dist AND x.released=y.mv ORDER BY released',
54             'GetAllAuthors' => 'SELECT distinct(author) FROM uploads',
55              
56             'InsertRequest' => 'INSERT INTO page_requests (type,name,weight) VALUES (?,?,5)',
57              
58             'ParseFailed' => 'REPLACE INTO uploads_failed (source,type,dist,version,file,pause,created) VALUES (?,?,?,?,?,?,?)',
59              
60             # SQLite backup
61             'CreateTable' => 'CREATE TABLE uploads (type text, author text, dist text, version text, filename text, released int)',
62             );
63              
64             my $extn = qr/\.(tar\.(gz|bz2)|tgz|zip)$/;
65              
66             my %oncpan = (
67             'backpan' => 2,
68             'cpan' => 1,
69             'upload' => 1
70             );
71              
72             #----------------------------------------------------------------------------
73             # The Application Programming Interface
74              
75             sub new {
76 14     14 1 18477 my $class = shift;
77              
78 14         36 my $self = {};
79 14         42 bless $self, $class;
80              
81 14         242 $self->_init_options(@_);
82 9         81 return $self;
83             }
84              
85             sub DESTROY {
86 14     14   41475 my $self = shift;
87             }
88              
89             __PACKAGE__->mk_accessors(
90             qw( uploads backpan cpan logfile logclean lastfile journal
91             mgenerate mupdate mbackup mreindex ));
92              
93             sub process {
94 5     5 1 1155838 my $self = shift;
95 5 100       42 $self->generate() if($self->mgenerate);
96 5 100       83 $self->reindex() if($self->mreindex);
97 5 100       239 $self->update() if($self->mupdate);
98 5 100       426 $self->backup() if($self->mbackup);
99             }
100              
101             sub generate {
102 1     1 1 15 my $self = shift;
103 1         6 my $db = $self->uploads;
104              
105 1         14 $self->_log("Restarting uploads database");
106 1         84 $db->do_query($phrasebook{'DeleteAll'});
107              
108 1         37212 $self->_log("Creating BACKPAN entries");
109 1         105 my @files = File::Find::Rule->file()->name($extn)->in($self->backpan);
110 1         9939 $self->_parse_archive('backpan',$_) for(@files);
111              
112 1         11 $self->_log("Creating CPAN entries");
113 1         94 @files = File::Find::Rule->file()->name($extn)->in($self->cpan);
114 1         7407 $self->_parse_archive('cpan',$_) for(@files);
115             }
116              
117             sub reindex {
118 3     3 1 48 my $self = shift;
119 3         18 my $db = $self->uploads;
120              
121 3         29 $self->_log("Reindexing by author");
122              
123 3         184 my $next = $db->iterator('hash',$phrasebook{'GetAllAuthors'});
124 3         838 while(my $author = $next->()) {
125 45         3530384 $self->_log(".. author = $author->{author}");
126 45         14364 my @rows = $db->get_query('hash',$phrasebook{'BuildAuthorIndex'},$author->{author});
127 45         23934 for my $row (@rows) {
128 51         246765 $self->_log(".... dist = $row->{dist}, latest = $row->{version}");
129 51         3098 $db->do_query($phrasebook{'DeleteIndex'},$row->{dist},$row->{author});
130 51         4441745 $db->do_query($phrasebook{'InsertIndex'},$oncpan{$row->{type}},$row->{author},$row->{version},$row->{released},$row->{dist});
131             }
132             }
133              
134 3         168907 $self->_log("Reindexing authors done");
135             }
136              
137             sub update {
138 1     1 1 16 my $self = shift;
139 1         5 my $db = $self->uploads;
140              
141 1         10 $self->_open_journal();
142              
143             # get list of db known CPAN distributions
144 1         277 my @rows = $db->get_query('hash',$phrasebook{'FindDistTypes'},'cpan');
145 1         1502 my %cpan = map {$_->{filename} => $_} @rows;
  63         172  
146              
147             # get currently mirrored CPAN entries
148 1         11 $self->_log("Updating CPAN entries");
149 1         108 my @files = File::Find::Rule->file()->name($extn)->in($self->cpan);
150 1         7219 for(@files) {
151 63 50       147 if(my $file = $self->_parse_archive('cpan',$_,1)) {
152 63         195 delete $cpan{$file};
153             } else {
154             #$self->_log(".. cannot parse: $_");
155             }
156             }
157              
158             # demote any distributions no longer on CPAN
159 1         7 $self->_log("Updating BACKPAN entries");
160 1         65 for my $file (keys %cpan) {
161             #$self->_log("backpan => $cpan{$file}->{dist} => $cpan{$file}->{version} => $cpan{$file}->{author} => $cpan{$file}->{released}");
162 0         0 $self->_write_journal('UpdateDistVersion','backpan',$cpan{$file}->{author},$cpan{$file}->{dist},$cpan{$file}->{version});
163 0         0 $db->do_query($phrasebook{'AmendIndex'},2,$cpan{$file}->{author},$cpan{$file}->{version},$cpan{$file}->{dist});
164             }
165              
166             # read NNTP
167 1         4 $self->_log("Updating NNTP entries");
168 1         36 my ($nntp,$num,$first,$last) = $self->_nntp_connect();
169 1         4 my $lastid = $self->_lastid();
170 1 50       5 return if($last <= $lastid);
171              
172 1         6 $self->_log(".. from $lastid to $last");
173 1         30 for(my $id = $lastid+1; $id <= $last; $id++) {
174             #$self->_log("NNTP ID = $id");
175 72870 50       256457 my $article = join "", @{$nntp->article($id) || []};
  72870         366559  
176 72870 100       4414645 next unless($article);
177 4         57 my $object = CPAN::Testers::Common::Article->new($article);
178 4 50       9021 next unless($object);
179 4         28 $self->_log("... [$id] subject=".($object->subject()));
180              
181 4         189 my ($name,$version,$cpanid,$date,$filename);
182 4 50       21 if($object->parse_upload()) {
183 4         771 $name = $object->distribution;
184 4         26 $version = $object->version;
185 4         23 $cpanid = $object->author;
186 4         26 $date = $object->epoch;
187 4         22 $filename = $object->filename;
188             }
189              
190             #$self->_log("... name=$name");
191             #$self->_log("... version=$version");
192             #$self->_log("... cpanid=$cpanid");
193             #$self->_log("... date=$date");
194              
195 4 50 33     65 next unless($name && $version && $cpanid && $date);
      33        
      33        
196             #$self->_log("upload => $name => $version => $cpanid => $date");
197              
198 4         21 $self->_update_index($cpanid,$version,$date,$name,1);
199 4         168173 my @rows = $db->get_query('array',$phrasebook{'FindDistVersion'},$cpanid,$name,$version);
200 4 100       1857 next if(@rows);
201 3         33 $self->_write_journal('InsertDistVersion','upload',$cpanid,$name,$version,$filename,$date);
202             }
203              
204 1         11 $self->_lastid($last);
205 1         6 $self->_close_journal();
206             }
207              
208             sub backup {
209 1     1 1 9 my $self = shift;
210 1         5 my $db = $self->uploads;
211              
212 1 50       10 if(my @journals = $self->_find_journals()) {
213 1         5 for my $driver (keys %backups) {
214 1 50 33     20 if($driver =~ /(CSV|SQLite)/i && !$backups{$driver}{'exists'}) {
215 1         9 $backups{$driver}{db}->do_query($phrasebook{'CreateTable'});
216 1         85826 $backups{$driver}{'exists'} = 1;
217             }
218             }
219            
220 1         6 for my $journal (@journals) {
221 1 50       13 next if($journal =~ /TMP$/); # don't process active journals
222 1         12 $self->_log("Processing journal $journal");
223 1         68 my $lines = $self->_read_journal($journal);
224 1         5 for my $line (@$lines) {
225 3         55011 my ($phrase,@args) = @$line;
226 3         14 for my $driver (keys %backups) {
227 3         29 $backups{$driver}{db}->do_query($phrasebook{$phrase},@args);
228             }
229             }
230              
231 1         18056 $self->_done_journal($journal);
232             }
233 1         58 $self->_log("Processed journals");
234             } else {
235 0         0 for my $driver (keys %backups) {
236 0 0       0 if($backups{$driver}{'exists'}) {
    0          
237 0         0 $backups{$driver}{db}->do_query($phrasebook{'DeleteAll'});
238             } elsif($driver =~ /(CSV|SQLite)/i) {
239 0         0 $backups{$driver}{db}->do_query($phrasebook{'CreateTable'});
240 0         0 $backups{$driver}{'exists'} = 1;
241             }
242             }
243              
244 0         0 $self->_log("Backup via DBD drivers");
245              
246 0         0 my $rows = $db->iterator('array',$phrasebook{'SelectAll'});
247 0         0 while(my $row = $rows->()) {
248 0         0 for my $driver (keys %backups) {
249 0         0 $backups{$driver}{db}->do_query($phrasebook{'InsertDistVersion'},@$row);
250             }
251             }
252             }
253              
254             # handle the CSV exception
255 1 50       123 if($backups{CSV}) {
256 0         0 $self->_log("Backup to CSV file");
257 0         0 $backups{CSV}{db} = undef; # close db handle
258 0 0       0 my $fh1 = IO::File->new('uploads','r') or die "Cannot read temporary database file 'uploads'\n";
259 0 0       0 my $fh2 = IO::File->new($backups{CSV}{dbfile},'w+') or die "Cannot write to CSV database file $backups{CSV}{dbfile}\n";
260 0         0 while(<$fh1>) { print $fh2 $_ }
  0         0  
261 0         0 $fh1->close;
262 0         0 $fh2->close;
263 0         0 unlink('uploads');
264             }
265             }
266              
267             sub help {
268 5     5 1 13 my ($self,$full,$mess) = @_;
269              
270 5 100       535 print "\n$mess\n\n" if($mess);
271              
272 5 100       15 if($full) {
273 4         116 print <
274              
275             Usage: $0 --config= [-g] [-r] [-u] [-b] [-h] [-v]
276             [--logfile=] [--logclean]
277             [--lastmail=] [--journal=]
278              
279             --config= database configuration file
280             -g generate new database
281             -r reindex database (*)
282             -u update existing database
283             -b backup database to portable files
284             -h this help screen
285             -v program version
286             --logfile= trace log file
287             --logclean overwrite exisiting log file
288             --lastmail= last id file
289             --journal= SQL journal file path
290              
291             Notes:
292             * A generate request automatically includes a reindex.
293              
294             HERE
295              
296             }
297              
298 5         98 print "$0 v$VERSION\n\n";
299 5         21 exit(0);
300             }
301              
302             #----------------------------------------------------------------------------
303             # Private Methods
304              
305             sub _parse_archive {
306 189     189   7118 my ($self,$type,$file,$update) = @_;
307 189         1328 my $db = $self->uploads;
308 189         2612 my $dist = CPAN::DistnameInfo->new($file);
309              
310 189         28896 my $name = $dist->dist; # "CPAN-DistnameInfo"
311 189         1600 my $version = $dist->version; # "0.02"
312 189         1338 my $cpanid = $dist->cpanid; # "GBARR"
313 189         1565 my $filename = $dist->filename; # "CPAN-DistnameInfo-0.02.tar.gz"
314 189         13560 my $date = (stat($file))[9];
315              
316 189 50 33     4067 unless($name && defined $version && $cpanid && $date) {
      33        
      33        
317             #$self->_log("PARSE: FAIL file=$file, $type => $name => $version => $cpanid => $date => $filename");
318 0         0 $file =~ s!/opt/projects/CPAN/!!;
319 0         0 $db->do_query($phrasebook{'ParseFailed'},$file,$type,$name,$version,$filename,$cpanid,$date);
320 0         0 return;
321             }
322             #$self->_log("$type => $name => $version => $cpanid => $date");
323              
324 189         1678 my @rows = $db->get_query('array',$phrasebook{'FindDistVersion'},$cpanid,$name,$version);
325 189 100       99087 if(@rows) {
326 126 100       949 if($type ne $rows[0]->[0]) {
327 63         399 $db->do_query($phrasebook{'UpdateDistVersion'},$type,$cpanid,$name,$version);
328 63 50 33     2362521 $self->_update_index($cpanid,$version,$date,$name,$oncpan{$type})
329             if($update && $type ne 'backpan');
330             }
331             } else {
332 63         609 $db->do_query($phrasebook{'InsertDistVersion'},$type,$cpanid,$name,$version,$filename,$date);
333 63 50       1478064 $self->_update_index($cpanid,$version,$date,$name,$oncpan{$type}) if($update);
334             }
335              
336 189         3073 return $filename;
337             }
338              
339             sub _update_index {
340 4     4   10 my ($self,$author,$version,$date,$name,$oncpan) = @_;
341 4         17 my $db = $self->uploads;
342              
343 4         43 my @index = $db->get_query('hash',$phrasebook{'FindIndex'},$name,$author);
344 4 100       1529 if(@index) {
345 3 100       15 if($date > $index[0]->{released}) {
346 2         12 $db->do_query($phrasebook{'UpdateIndex'},$oncpan,$version,$date,$name,$author);
347 2         256638 $self->_log("... index update [$author,$version,$date,$name,$oncpan]");
348             }
349             } else {
350 1         15 $db->do_query($phrasebook{'InsertIndex'},$oncpan,$author,$version,$date,$name);
351 1         63576 $self->_log("... index insert [$author,$version,$date,$name,$oncpan]");
352             }
353              
354             # add to page_requests table to update letter index pages and individual pages
355 4         164 $db->do_query($phrasebook{'InsertRequest'},'ixauth',substr($author,0,1));
356 4         329138 $db->do_query($phrasebook{'InsertRequest'},'ixdist',substr($name,0,1));
357 4         276010 $db->do_query($phrasebook{'InsertRequest'},'author',$author);
358 4         170305 $db->do_query($phrasebook{'InsertRequest'},'distro',$name);
359             }
360              
361             sub _nntp_connect {
362             # connect to NNTP server
363 1 50   1   13 my $nntp = Net::NNTP->new("nntp.perl.org") or die "Cannot connect to nntp.perl.org";
364 1         30 my ($num,$first,$last) = $nntp->group("perl.cpan.uploads");
365              
366 1         89 return ($nntp,$num,$first,$last);
367             }
368              
369             sub _lastid {
370 6     6   1729 my ($self,$id) = @_;
371 6         32 my $f = $self->lastfile;
372              
373 6 100       171 unless( -f $f) {
374 2         244 mkpath(dirname($f));
375 2         18 overwrite_file( $f, 0 );
376 2   50     612 $id ||= 0;
377             }
378              
379 6 100       21 if($id) { overwrite_file( $f, $id ); }
  2         32  
380 4         20 else { $id = read_file($f); }
381              
382 6         878 return $id;
383             }
384              
385             # generate atomic journal file name
386             sub _open_journal {
387 1     1   3 my $self = shift;
388 1         43 my @now = localtime(time);
389 1         6 my $file = sprintf "%s.%04d%02d%02d%02d%02d%02d", $self->journal, $now[5]+1900,$now[4]+1,$now[3],$now[2],$now[1],$now[0];
390 1 50       23 $self->{current} = IO::AtomicFile->new($file,'w+') or die "Cannot write to journal file [$file]: $!\n";
391             }
392              
393             sub _write_journal {
394 3     3   59 my ($self,$phrase,@args) = @_;
395 3         15 my $fh = $self->{current};
396              
397 3         148 print $fh "$phrase," . join(',',@args) . "\n";
398              
399 3         24 my $db = $self->uploads;
400 3         51 $db->do_query($phrasebook{$phrase},@args);
401             }
402              
403             sub _close_journal {
404 1     1   2 my $self = shift;
405 1         9 $self->{current}->close;
406             }
407              
408             sub _find_journals {
409 1     1   2 my $self = shift;
410 1         12 my @files = glob($self->journal . '.*');
411 1         145 return @files;
412             }
413              
414             sub _read_journal {
415 1     1   3 my ($self,$journal) = @_;
416 1         183 my @lines;
417              
418 1 50       9 my $fh = IO::File->new($journal,'r') or die "Cannot read journal file [$journal]: $!\n";
419 1         762 while(<$fh>) {
420 3         22 my @fields = split(/,/);
421 3         18 push @lines, \@fields;
422             }
423 1         5 $fh->close;
424 1         23 return \@lines;
425             }
426              
427             sub _done_journal {
428 1     1   5 my ($self,$journal) = @_;
429 1         5 my $cmd = "mv $journal logs";
430 1         17720 system($cmd);
431             }
432              
433             sub _init_options {
434 14     14   31 my $self = shift;
435 14         574 my %hash = @_;
436 14         188 my %options;
437              
438 14         307 GetOptions( \%options,
439             'config=s',
440             'generate|g',
441             'update|u',
442             'reindex|r',
443             'backup|b',
444             'journal|j=s',
445             'logfile|l=s',
446             'logclean=s',
447             'lastfile=s',
448             'help|h',
449             'version|v'
450             );
451              
452             # default to API settings if no command line option
453 14         10817 for(qw(config generate update reindex fast backup help version)) {
454 112 100 33     447 $options{$_} ||= $hash{$_} if(defined $hash{$_});
455             }
456              
457 14 100       79 $self->help(1) if($options{help});
458 13 100       52 $self->help(0) if($options{version});
459              
460 12 100 100     110 $self->help(1,"Must specify at least one option from 'generate' (-g), 'reindex' (-r),\n'update' (-u) and/or 'backup' (-b)")
      100        
      100        
461             unless($options{generate} || $options{update} || $options{backup} || $options{reindex});
462 11 100       42 $self->help(1,"Must specific the configuration file") unless( $options{config});
463 10 100       280 $self->help(1,"Configuration file [$options{config}] not found") unless(-f $options{config});
464              
465             # load configuration
466 9         131 my $cfg = Config::IniFiles->new( -file => $options{config} );
467              
468             # configure sources
469 9 100       35662 if($options{generate}) {
470 1         8 my $dir = $cfg->val('MASTER','BACKPAN');
471 1 50       27 $self->help(1,"No source location for 'BACKPAN' in config file") if(! $dir);
472 1 50       43 $self->help(1,"Cannot find source location for 'BACKPAN': [$dir]") if(!-d $dir);
473 1         7 $self->backpan($dir);
474 1         28 $self->mgenerate(1);
475 1         9 $self->mreindex(1);
476             }
477 9 100 100     91 if($options{generate} || $options{update}) {
478 7         34 my $dir = $cfg->val('MASTER','CPAN');
479 7 50       165 $self->help(1,"No source location for 'CPAN' in config file") if(! $dir);
480 7 50       195 $self->help(1,"Cannot find source location for 'CPAN': [$dir]") if(!-d $dir);
481 7         40 $self->cpan($dir);
482             }
483 9 100       128 if($options{reindex}) {
484 1         7 $self->mreindex(1);
485             }
486              
487 9 100       76 $self->mupdate(1) if($options{update});
488 9   50     131 $self->logfile( $hash{logfile} || $options{logfile} || $cfg->val('MASTER','logfile' ) || LOGFILE );
489 9   100     389 $self->logclean( $hash{logclean} || $options{logclean} || $cfg->val('MASTER','logclean' ) || 0 );
490 9   50     344 $self->lastfile( $hash{lastfile} || $options{lastfile} || $cfg->val('MASTER','lastfile' ) || LASTMAIL );
491 9   100     314 $self->journal( $hash{journal} || $options{journal} || $cfg->val('MASTER','journal' ) || JOURNAL );
492              
493             # configure upload DB
494 9 50       278 $self->help(1,"No configuration for UPLOADS database") unless($cfg->SectionExists('UPLOADS'));
495 9   100     211 my %opts = map {$_ => ($cfg->val('UPLOADS',$_) || undef)} qw(driver database dbfile dbhost dbport dbuser dbpass);
  63         1246  
496 9         342 my $db = CPAN::Testers::Common::DBUtils->new(%opts);
497 9 50       308 $self->help(1,"Cannot configure UPLOADS database") unless($db);
498 9         132 $self->uploads($db);
499              
500             # configure backup DBs
501 9 100       250 if($options{backup}) {
502 1 50       5 $self->help(1,"No configuration for BACKUPS with backup option") unless($cfg->SectionExists('BACKUPS'));
503              
504 1         34 my %available_drivers = map { $_ => 1 } DBI->available_drivers;
  7         635  
505 1         7 my @drivers = $cfg->val('BACKUPS','drivers');
506 1         28 for my $driver (@drivers) {
507 2 100       8 unless($available_drivers{$driver}) {
508 1         169 warn "No DBI support for '$driver', ignoring\n";
509 1         6 next;
510             }
511              
512 1 50       5 $self->help(1,"No configuration for backup option '$driver'") unless($cfg->SectionExists($driver));
513              
514 1   100     27 my %opt = map {$_ => ($cfg->val($driver,$_) || undef)} qw(driver database dbfile dbhost dbport dbuser dbpass);
  7         146  
515 1 50       68 $backups{$driver}{'exists'} = $driver =~ /SQLite/i ? -f $opt{database} : 1;
516              
517             # CSV is a bit of an oddity!
518 1 50       6 if($driver =~ /CSV/i) {
519 0         0 $backups{$driver}{'exists'} = 0;
520 0         0 $backups{$driver}{'dbfile'} = $opt{dbfile};
521 0         0 $opt{dbfile} = 'uploads';
522 0         0 unlink($opt{dbfile});
523             }
524              
525 1         11 $backups{$driver}{db} = CPAN::Testers::Common::DBUtils->new(%opt);
526 1 50       26 $self->help(1,"Cannot configure BACKUPS database for '$driver'") unless($backups{$driver}{db});
527             }
528              
529 1 50       12 $self->mbackup(1) if(keys %backups);
530             }
531             }
532              
533             sub _log {
534 122     122   4355 my $self = shift;
535 122 50       660 my $log = $self->logfile or return;
536 122 100       4499 mkpath(dirname($log)) unless(-f $log);
537              
538 122 100       652 my $mode = $self->logclean ? 'w+' : 'a+';
539 122         1341 $self->logclean(0);
540              
541 122         4491 my @dt = localtime(time);
542 122         1325 my $dt = sprintf "%04d/%02d/%02d %02d:%02d:%02d", $dt[5]+1900,$dt[4]+1,$dt[3],$dt[2],$dt[1],$dt[0];
543              
544 122 50       1517 my $fh = IO::File->new($log,$mode) or die "Cannot write to log file [$log]: $!\n";
545 122         20222 print $fh "$dt ", @_, "\n";
546 122         721 $fh->close;
547             }
548              
549             q!Will code for a damn fine Balti!;
550              
551             __END__