File Coverage

blib/lib/MToken/Store.pm
Criterion Covered Total %
statement 183 217 84.3
branch 45 96 46.8
condition 16 47 34.0
subroutine 34 38 89.4
pod 14 14 100.0
total 292 412 70.8


line stmt bran cond sub pod time code
1             package MToken::Store; # $Id: Store.pm 116 2021-10-12 15:17:49Z minus $
2 2     2   70228 use strict;
  2         18  
  2         71  
3 2     2   592 use utf8;
  2         17  
  2         15  
4              
5             =encoding utf-8
6              
7             =head1 NAME
8              
9             MToken::Store - MToken store class
10              
11             =head1 VERSION
12              
13             Version 1.00
14              
15             =head1 SYNOPSIS
16              
17             use MToken::Store;
18              
19             my $store = MToken::Store->new(
20             file => "/tmp/test.db",
21             attributes => "RaiseError=0; PrintError=0; sqlite_unicode=1",
22             do_init => 1, # Need to try initialize the db
23             );
24              
25             my $store = MToken::Store->new(
26             dsn => "DBI:mysql:database=MToken;host=mysql.example.com",
27             user => "username",
28             password => "password",
29             set => [
30             "RaiseError 0",
31             "PrintError 0",
32             "mysql_enable_utf8 1",
33             ],
34             );
35              
36             die($store->error) unless $store->status;
37              
38             =head1 DESCRIPTION
39              
40             This module provides store methods.
41              
42             =head2 SQLITE DDL
43              
44             CREATE TABLE "mtoken" (
45             "id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT UNIQUE,
46             "file" CHAR(256) NOT NULL UNIQUE,
47             "size" INTEGER NOT NULL,
48             "mtime" INTEGER NOT NULL,
49             "checksum" CHAR(64) DEFAULT NULL,
50             "tags" CHAR(256) DEFAULT NULL,
51             "subject" CHAR(1024) DEFAULT NULL,
52             "content" TEXT DEFAULT NULL
53             );
54              
55             =head2 MYSQL DDL
56              
57             CREATE DATABASE `mtoken` /*!40100 DEFAULT CHARACTER SET utf8 COLLATE utf8_bin */;
58             CREATE TABLE IF NOT EXISTS `mtoken` (
59             `id` INT(11) NOT NULL AUTO_INCREMENT,
60             `file` VARCHAR(256) COLLATE utf8_bin NOT NULL, -- File name
61             `size` INT(11) NOT NULL, -- File size
62             `mtime` INT(11) NOT NULL, -- Unixtime value of modified time (mtime)
63             `checksum` VARCHAR(64) NOT NULL, -- Checksum (MD5/SHA1/SHA256)
64             `tags` VARCHAR(256) DEFAULT NULL, -- Tags
65             `subject` VARCHAR(1024) DEFAULT NULL, -- Subject
66             `content` TEXT COLLATE utf8_bin DEFAULT NULL,
67             PRIMARY KEY (`id`),
68             UNIQUE KEY `file` (`file`)
69             ) ENGINE=MyISAM DEFAULT CHARSET=utf8 COLLATE=utf8_bin;
70              
71             =head2 POSTGRESQL DDL
72              
73             CREATE TABLE IF NOT EXISTS `mtoken` (
74             `id` INT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
75             `file` CHAR(256) COLLATE utf8_bin NOT NULL UNIQUE,
76             `size` INTEGER NOT NULL,
77             `mtime` INTEGER NOT NULL,
78             `checksum` CHAR(64) DEFAULT NULL,
79             `tags` CHAR(256) DEFAULT NULL,
80             `subject` CHAR(1024) DEFAULT NULL,
81             `content` TEXT COLLATE utf8_bin DEFAULT NULL
82             );
83              
84             =head1 METHODS
85              
86             =head2 new
87              
88             my $store = MToken::Store->new(
89             file => "/tmp/test.db",
90             attributes => "RaiseError=0; PrintError=0; sqlite_unicode=1",
91             do_init => 1, # Need to try initialize the db
92             );
93              
94             # ... or MySQL:
95              
96             my $store = MToken::Store->new(
97             dsn => "DBI:mysql:database=mtoken;host=mysql.example.com",
98             user => "username",
99             password => "password",
100             set => [
101             "RaiseError 0",
102             "PrintError 0",
103             "mysql_enable_utf8 1",
104             ],
105             );
106              
107             Creates DBI object
108              
109             =head2 add
110              
111             $store->add(
112             file => "test.txt",
113             size => 1024,
114             mtime => 1590000000,
115             checksum => "1a6f4a41ae8eec2da84dbfa48636e02e33575dbd",
116             tags => "test, example",
117             subject => "Test file for example",
118             content => "...Content of the file...",
119             ) or die($store->error);
120              
121             Add new recored
122              
123             =head2 count
124              
125             print $store->count();
126              
127             Returns count of records
128              
129             =head2 del
130              
131             $store->del("test.txt") or die($store->error);
132              
133             Delete record by filename
134              
135             $store->del(1) or die($store->error);
136              
137             Delete record by record id
138              
139             =head2 dsn
140              
141             my $dsn = $store->dsn;
142              
143             Returns DSN string of current database connection
144              
145             =head2 error
146              
147             my $error = $store->error;
148              
149             Returns error message
150              
151             my $status = $store->error( "Error message" );
152              
153             Sets error message if argument is provided.
154             This method in "set" context returns status of the operation as status() method.
155              
156             =head2 file
157              
158             my $file = $store->file;
159              
160             Returns the file of SQLite database
161              
162             =head2 get
163              
164             my %data = $store->get("test.txt");
165              
166             Returns data from database by filename
167              
168             my %data = $store->get(1);
169              
170             Returns data from database by record id
171              
172             =head2 getall
173              
174             my @table = $store->getall();
175             my @table_100 = $store->getall(100); # row_count
176             my @table_100 = $store->getall(100, 100); # offset, row_count
177              
178             Returns data from database with limit supporting
179              
180             =head2 is_sqlite
181              
182             print $store->is_sqlite ? "Is SQLite" : "Is NOT SQLite"
183              
184             Returns true if type of current database is SQLite
185              
186             =head2 ping
187              
188             $store->ping ? 'OK' : 'Database session is expired';
189              
190             Checks the connection to database
191              
192             =head2 set
193              
194             $store->set(
195             file => "test.txt",
196             size => 1024,
197             mtime => 1590000000,
198             checksum => "1a6f4a41ae8eec2da84dbfa48636e02e33575dbd",
199             tags => "test, example",
200             subject => "Test file for example",
201             content => "...New content of the file...",
202             ) or die($store->error);
203              
204             Update recored by document number
205              
206             $store->set(
207             id => 1,
208             file => "test.txt",
209             size => 1024,
210             mtime => 1590000000,
211             checksum => "1a6f4a41ae8eec2da84dbfa48636e02e33575dbd",
212             tags => "test, example",
213             subject => "Test file for example",
214             content => "...New content of the file...",
215             ) or die($store->error);
216              
217             Update recored by record id
218              
219             =head2 status
220              
221             my $status = $store->status;
222             my $status = $store->status( 1 ); # Sets the status value and returns it
223              
224             Get/set BOOL status of the operation
225              
226             =head2 truncate
227              
228             $store->truncate or die($store->error);
229              
230             Delete all records
231              
232             =head1 HISTORY
233              
234             See C file
235              
236             =head1 TO DO
237              
238             See C file
239              
240             =head1 BUGS
241              
242             * none noted
243              
244             =head1 SEE ALSO
245              
246             L, L
247              
248             =head1 AUTHOR
249              
250             Serż Minus (Sergey Lepenkov) L Eabalama@cpan.orgE
251              
252             =head1 COPYRIGHT
253              
254             Copyright (C) 1998-2021 D&D Corporation. All Rights Reserved
255              
256             =head1 LICENSE
257              
258             This program is free software; you can redistribute it and/or
259             modify it under the same terms as Perl itself.
260              
261             See C file and L
262              
263             =cut
264              
265 2     2   143 use vars qw/$VERSION/;
  2         4  
  2         109  
