File Coverage

blib/lib/App/MBUtiny/Collector/DBI.pm
Criterion Covered Total %
statement 102 173 58.9
branch 19 62 30.6
condition 12 51 23.5
subroutine 23 29 79.3
pod 9 10 90.0
total 165 325 50.7


line stmt bran cond sub pod time code
1             package App::MBUtiny::Collector::DBI; # $Id: DBI.pm 128 2019-07-06 15:27:48Z abalama $
2 4     4   109693 use strict;
  4         22  
  4         113  
3 4     4   949 use utf8;
  4         40  
  4         18  
4              
5             =encoding utf8
6              
7             =head1 NAME
8              
9             App::MBUtiny::Collector::DBI - Collector database interface
10              
11             =head1 VERSION
12              
13             Version 1.02
14              
15             =head1 SYNOPSIS
16              
17             use App::MBUtiny::Collector::DBI;
18              
19             my $dbi = new App::MBUtiny::Collector::DBI(
20             dsn => "DBI:mysql:database=mbutiny;host=mysql.example.com",
21             user => "username",
22             password => "password",
23             set => [
24             "RaiseError 0",
25             "PrintError 0",
26             "mysql_enable_utf8 1",
27             ],
28             );
29             print STDERR $dbi->error if $dbi->error;
30              
31             =head1 DESCRIPTION
32              
33             Collector database interface
34              
35             =head2 new
36              
37             my $dbi = new App::MBUtiny::Collector::DBI(
38             dsn => "DBI:mysql:database=mbutiny;host=mysql.example.com",
39             user => "username",
40             password => "password",
41             set => [
42             "RaiseError 0",
43             "PrintError 0",
44             "mysql_enable_utf8 1",
45             ],
46             );
47              
48             Creates DBI object
49              
50             =head2 add
51              
52             $dbi->add(
53             type => 0,
54             name => "foo",
55             addr => "127.0.0.1",
56             status => 0,
57             file => "foo-2019-06-25.tar.gz",
58             size => 123456,
59             md5 => "...",
60             sha1 => "...",
61             error => "...",
62             comment => "...",
63             ) or die $dbi->error;
64              
65             Add new record on collector database
66              
67             =head2 del
68              
69             $dbi->del(
70             type => 0,
71             name => "foo",
72             addr => "127.0.0.1",
73             file => "foo-2019-06-25.tar.gz",
74             ) or die $dbi->error;
75              
76             Delete record from collector database
77              
78             =head2 dsn
79              
80             my $dsn = $dbi->dsn;
81              
82             Returns DSN string of current collector database connection
83              
84             =head2 error
85              
86             my $error = $dbi->error;
87             $dbi->error("Error message");
88              
89             Gets/sets error string
90              
91             =head2 get
92              
93             my %info = $dbi->get(
94             name => "foo",
95             file => "foo-2019-06-25.tar.gz",
96             );
97              
98             Gets information about file from collector database
99              
100             Format:
101              
102             {
103             id => 1,
104             type => 0,
105             time => 123456789,
106             name => "foo"
107             addr => "127.0.0.1",
108             status => 1,
109             file => "foo-2019-06-25.tar.gz",
110             size => 123456,
111             md5 => "...",
112             sha1 => "...",
113             error => "...",
114             comment => "...",
115             }
116              
117             =over 4
118              
119             =item B
120              
121             Client ip addr (IPv4/IPv6)
122              
123             =item B
124              
125             Comment data
126              
127             =item B
128              
129             Error message
130              
131             =item B
132              
133             Backup filename
134              
135             =item B
136              
137             Record ID. Autoincremented value!
138              
139             =item B
140              
141             MD5-checksum of backup file
142              
143             =item B
144              
145             Name of mbutiny host
146              
147             =item B
148              
149             SHA1-checksum of backup file
150              
151             =item B
152              
153             Size of backup file
154              
155             =item B
156              
157             Backup status: 0=false, 1=true
158              
159             Default: 0
160              
161             =item B
162              
163             Time of record insert
164              
165             Default: time()
166              
167             =item B
168              
169             Type of collector: 0=internal, 1=external
170              
171             Default: 0
172              
173             =back
174              
175             =head2 is_sqlite
176              
177             print $dbi->is_sqlite ? "Is SQLite" : "Is not SQLite"
178              
179             Returns true if type of current collector database is SQLite
180              
181             =head2 list
182              
183             my @files = $dbi->list(
184             name => "foo"
185             );
186              
187             Returns list of files by specified the name
188              
189             Record format of return result: see L
190              
191             =head2 report
192              
193             my @files = $dbi->report(
194             start => 123456789
195             );
196              
197             Returns list of all last backup files, starting at the specified the "start" value
198              
199             Record format of return result: see L
200              
201             =head1 SEE ALSO
202              
203             L, L
204              
205             =head1 AUTHOR
206              
207             Serż Minus (Sergey Lepenkov) L Eabalama@cpan.orgE
208              
209             =head1 COPYRIGHT
210              
211             Copyright (C) 1998-2019 D&D Corporation. All Rights Reserved
212              
213             =head1 LICENSE
214              
215             This program is free software; you can redistribute it and/or
216             modify it under the same terms as Perl itself.
217              
218             See C file and L
219              
220             =cut
221              
222 4     4   199 use vars qw/$VERSION @EXPORT_OK/;
  4         8  
  4         238  