266             $VERSION = '1.00';
267              
268 2     2   13 use Carp;
  2         4  
  2         141  
269 2     2   996 use CTK::DBI;
  2         227186  
  2         104  
270 2     2   18 use CTK::Util qw/ touch /;
  2         5  
  2         106  
271 2     2   470 use CTK::ConfGenUtil;
  2         1084  
  2         159  
272 2     2   480 use CTK::TFVals qw/ :ALL /;
  2         2081  
  2         494  
273 2     2   15 use File::Spec;
  2         6  
  2         97  
274              
275             use constant {
276 2         238 DBFILE_MOD => 0666,
277             DEFAULT_DSN_MASK => 'DBI:SQLite:dbname=%s',
278             DEFAULT_DBI_ATTR => {
279             dsn => '', # See DEFAULT_DSN_MASK
280             user => '',
281             password => '',
282             set => [
283             'RaiseError 0',
284             'PrintError 0',
285             'sqlite_unicode 1',
286             ],
287             },
288 2     2   12 };
  2         4  
289              
290 2     2   14 use constant DDL_CREATE_TABLE => <<'DDL';
  2         6  
  2         142  
291             CREATE TABLE IF NOT EXISTS "mtoken" (
292             "id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT UNIQUE,
293             "file" CHAR(256) NOT NULL UNIQUE,
294             "size" INTEGER NOT NULL,
295             "mtime" INTEGER NOT NULL,
296             "checksum" CHAR(64) DEFAULT NULL,
297             "tags" CHAR(256) DEFAULT NULL,
298             "subject" CHAR(1024) DEFAULT NULL,
299             "content" TEXT DEFAULT NULL
300             )
301             DDL
302 2     2   15 use constant DDL_CREATE_INDEX => <<'DDL';
  2         5  
  2         112  