223             $VERSION = '1.02';
224              
225 4     4   23 use Carp;
  4         7  
  4         185  
226 4     4   1519 use CTK::DBI;
  4         497724  
  4         164  
227 4     4   34 use CTK::Util qw/touch sharedstatedir/;
  4         8  
  4         197  
228 4     4   891 use CTK::ConfGenUtil;
  4         1791  
  4         305  
229 4     4   819 use CTK::TFVals qw/ :ALL /;
  4         3417  
  4         827  
230 4     4   30 use File::Spec;
  4         8  
  4         213  
231              
232             use constant {
233 4         411 PREFIX => 'mbutiny',
234             COLLECTOR_DB_FILENAME => 'mbutiny.db',
235             DEFAULT_ADDR => '127.0.0.1',
236             REPORT_PERIOD => 24*60*60 + 1, # Yesterday + 1sec
237             DEFAULT_DSN_MASK => 'dbi:SQLite:dbname=%s',
238             DEFAULT_DBI_ATTR => {
239             dsn => '', # See DEFAULT_DSN_MASK
240             user => '',
241             password => '',
242             set => [
243             'RaiseError 0',
244             'PrintError 0',
245             'sqlite_unicode 1',
246             ],
247             },
248 4     4   19 };
  4         9  
249              
250 4     4   25 use constant COLLECTOR_DDL => <<'DDL';
  4         6  
  4         200  