303             CREATE UNIQUE INDEX "file" ON "mtoken" (
304             "file"
305             )
306             DDL
307              
308 2     2   12 use constant DML_ADD => <<'DML';
  2         5  
  2         104  
309             INSERT INTO `mtoken`
310             (`file`,`size`,`mtime`,`checksum`,`tags`,`subject`,`content`)
311             VALUES
312             (?,?,?,?,?,?,?)
313             DML
314              
315 2     2   13 use constant DML_GET_COUNT => <<'DML';
  2         4  
  2         126  
316             SELECT COUNT(`id`) AS `cnt`
317             FROM `mtoken`
318             DML
319              
320 2     2   19 use constant DML_GET_BY_ID => <<'DML';
  2         6  
  2         154  
321             SELECT `id`,`file`,`size`,`mtime`,`checksum`,`tags`,`subject`,`content`
322             FROM `mtoken`
323             WHERE `id` = ?
324             DML
325              
326 2     2   13 use constant DML_GET_BY_FILE => <<'DML';
  2         5  
  2         130  
327             SELECT `id`,`file`,`size`,`mtime`,`checksum`,`tags`,`subject`,`content`
328             FROM `mtoken`
329             WHERE `file` = ?
330             DML
331              
332 2     2   14 use constant DML_GETALL => <<'DML';
  2         4  
  2         123  
333             SELECT `id`,`file`,`size`,`mtime`,`checksum`,`tags`,`subject`
334             FROM `mtoken`
335             ORDER BY `file` ASC
336             DML
337              
338 2     2   13 use constant DML_DEL_BY_ID => <<'DML';
  2         4  
  2         167  
339             DELETE FROM `mtoken` WHERE `id` = ?
340             DML
341              
342 2     2   15 use constant DML_DEL_BY_FILE => <<'DML';
  2         21  
  2         128  
343             DELETE FROM `mtoken` WHERE `file` = ?
344             DML
345              
346 2     2   13 use constant DML_TRUNCATE => <<'DML';
  2         4  
  2         108  
347             DELETE FROM `mtoken`
348             DML
349              
350 2     2   13 use constant DML_SET_BY_ID => <<'DML';
  2         4  
  2         116  
351             UPDATE `mtoken`
352             SET `file` = ?, `size` = ?, `mtime` = ?, `checksum` = ?, `tags` = ?, `subject` = ?, `content` = ?
353             WHERE `id` = ?
354             DML
355              
356 2     2   14 use constant DML_SET_BY_FILE => <<'DML';
  2         4  
  2         4298  
357             UPDATE `mtoken`
358             SET `file` = ?, `size` = ?, `mtime` = ?, `checksum` = ?, `tags` = ?, `subject` = ?, `content` = ?
359             WHERE `file` = ?
360             DML
361              
362             sub new {
363 1     1 1 105 my $class = shift;
364 1         7 my %args = @_;
365 1         4 my $args_set = $args{'set'};
366 1 50       4 unless ($args{dsn}) {
367 1         3 my $dda = DEFAULT_DBI_ATTR;
368 1         6 foreach (keys %$dda) {
369 4 100       11 next if $_ eq 'set';
370 3   33     17 $args{$_} //= $dda->{$_}
371             }
372             }
373 1   50     5 my $file = $args{file} // "";
374 1   33     10 my $dsn = $args{dsn} || sprintf(DEFAULT_DSN_MASK, $file);
375 1         6 my %attrs_from_set = _set2attr($args_set);
376 1 50       5 my %attrs_from_str = $args{attributes} ? _parseDBAttributes($args{attributes}) : ();
377 1         15 my %attrs = (%attrs_from_set, %attrs_from_str);
378 1 50       5 %attrs = _set2attr(DEFAULT_DBI_ATTR()->{set}) unless %attrs;
379              
380             # DB
381             my $db = CTK::DBI->new(
382             -dsn => $dsn,
383             -debug => 0,
384             -username => $args{'user'},
385             -password => $args{'password'},
386             -attr => {%attrs},
387             $args{timeout} ? (
388             -timeout_connect => $args{timeout},
389             -timeout_request => $args{timeout},
390 1 50       15 ) : (),
391             );
392 1 50       13372 my $dbh = $db->connect if $db;
393              
394             # SQLite
395 1         8 my $fnew = 0;
396 1         2 my $issqlite = 0;
397 1 50       6 my $do_init = $args{do_init} ? 1 : 0;
398 1 50 33     10 if ($dbh && $dsn =~ /SQLite/i) { # If SQLite (default)
399 1         18 $file = $dbh->sqlite_db_filename();
400 1 50       6 if ($do_init) {
401 1 50 33     36 unless ($file && (-e $file) && !(-z $file)) {
      33        
402 1         14 touch($file);
403 1         128 chmod(DBFILE_MOD, $file);
404 1         4 $fnew = 1;
405             }
406             }
407 1         2 $issqlite = 1;
408             }
409              
410             # Defaults
411 1         2 my $status = 1; # Ok
412 1         3 my $error = "";
413 1 50 33     11 if (!$db) {
    50          
    50          
414 0         0 $error = sprintf("Can't init database \"%s\"", $dsn);
415 0         0 $status = 0;
416             } elsif (!$dbh) {
417 0   0     0 $error = sprintf("Can't connect to database \"%s\": %s", $dsn, $DBI::errstr || "unknown error");
418 0         0 $status = 0;
419             } elsif ($fnew && $do_init) {
420 1         7 $db->execute(DDL_CREATE_TABLE);
421 1 50       15098 $db->execute(DDL_CREATE_INDEX) unless $dbh->err;
422 1         11329 $error = $dbh->errstr();
423 1 50       10 $status = 0 if $dbh->err;
424             }
425 1 50       4 unless ($error) { # No errors
426 1 50       8 unless ($dbh->ping) {
427 0   0     0 $error = sprintf("Can't init database \"%s\". Ping failed: %s",
428             $dsn, $dbh->errstr() || "unknown error");
429 0         0 $status = 0;
430             }
431             }
432              
433 1   50     61 my $self = bless {
434             file => $file,
435             issqlite=> $issqlite,
436             dsn => $dsn,
437             error => $error // "",
438             dbi => $db,
439             status => $status,
440             }, $class;
441              
442 1         12 return $self;
443             }
444              
445             sub status {
446 10     10 1 39 my $self = shift;
447 10         17 my $value = shift;
448 10 100       45 return fv2zero($self->{status}) unless defined($value);
449 7 50       21 $self->{status} = $value ? 1 : 0;
450 7         13 return $self->{status};
451             }
452             sub error {
453 7     7 1 15 my $self = shift;
454 7         13 my $value = shift;
455 7 50       20 return uv2null($self->{error}) unless defined($value);
456 7         18 $self->{error} = $value;
457 7 50       65 $self->status($value ne "" ? 0 : 1);
458 7         11 return $value;
459             }
460             sub ping {
461 8     8 1 960 my $self = shift;
462 8 50       34 return 0 unless $self->{dsn};
463 8         14 my $dbi = $self->{dbi};
464 8 50       23 return 0 unless $dbi;
465 8         17 my $dbh = $dbi->{dbh};
466 8 50       21 return 0 unless $dbh;
467 8 50       52 return 0 unless $dbh->can('ping');
468 8         42 return $dbh->ping();
469             }
470             sub dsn {
471 0     0 1 0 my $self = shift;
472 0         0 return $self->{dsn};
473             }
474             sub is_sqlite {
475 0     0 1 0 my $self = shift;
476 0 0       0 return $self->{issqlite} ? 1 : 0;
477             }
478             sub file {
479 0     0 1 0 my $self = shift;
480 0         0 return $self->{file};
481             }
482              
483             # CRUD Methods
484              
485             sub add {
486 2     2 1 8 my $self = shift;
487 2         13 my %data = @_;
488 2 50       8 return unless $self->ping;
489 2         81 $self->error("");
490 2         5 my $dbi = $self->{dbi};
491              
492             # Add
493             $dbi->execute(DML_ADD,
494             $data{file},
495             $data{size} || 0,
496             $data{mtime} || time(),
497             $data{checksum},
498             $data{tags},
499             $data{subject},
500             $data{content},
501 2   50     42 );
      33        
502 2 50       24112 if ($dbi->connect->err) {
503 0         0 $self->error(sprintf("Can't insert new record: %s", uv2null($dbi->connect->errstr)));
504 0         0 return;
505             }
506              
507 2         41 return 1;
508             }
509             sub count {
510 0     0 1 0 my $self = shift;
511 0 0       0 return 0 unless $self->ping;
512 0         0 $self->error("");
513 0         0 my $dbi = $self->{dbi};
514              
515 0   0     0 my $cnt = $dbi->field(DML_GET_COUNT) || 0;
516 0 0       0 if ($dbi->connect->err) {
517 0         0 $self->error(sprintf("Can't get count: %s", uv2null($dbi->connect->errstr)));
518 0         0 return 0;
519             }
520              
521 0         0 return $cnt;
522             }
523             sub get {
524 1     1 1 4 my $self = shift;
525 1   50     4 my $id = shift || 0;
526 1 50       4 return () unless $self->ping;
527 1         51 $self->error("");
528 1         2 my $dbi = $self->{dbi};
529              
530 1 50       7 my %rec = $dbi->recordh(is_int($id) ? DML_GET_BY_ID : DML_GET_BY_FILE, $id);
531 1 50       699 if ($dbi->connect->err) {
532 0         0 $self->error(sprintf("Can't get record: %s", uv2null($dbi->connect->errstr)));
533 0         0 return ();
534             }
535              
536 1         19 return %rec;
537             }
538             sub getall {
539 1     1 1 4 my $self = shift;
540 1         5 my $row_count = pop;
541 1         2 my $offset = pop;
542 1 50       4 return () unless $self->ping;
543 1         78 $self->error("");
544 1         4 my $dbi = $self->{dbi};
545 1 0       7 my $limit = $row_count
    50          
546             ? $offset
547             ? sprintf(" LIMIT %d, %d", $offset, $row_count)
548             : sprintf(" LIMIT %d", $row_count)
549             : "";
550              
551 1         18 my @tbl = $dbi->table(sprintf("%s%s", DML_GETALL, $limit ));
552 1 50       764 if ($dbi->connect->err) {
553 0         0 $self->error(sprintf("Can't get records: %s", uv2null($dbi->connect->errstr)));
554 0         0 return ();
555             }
556 1         20 return @tbl;
557             }
558             sub del {
559 1     1 1 535 my $self = shift;
560 1   50     5 my $id = shift || 0;
561 1 50       5 return 0 unless $self->ping;
562 1         48 $self->error("");
563 1         5 my $dbi = $self->{dbi};
564              
565 1 50       5 $dbi->execute(is_int($id) ? DML_DEL_BY_ID : DML_DEL_BY_FILE, $id);
566 1 50       11834 if ($dbi->connect->err) {
567 0         0 $self->error(sprintf("Can't delete record: %s", uv2null($dbi->connect->errstr)));
568 0         0 return 0;
569             }
570 1         15 return 1;
571             }
572             sub truncate {
573 1     1 1 2 my $self = shift;
574 1 50       5 return 0 unless $self->ping;
575 1         43 $self->error("");
576 1         2 my $dbi = $self->{dbi};
577              
578 1         5 $dbi->execute(DML_TRUNCATE);
579 1 50       11181 if ($dbi->connect->err) {
580 0         0 $self->error(sprintf("Can't truncate table: %s", uv2null($dbi->connect->errstr)));
581 0         0 return 0;
582             }
583 1         20 return 1;
584             }
585             sub set {
586 1     1 1 3 my $self = shift;
587 1         7 my %data = @_;
588 1 50       3 return unless $self->ping;
589 1         40 $self->error("");
590 1         3 my $dbi = $self->{dbi};
591              
592             # Set by id or num
593             $dbi->execute($data{id} ? DML_SET_BY_ID : DML_SET_BY_FILE,
594             $data{file},
595             $data{size} || 0,
596             $data{mtime} || time(),
597             $data{checksum},
598             $data{tags},
599             $data{subject},
600             $data{content},
601             $data{id} || $data{file},
602 1 50 50     19 );
      33        
      33        
603 1 50       12445 if ($dbi->connect->err) {
604 0         0 $self->error(sprintf("Can't update record: %s", uv2null($dbi->connect->errstr)));
605 0         0 return;
606             }
607              
608 1         27 return 1;
609             }
610              
611             sub _set2attr {
612 1     1   2 my $in = shift;
613 1 50       8 my $attr = is_array($in) ? $in : array($in => "set");
614 1         98 my %attrs;
615 1         3 foreach (@$attr) {
616 0 0       0 $attrs{$1} = $2 if $_ =~ /^\s*(\S+)\s+(.+)$/;
617             }
618 1         4 return %attrs;
619             }
620             sub _parseDBAttributes { # Example: foo=123, bar=gii,baz;qux=werwe
621 1   50 1   5 my $t = shift || return ();
622 1         2 my %as = ();
623 1 50       10 for ($t ? (split /\s*[,;]\s*/, $t) : ()) {
624 3         18 my ($k,$v) = (split /\s*=>?\s*|\s*\:\s*/, $_);
625 3         9 $as{$k} = $v;
626             }
627 1         7 return %as;
628             }
629              
630             1;
631              
632             __END__