251             CREATE TABLE IF NOT EXISTS mbutiny (
252             `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
253             `type` INTEGER DEFAULT 0, -- 0=internal/1=external
254             `time` NUMERIC DEFAULT 0, -- time()
255             `name` CHAR(255) DEFAULT NULL, -- name of mbutiny host
256             `addr` CHAR(45) DEFAULT NULL, -- client ip addr
257             `status` INTEGER DEFAULT 0, -- backup status
258             `file` CHAR(255) DEFAULT NULL, -- backup filename
259             `size` INTEGER DEFAULT 0, -- size of backup file
260             `md5` CHAR(32) DEFAULT NULL, -- md5-checksum of backup file
261             `sha1` CHAR(40) DEFAULT NULL, -- sha1-checksum of backup file
262             `error` TEXT DEFAULT NULL, -- error message
263             `comment` TEXT DEFAULT NULL -- comment
264             )
265             DDL
266              
267 4     4   25 use constant COLLECTOR_INSERT => <<'DML';
  4         10  
  4         162  
268             INSERT INTO mbutiny
269             (`type`, `time`, `name`, `addr`, `status`, `file`, `size`, `md5`, `sha1`, `error`, `comment`)
270             VALUES
271             (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
272             DML
273              
274 4     4   29 use constant COLLECTOR_DELETE => <<'DML';
  4         16  
  4         229  
275             DELETE FROM mbutiny WHERE `type` = ? AND `name` = ? AND `file` = ? AND `addr` = ?
276             DML
277              
278 4     4   26 use constant COLLECTOR_SELECT => <<'DML';
  4         10  
  4         186  
279             SELECT `id`, `type`, `time`, `name`, `addr`, `status`, `file`, `size`, `md5`, `sha1`, `error`, `comment`
280             FROM mbutiny
281             WHERE `name` = ?
282             DML
283              
284 4     4   22 use constant COLLECTOR_SELECT_FILE => <<'DML';
  4         8  
  4         203  
285             SELECT `id`, `type`, `time`, `name`, `addr`, `status`, `file`, `size`, `md5`, `sha1`, `error`, `comment`
286             FROM mbutiny
287             WHERE `name` = ? AND `file` = ?
288             ORDER BY `time` DESC
289             LIMIT 1
290             DML
291              
292 4     4   22 use constant COLLECTOR_SELECT_LASTFILE => <<'DML';
  4         14  
  4         189  
293             SELECT `id`, `type`, `time`, `name`, `addr`, `status`, `file`, `size`, `md5`, `sha1`, `error`, `comment`
294             FROM mbutiny
295             WHERE `name` = ?
296             ORDER BY `time` DESC
297             LIMIT 1
298             DML
299              
300 4     4   21 use constant COLLECTOR_REPORT => <<'DML';
  4         7  
  4         170  
301             SELECT `id`, `type`, `time`, `name`, `addr`, `status`, `file`, `size`, `md5`, `sha1`, `error`, `comment`
302             FROM mbutiny
303             WHERE `time` > ?
304             DML
305              
306 4     4   24 use base qw/Exporter/;
  4         7  
  4         5701  
307             @EXPORT_OK = qw/
308             COLLECTOR_DB_FILENAME
309             COLLECTOR_DB_FILE
310             /;
311              
312 0     0 0 0 sub COLLECTOR_DB_FILE { File::Spec->catfile(sharedstatedir(), PREFIX, COLLECTOR_DB_FILENAME) }
313              
314             sub new {
315 2     2 1 168 my $class = shift;
316 2         8 my %args = @_;
317 2 50       21 unless ($args{dsn}) {
318 2         5 my $dda = DEFAULT_DBI_ATTR;
319 2         11 foreach (%$dda) {
320 16   66     51 $args{$_} //= $dda->{$_}
321             }
322             }
323 2   33     8 my $file = $args{file} || COLLECTOR_DB_FILE();
324 2   33     15 my $dsn = $args{dsn} || sprintf(DEFAULT_DSN_MASK, $file);
325              
326             # DB
327             my $db = new CTK::DBI(
328             -dsn => $dsn,
329             -debug => 0,
330             -username => $args{'user'},
331             -password => $args{'password'},
332             -attr => _attr($args{'set'}),
333             $args{timeout} ? (
334             -timeout_connect => $args{timeout},
335             -timeout_request => $args{timeout},
336             ) : (),
337 2 50       10 $args{user} ? () : (),
    50          
338             );
339 2 50       20394 my $dbh = $db->connect if $db;
340              
341             # SQLite
342 2         13 my $fnew = 0;
343 2         4 my $issqlite = 0;
344 2 50 33     20 if ($dbh && $dsn =~ /SQLite/i) {
345 2         15 $file = $dbh->sqlite_db_filename();
346 2 100 33     63 unless ($file && (-e $file) && !(-z $file)) {
      66        
347 1         8 touch($file);
348 1         92 chmod(0666, $file);
349 1         4 $fnew = 1;
350             }
351 2         5 $issqlite = 1;
352             }
353              
354 2         10 my $error = "";
355 2 50       14 if (!$db) {
    50          
    100          
356 0         0 $error = sprintf("Can't init database \"%s\"", $dsn);
357             } elsif (!$dbh) {
358 0   0     0 $error = sprintf("Can't connect to database \"%s\": %s", $dsn, $DBI::errstr || "unknown error");
359             } elsif ($fnew) {
360 1         6 $db->execute(COLLECTOR_DDL);
361 1         13119 $error = $dbh->errstr();
362             }
363 2 50       13 unless ($error) {
364 2 50 0     16 $error = sprintf("Can't init database \"%s\". Ping failed: %s",
365             $dsn, $dbh->errstr() || "unknown error") unless $dbh->ping;
366             }
367              
368 2         87 my $self = bless {
369             file => $file,
370             issqlite=> $issqlite,
371             dsn => $dsn,
372             error => $error,
373             db => $db,
374             }, $class;
375              
376 2         13 return $self;
377             }
378             sub error {
379 3     3 1 15 my $self = shift;
380 3         3 my $err = shift;
381 3 100       26 return $self->{error} unless defined $err;
382 1         2 $self->{error} = $err;
383 1         2 return $self->{error};
384             }
385             sub dsn {
386 1     1 1 2 my $self = shift;
387 1         7 return $self->{dsn};
388             }
389             sub is_sqlite {
390 0     0 1 0 my $self = shift;
391 0 0       0 return $self->{issqlite} ? 1 : 0;
392             }
393             sub add {
394 1     1 1 2 my $self = shift;
395 1         5 my %params = @_;
396 1         3 my $db = $self->{db};
397 1 50       2 unless ($db) {
398 0 0       0 $self->error(sprintf("Database \"%s\" connect failed", $self->dsn))
399             unless $self->error;
400 0         0 return 0;
401             }
402 1         10 $self->error("");
403             $db->execute(COLLECTOR_INSERT,
404             $params{type} || 0,
405             time(),
406             $params{name},
407             $params{addr} || DEFAULT_ADDR,
408             $params{status} || 0,
409             $params{file},
410             $params{size} || 0,
411             $params{md5},
412             $params{sha1},
413             $params{error},
414             $params{comment},
415 1   50     15 )->finish;
      50        
      50        
      50        
416 1 50       13004 if (my $dberr = $db->connect->errstr()) {
417 0   0     0 $self->error($dberr || $DBI::errstr || "unknown error");
418 0         0 return 0;
419             }
420 1         25 return 1;
421             }
422             sub del {
423 0     0 1 0 my $self = shift;
424 0         0 my %params = @_;
425 0         0 my $db = $self->{db};
426 0 0       0 unless ($db) {
427 0 0       0 $self->error(sprintf("Database \"%s\" connect failed", $self->dsn))
428             unless $self->error;
429 0         0 return 0;
430             }
431 0         0 $self->error("");
432             $db->execute(COLLECTOR_DELETE,
433             $params{type} || 0,
434             $params{name}, $params{file},
435 0   0     0 $params{addr} || DEFAULT_ADDR,
      0        
436             )->finish;
437              
438 0 0       0 if (my $dberr = $db->connect->errstr()) {
439 0   0     0 $self->error($dberr || $DBI::errstr || "unknown error");
440 0         0 return 0;
441             }
442 0         0 return 1;
443             }
444             sub get {
445 0     0 1 0 my $self = shift;
446 0         0 my %params = @_;
447 0         0 my $db = $self->{db};
448 0 0       0 unless ($db) {
449 0 0       0 $self->error(sprintf("Database \"%s\" connect failed", $self->dsn))
450             unless $self->error;
451 0         0 return;
452             }
453 0         0 $self->error("");
454 0   0     0 my $name = $params{name} || "";
455 0   0     0 my $file = $params{file} || "";
456 0         0 my %info = ();
457 0 0       0 if ($file) {
458 0         0 %info = $db->recordh(COLLECTOR_SELECT_FILE, $name, $file);
459             } else {
460 0         0 %info = $db->recordh(COLLECTOR_SELECT_LASTFILE, $name);
461             }
462 0 0       0 if (my $dberr = $db->connect->errstr()) {
463 0   0     0 $self->error($dberr || $DBI::errstr || "unknown error");
464 0         0 return ();
465             }
466 0         0 return %info;
467             }
468             sub list {
469 0     0 1 0 my $self = shift;
470 0         0 my %params = @_;
471 0         0 my $db = $self->{db};
472 0 0       0 unless ($db) {
473 0 0       0 $self->error(sprintf("Database \"%s\" connect failed", $self->dsn))
474             unless $self->error;
475 0         0 return ();
476             }
477 0         0 $self->error("");
478 0         0 my %table = $db->tableh("id", COLLECTOR_SELECT, $params{name});
479 0 0       0 if (my $dberr = $db->connect->errstr()) {
480 0   0     0 $self->error($dberr || $DBI::errstr || "unknown error");
481 0         0 return ();
482             }
483 0         0 my @out = ();
484 0         0 foreach my $k (sort {$a <=> $b} keys %table) {
  0         0  
485 0         0 push @out, $table{$k};
486             }
487 0         0 return @out;
488             }
489             sub report {
490 0     0 1 0 my $self = shift;
491 0         0 my %params = @_;
492 0         0 my $db = $self->{db};
493 0 0       0 unless ($db) {
494 0 0       0 $self->error(sprintf("Database \"%s\" connect failed", $self->dsn))
495             unless $self->error;
496 0         0 return ();
497             }
498 0         0 $self->error("");
499              
500 0   0     0 my $start = $params{start} || (time() - REPORT_PERIOD);
501 0         0 my %table = $db->tableh("id", COLLECTOR_REPORT, $start);
502 0 0       0 if (my $dberr = $db->connect->errstr()) {
503 0   0     0 $self->error($dberr || $DBI::errstr || "unknown error");
504 0         0 return ();
505             }
506 0         0 my @out = ();
507 0         0 foreach my $k (sort {$a <=> $b} keys %table) {
  0         0  
508 0         0 push @out, $table{$k};
509             }
510 0         0 return @out;
511             }
512              
513             sub _attr { # Sets attributes
514 2     2   4 my $in = shift;
515 2 50       10 my $attr = is_array($in) ? $in : array($in => "set");
516 2         21 my %attrs;
517 2         5 foreach (@$attr) {
518 6 50       36 $attrs{$1} = $2 if $_ =~ /^\s*(\S+)\s+(.+)$/;
519             }
520 2         30 return {%attrs};
521             }
522              
523             1;
524              
525             __END__