move sql schema in __DATA__
[BackupPC.git] / bin / BackupPC_ASA_SearchUpdate
1 #!/usr/bin/perl
2
3 use strict;
4 use lib "/usr/local/BackupPC/lib";
5
6 use DBI;
7 use BackupPC::Lib;
8 use BackupPC::View;
9 use Data::Dumper;
10 use Getopt::Long::Descriptive;
11 use Time::HiRes qw/time/;
12 use File::Pid;
13 use POSIX qw/strftime/;
14 use BackupPC::Search;
15 use Cwd qw/abs_path/;
16 use Data::Dump qw(dump);
17
18 use constant BPC_FTYPE_DIR => 5;
19 use constant EST_CHUNK => 4096;
20
21 # daylight saving time change offset for 1h
22 my $dst_offset = 60 * 60;
23
24 my $debug = 0;
25 $|=1;
26
27 my $start_t = time();
28
29 my $pid_path = abs_path($0);
30 $pid_path =~ s/\W+/_/g;
31
32 my $pidfile = new File::Pid({
33         file => "/tmp/search_update.pid",
34 });
35
36 if (my $pid = $pidfile->running ) {
37         die "$0 already running: $pid\n";
38 } elsif ($pidfile->pid ne $$) {
39         $pidfile->remove;
40         $pidfile = new File::Pid;
41 }
42 print STDERR "$0 using pid ",$pidfile->pid," file ",$pidfile->file,"\n";
43 $pidfile->write;
44
45 my $t_fmt = '%Y-%m-%d %H:%M:%S';
46
47 my $hosts;
48 my $bpc = BackupPC::Lib->new || die;
49 my %Conf = $bpc->Conf();
50 my $TopDir = $bpc->TopDir();
51 my $beenThere = {};
52
53 my $dsn = $Conf{SearchDSN} || die "Need SearchDSN in config.pl\n";
54 my $user = $Conf{SearchUser} || '';
55
56 my $index_node_url = $Conf{HyperEstraierIndex};
57
58 my $dbh = DBI->connect($dsn, $user, "", { RaiseError => 1, AutoCommit => 0 });
59
60 my ($opt,$usage) = describe_options(
61 "%c %o",
62 [ 'create|c',   "create database on first use" ],
63 [ 'delete|d',   "delete database before import" ],
64 [ 'max|m=i',    "import just max increments for one host" ],
65 [ 'host|h=s@',  "import just host(s)" ],
66 [],
67 [ 'verbose|v:i', 'set verbosity (debug) level' ],
68 [ 'index|i',    'update full text index' ],
69 [ 'junk|j',     "update full text, don't check existing files" ],
70 [ 'fast|f',     "don't do anything with full text index" ],
71 [ 'quiet|q',    "be quiet for hosts without changes" ],
72 [ 'help',       "show help" ],
73 );
74
75 print($usage->text), exit if $opt->help;
76
77 warn "hosts: ",dump( $opt->host );
78
79 #---- subs ----
80
81 sub status {
82         my $text = shift;
83         $text =~ s{\s+$}{};
84         my $new = $0;
85         $new =~ s{^[\w\/]+/(\w+) }{$1 }; # strip path from process name
86         if ( $text =~ m/^\|/ ) {
87                 $new =~ s/\|.*/$text/ or $new .= " $text";
88         } else {
89                 $new =~ s/\s+.*/ $text/ or $new .= " $text";
90         }
91         $0 = $new;
92 }
93
94 sub fmt_time {
95         my $t = shift || return;
96         my $out = "";
97         my ($ss,$mm,$hh) = gmtime($t);
98         $out .= "${hh}h" if ($hh);
99         $out .= sprintf("%02d:%02d", $mm,$ss);
100         return $out;
101 }
102
103 sub curr_time {
104         return strftime($t_fmt,localtime());
105 }
106
107 sub hest_update {
108
109         my ($host_id, $share_id, $num) = @_;
110
111         my $skip_check = $opt->junk && print STDERR "Skipping check for existing files -- this should be used only with initital import\n";
112
113         print curr_time," updating fulltext:";
114
115         my $t = time();
116
117         my $offset = 0;
118         my $added = 0;
119
120         my $search = BackupPC::Search->search_module;
121
122         my $results = 0;
123
124         do {
125
126                 my $where = '';
127                 my @data;
128                 if (defined($host_id) && defined($share_id) && defined($num)) {
129                         $where = qq{
130                         WHERE
131                                 hosts.id = ? AND
132                                 shares.id = ? AND
133                                 files.backupnum = ?
134                         };
135                         @data = ( $host_id, $share_id, $num );
136                 }
137
138                 my $limit = sprintf('LIMIT '.EST_CHUNK.' OFFSET %d', $offset);
139
140                 my $sth = $dbh->prepare(qq{
141                         SELECT
142                                 files.id                        AS fid,
143                                 hosts.name                      AS hname,
144                                 shares.name                     AS sname,
145                                 -- shares.share                 AS sharename,
146                                 files.backupnum                 AS backupnum,
147                                 -- files.name                   AS filename,
148                                 files.path                      AS filepath,
149                                 files.date                      AS date,
150                                 files.type                      AS type,
151                                 files.size                      AS size,
152                                 files.shareid                   AS shareid,
153                                 backups.date                    AS backup_date
154                         FROM files 
155                                 INNER JOIN shares       ON files.shareID=shares.ID
156                                 INNER JOIN hosts        ON hosts.ID = shares.hostID
157                                 INNER JOIN backups      ON backups.num = files.backupNum and backups.hostID = hosts.ID AND backups.shareID = shares.ID
158                         $where
159                         $limit
160                 });
161
162                 $sth->execute(@data);
163                 $results = $sth->rows;
164
165                 if ($results == 0) {
166                         print " - no new files\n";
167                         return;
168                 } else {
169                         print "...";
170                 }
171
172                 sub fmt_date {
173                         my $t = shift || return;
174                         my $iso = BackupPC::Lib::timeStamp($t);
175                         $iso =~ s/\s/T/;
176                         return $iso;
177                 }
178
179                 while (my $row = $sth->fetchrow_hashref()) {
180                         next if $search->exists( $row );
181                         $search->add_doc( $row );
182                         $added++;
183                 }
184
185                 print "$added";
186                 status "| $added";
187
188                 $offset += EST_CHUNK;
189
190         } while ($results == EST_CHUNK);
191
192         $search->commit;
193
194         my $dur = (time() - $t) || 1;
195         printf(" [%.2f/s dur: %s]\n",
196                 ( $added / $dur ),
197                 fmt_time($dur)
198         );
199 }
200
201 #---- /subs ----
202
203
204 ## update index ##
205 if ( ( $opt->index || $opt->junk ) && !$opt->create ) {
206         # update all
207         print "force update of Hyper Estraier index ";
208         print "by -i flag" if ($opt->index);
209         print "by -j flag" if ($opt->junk);
210         print "\n";
211         hest_update();
212 }
213
214 ## create tables ##
215 if ($opt->create) {
216         sub do_index {
217                 my $index = shift || return;
218                 my ($table,$col,$unique) = split(/:/, $index);
219                 $unique ||= '';
220                 $index =~ s/\W+/_/g;
221                 print "$index on $table($col)" . ( $unique ? "u" : "" ) . " ";
222                 $dbh->do(qq{ create $unique index $index on $table($col) });
223         }
224
225         print "creating tables...\n";
226
227         my $sql;
228         { local $/ = undef; $sql = <DATA> };
229         $dbh->do( $sql );
230
231         print "creating indexes: ";
232
233         foreach my $index (qw(
234                 hosts:name
235                 backups:hostID
236                 backups:num
237                 backups:shareID
238                 shares:hostID
239                 shares:name
240                 files:shareID
241                 files:path
242                 files:name
243                 files:date
244                 files:size
245                 archive:dvd_nr
246                 archive_burned:archive_id
247                 backup_parts:backup_id,part_nr:unique
248         )) {
249                 do_index($index);
250         }
251
252         print " creating sequence: ";
253         foreach my $seq (qw/dvd_nr/) {
254                 print "$seq ";
255                 $dbh->do( qq{ CREATE SEQUENCE $seq } );
256         }
257
258         print "...\n";
259
260         $dbh->commit;
261
262 }
263
264 ## delete data before inseting ##
265 if ($opt->delete) {
266         print "deleting ";
267         foreach my $table (qw(files dvds backups shares hosts)) {
268                 print "$table ";
269                 $dbh->do(qq{ DELETE FROM $table });
270         }
271         print " done...\n";
272
273         $dbh->commit;
274 }
275
276 ## insert new values ##
277
278 # get hosts
279 $hosts = $bpc->HostInfoRead();
280 my $hostID;
281 my $shareID;
282
283 my $sth;
284
285 $sth->{insert_hosts} = $dbh->prepare(qq{
286 INSERT INTO hosts (name, IP) VALUES (?,?)
287 });
288
289 $sth->{hosts_by_name} = $dbh->prepare(qq{
290 SELECT id FROM hosts WHERE name=?
291 });
292
293 $sth->{backups_count} = $dbh->prepare(qq{
294 SELECT COUNT(*)
295 FROM backups
296 WHERE hostID=? AND num=? AND shareid=?
297 });
298
299 $sth->{insert_backups} = $dbh->prepare(qq{
300 INSERT INTO backups (hostID, num, date, type, shareid, size)
301 VALUES (?,?,?,?,?,-1)
302 });
303
304 $sth->{update_backups_size} = $dbh->prepare(qq{
305 UPDATE backups SET size = ?
306 WHERE hostID = ? and num = ? and date = ? and type =? and shareid = ?
307 });
308
309 $sth->{insert_files} = $dbh->prepare(qq{
310 INSERT INTO files
311         (shareID, backupNum, name, path, date, type, size)
312         VALUES (?,?,?,?,?,?,?)
313 });
314
315 my @hosts = keys %{$hosts};
316 my $host_nr = 0;
317
318 foreach my $host_key (@hosts) {
319
320         my $hostname = $hosts->{$host_key}->{'host'} || die "can't find host for $host_key";
321
322         next if $opt->host && ! grep { m/^$hostname$/ } @{ $opt->host };
323
324         $sth->{hosts_by_name}->execute($hostname);
325
326         unless (($hostID) = $sth->{hosts_by_name}->fetchrow_array()) {
327                 $sth->{insert_hosts}->execute(
328                         $hosts->{$host_key}->{'host'},
329                         $hosts->{$host_key}->{'ip'}
330                 );
331
332                 $hostID = $dbh->last_insert_id(undef,undef,'hosts',undef);
333         }
334
335         $host_nr++;
336         # get backups for a host
337         my @backups = $bpc->BackupInfoRead($hostname);
338         my $incs = scalar @backups;
339
340         my $host_header = sprintf("host %s [%d/%d]: %d increments\n",
341                 $hosts->{$host_key}->{'host'},
342                 $host_nr,
343                 ($#hosts + 1),
344                 $incs
345         );
346         print $host_header unless $opt->quiet;
347  
348         my $inc_nr = 0;
349         $beenThere = {};
350
351         foreach my $backup (@backups) {
352
353                 $inc_nr++;
354                 last if defined $opt->max && $inc_nr > $opt->max;
355
356                 my $backupNum = $backup->{'num'};
357                 my @backupShares = ();
358
359                 my $share_header = sprintf("%-10s %2d/%-2d #%-2d %s %5s/%5s files (date: %s dur: %s)\n", 
360                         $hosts->{$host_key}->{'host'},
361                         $inc_nr, $incs, $backupNum, 
362                         $backup->{type} || '?',
363                         $backup->{nFilesNew} || '?', $backup->{nFiles} || '?',
364                         strftime($t_fmt,localtime($backup->{startTime})),
365                         fmt_time($backup->{endTime} - $backup->{startTime})
366                 );
367                 print $share_header unless $opt->quiet;
368                 status "$hostname $backupNum $share_header";
369
370                 my $files = BackupPC::View->new($bpc, $hostname, \@backups, { only_increment => 1 });
371
372                 foreach my $share ($files->shareList($backupNum)) {
373
374                         my $t = time();
375
376                         $shareID = getShareID($share, $hostID, $hostname);
377                 
378                         $sth->{backups_count}->execute($hostID, $backupNum, $shareID);
379                         my ($count) = $sth->{backups_count}->fetchrow_array();
380                         # skip if allready in database!
381                         next if ($count > 0);
382
383                         # dump host and share header for -q
384                         if ( $opt->quiet ) {
385                                 if ($host_header) {
386                                         print $host_header;
387                                         $host_header = undef;
388                                 }
389                                 print $share_header;
390                         }
391
392                         # dump some log
393                         print curr_time," ", $share;
394
395                         $sth->{insert_backups}->execute(
396                                 $hostID,
397                                 $backupNum,
398                                 $backup->{'endTime'},
399                                 substr($backup->{'type'},0,4),
400                                 $shareID,
401                         );
402
403                         my ($f, $nf, $d, $nd, $size) = recurseDir($bpc, $hostname, $files, $backupNum, $share, "", $shareID);
404
405                         eval {
406                                 $sth->{update_backups_size}->execute(
407                                         $size,
408                                         $hostID,
409                                         $backupNum,
410                                         $backup->{'endTime'},
411                                         substr($backup->{'type'},0,4),
412                                         $shareID,
413                                 );
414                                 print " commit";
415                                 $dbh->commit();
416                         };
417                         if ($@) {
418                                 print " rollback";
419                                 $dbh->rollback();
420                         }
421
422                         my $dur = (time() - $t) || 1;
423                         my $status = sprintf("%d/%d files %d/%d dirs %0.2f MB [%.2f/s dur: %s]",
424                                 $nf, $f, $nd, $d,
425                                 ($size / 1024 / 1024),
426                                 ( ($f+$d) / $dur ),
427                                 fmt_time($dur)
428                         );
429                         print " $status\n";
430                         status "$hostname $backupNum $status";
431
432                         if ($nf + $nd > 0) {
433                                 status "$hostname $backupNum full-text | indexing";
434                                 #eval { hest_update($hostID, $shareID, $backupNum) };
435                                 #warn "ERROR: $@" if $@;
436                                 hest_update($hostID, $shareID, $backupNum);
437                                 # eval breaks our re-try logic
438                         }
439                 }
440
441         }
442 }
443 undef $sth;
444 $dbh->commit();
445 $dbh->disconnect();
446
447 print "total duration: ",fmt_time(time() - $start_t),"\n";
448
449 $pidfile->remove;
450
451 sub getShareID() {
452
453         my ($share, $hostID, $hostname) = @_;
454
455         $sth->{share_id} ||= $dbh->prepare(qq{
456                 SELECT ID FROM shares WHERE hostID=? AND name=?
457         });
458
459         $sth->{share_id}->execute($hostID,$share);
460
461         my ($id) = $sth->{share_id}->fetchrow_array();
462
463         return $id if (defined($id));
464
465         $sth->{insert_share} ||= $dbh->prepare(qq{
466                 INSERT INTO shares 
467                         (hostID,name,share) 
468                 VALUES (?,?,?)
469         });
470
471         my $drop_down = $hostname . '/' . $share;
472         $drop_down =~ s#//+#/#g;
473
474         $sth->{insert_share}->execute($hostID,$share, $drop_down);
475         return $dbh->last_insert_id(undef,undef,'shares',undef);
476 }
477
478 sub found_in_db {
479
480         my @data = @_;
481         shift @data;
482
483         my ($key, $shareID,undef,$name,$path,$date,undef,$size) = @_;
484
485         return $beenThere->{$key} if (defined($beenThere->{$key}));
486
487         $sth->{file_in_db} ||= $dbh->prepare(qq{
488                 SELECT 1 FROM files
489                 WHERE shareID = ? and
490                         path = ? and 
491                         size = ? and
492                         ( date = ? or date = ? or date = ? )
493                 LIMIT 1
494         });
495
496         my @param = ($shareID,$path,$size,$date, $date-$dst_offset, $date+$dst_offset);
497         $sth->{file_in_db}->execute(@param);
498         my $rows = $sth->{file_in_db}->rows;
499         print STDERR "## found_in_db($shareID,$path,$date,$size) ",( $rows ? '+' : '-' ), join(" ",@param), "\n" if ($debug >= 3);
500
501         $beenThere->{$key}++;
502
503         $sth->{'insert_files'}->execute(@data) unless ($rows);
504         return $rows;
505 }
506
507 ####################################################
508 # recursing through filesystem structure and       #
509 # and returning flattened files list               #
510 ####################################################
511 sub recurseDir($$$$$$$$) {
512
513         my ($bpc, $hostname, $files, $backupNum, $share, $dir, $shareID) = @_;
514
515         print STDERR "\nrecurse($hostname,$backupNum,$share,$dir,$shareID)\n" if ($debug >= 1);
516
517         my ($nr_files, $new_files, $nr_dirs, $new_dirs, $size) = (0,0,0,0,0);
518
519         { # scope
520                 my @stack;
521
522                 print STDERR "# dirAttrib($backupNum, $share, $dir)\n" if ($debug >= 2);
523                 my $filesInBackup = $files->dirAttrib($backupNum, $share, $dir);
524
525                 # first, add all the entries in current directory
526                 foreach my $path_key (keys %{$filesInBackup}) {
527                         print STDERR "# file ",Dumper($filesInBackup->{$path_key}),"\n" if ($debug >= 3);
528                         my @data = (
529                                 $shareID,
530                                 $backupNum,
531                                 $path_key,
532                                 $filesInBackup->{$path_key}->{'relPath'},
533                                 $filesInBackup->{$path_key}->{'mtime'},
534                                 $filesInBackup->{$path_key}->{'type'},
535                                 $filesInBackup->{$path_key}->{'size'}
536                         );
537
538                         my $key = join(" ", (
539                                 $shareID,
540                                 $dir,
541                                 $path_key,
542                                 $filesInBackup->{$path_key}->{'mtime'},
543                                 $filesInBackup->{$path_key}->{'size'}
544                         ));
545
546                         my $key_dst_prev = join(" ", (
547                                 $shareID,
548                                 $dir,
549                                 $path_key,
550                                 $filesInBackup->{$path_key}->{'mtime'} - $dst_offset,
551                                 $filesInBackup->{$path_key}->{'size'}
552                         ));
553
554                         my $key_dst_next = join(" ", (
555                                 $shareID,
556                                 $dir,
557                                 $path_key,
558                                 $filesInBackup->{$path_key}->{'mtime'} + $dst_offset,
559                                 $filesInBackup->{$path_key}->{'size'}
560                         ));
561
562                         my $found;
563                         if (
564                                 ! defined($beenThere->{$key}) &&
565                                 ! defined($beenThere->{$key_dst_prev}) &&
566                                 ! defined($beenThere->{$key_dst_next}) &&
567                                 ! ($found = found_in_db($key, @data))
568                         ) {
569                                 print STDERR "# key: $key [", $beenThere->{$key},"]" if ($debug >= 2);
570
571                                 if ($filesInBackup->{$path_key}->{'type'} == BPC_FTYPE_DIR) {
572                                         $new_dirs++ unless ($found);
573                                         print STDERR " dir\n" if ($debug >= 2);
574                                 } else {
575                                         $new_files++ unless ($found);
576                                         print STDERR " file\n" if ($debug >= 2);
577                                 }
578                                 $size += $filesInBackup->{$path_key}->{'size'} || 0;
579                         }
580
581                         if ($filesInBackup->{$path_key}->{'type'} == BPC_FTYPE_DIR) {
582                                 $nr_dirs++;
583
584                                 my $full_path = $dir . '/' . $path_key;
585                                 push @stack, $full_path;
586                                 print STDERR "### store to stack: $full_path\n" if ($debug >= 3);
587
588 #                               my ($f,$nf,$d,$nd) = recurseDir($bpc, $hostname, $backups, $backupNum, $share, $path_key, $shareID) unless ($beenThere->{$key});
589 #
590 #                               $nr_files += $f;
591 #                               $new_files += $nf;
592 #                               $nr_dirs += $d;
593 #                               $new_dirs += $nd;
594
595                         } else {
596                                 $nr_files++;
597                         }
598                 }
599
600                 print STDERR "## STACK ",join(", ", @stack),"\n" if ($debug >= 2);
601
602                 while ( my $dir = shift @stack ) {
603                         my ($f,$nf,$d,$nd, $s) = recurseDir($bpc, $hostname, $files, $backupNum, $share, $dir, $shareID);
604                         print STDERR "# $dir f: $f nf: $nf d: $d nd: $nd\n" if ($debug >= 1);
605                         $nr_files += $f;
606                         $new_files += $nf;
607                         $nr_dirs += $d;
608                         $new_dirs += $nd;
609                         $size += $s;
610                 }
611         }
612
613         return ($nr_files, $new_files, $nr_dirs, $new_dirs, $size);
614 }
615
616 __DATA__
617
618 create table hosts (
619         ID      SERIAL          PRIMARY KEY,
620         name    VARCHAR(30)     NOT NULL,
621         IP      VARCHAR(15)
622 );            
623
624 create table shares (
625         ID      SERIAL          PRIMARY KEY,
626         hostID  INTEGER         NOT NULL references hosts(id),
627         name    VARCHAR(30)     NOT NULL,
628         share   VARCHAR(200)    NOT NULL
629 );            
630
631 create table dvds (
632         ID      SERIAL          PRIMARY KEY, 
633         num     INTEGER         NOT NULL,
634         name    VARCHAR(255)    NOT NULL,
635         mjesto  VARCHAR(255)
636 );
637
638 create table backups (
639         id      serial,
640         hostID  INTEGER         NOT NULL references hosts(id),
641         num     INTEGER         NOT NULL,
642         date    integer         NOT NULL, 
643         type    CHAR(4)         not null,
644         shareID integer         not null references shares(id),
645         size    bigint          not null,
646         inc_size bigint         not null default -1,
647         inc_deleted boolean     default false,
648         parts   integer         not null default 0,
649         PRIMARY KEY(id)
650 );            
651
652 create table backup_parts (
653         id serial,
654         backup_id int references backups(id),
655         part_nr int not null check (part_nr > 0),
656         tar_size bigint not null check (tar_size > 0),
657         size bigint not null check (size > 0),
658         md5 text not null,
659         items int not null check (items > 0),
660         date timestamp default now(),
661         filename text not null,
662         primary key(id)
663 );
664
665 create table files (
666         ID              SERIAL,
667         shareID         INTEGER NOT NULL references shares(id),
668         backupNum       INTEGER NOT NULL,
669         name            VARCHAR(255) NOT NULL,
670         path            VARCHAR(255) NOT NULL,
671         date            integer NOT NULL,
672         type            INTEGER NOT NULL,
673         size            bigint  NOT NULL,
674         primary key(id)
675 );
676
677 create table archive (
678         id              serial,
679         dvd_nr          int not null,
680         total_size      bigint default -1,
681         note            text,
682         username        varchar(20) not null,
683         date            timestamp default now(),
684         primary key(id)
685 );      
686
687 create table archive_parts (
688         archive_id      int not null references archive(id) on delete cascade,
689         backup_part_id  int not null references backup_parts(id),
690         primary key(archive_id, backup_part_id)
691 );
692
693 create table archive_burned (
694         archive_id      int references archive(id),
695         date            timestamp default now(),
696         part            int not null default 1,
697         copy            int not null default 1,
698         iso_size bigint default -1
699 );
700
701 -- report backups and corresponding dvd
702
703 --create view backups_on_dvds as
704 --select
705 --      backups.id as id,
706 --      hosts.name || ':' || shares.name as share,
707 --      backups.num as num,
708 --      backups.type as type,
709 --      abstime(backups.date) as backup_date,
710 --      backups.size as size,
711 --      backups.inc_size as gzip_size,
712 --      archive.id as archive_id,
713 --      archive.dvd_nr
714 --from backups
715 --join shares on backups.shareid=shares.id
716 --join hosts on shares.hostid = hosts.id
717 --left outer join archive_backup on backups.id = archive_backup.backup_id
718 --left outer join archive on archive_backup.archive_id = archive.id
719 --where backups.parts > 0 and size > 0
720 --order by backups.date
721 --;
722
723
724 create or replace function backup_parts_check() returns trigger as '
725 declare
726         b_parts integer;
727         b_counted integer;
728         b_id    integer;
729 begin
730         -- raise notice ''old/new parts %/% backup_id %/%'', old.parts, new.parts, old.id, new.id;
731         if (TG_OP=''UPDATE'') then
732                 b_id := new.id;
733                 b_parts := new.parts;
734         elsif (TG_OP = ''INSERT'') then
735                 b_id := new.id;
736                 b_parts := new.parts;
737         end if;
738         b_counted := (select count(*) from backup_parts where backup_id = b_id);
739         -- raise notice ''backup % parts %'', b_id, b_parts;
740         if ( b_parts != b_counted ) then
741                 raise exception ''Update of backup % aborted, requested % parts and there are really % parts'', b_id, b_parts, b_counted;
742         end if;
743         return null;
744 end;
745 ' language plpgsql;
746
747 create trigger do_backup_parts_check
748         after insert or update or delete on backups
749         for each row execute procedure backup_parts_check();
750
751 create or replace function backup_backup_parts_check() returns trigger as '
752 declare
753         b_id            integer;
754         my_part_nr      integer;
755         calc_part       integer;
756 begin
757         if (TG_OP = ''INSERT'') then
758                 -- raise notice ''trigger: % backup_id %'', TG_OP, new.backup_id;
759                 b_id = new.backup_id;
760                 my_part_nr = new.part_nr;
761                 execute ''update backups set parts = parts + 1 where id = '' || b_id;
762         elsif (TG_OP = ''DELETE'') then
763                 -- raise notice ''trigger: % backup_id %'', TG_OP, old.backup_id;
764                 b_id = old.backup_id;
765                 my_part_nr = old.part_nr;
766                 execute ''update backups set parts = parts - 1 where id = '' || b_id;
767         end if;
768         calc_part := (select count(part_nr) from backup_parts where backup_id = b_id);
769         if ( my_part_nr != calc_part ) then
770                 raise exception ''Update of backup_parts with backup_id % aborted, requested part_nr is % and calulated next is %'', b_id, my_part_nr, calc_part;
771         end if;
772         return null;
773 end;
774 ' language plpgsql;
775
776 create trigger do_backup_backup_parts_check
777         after insert or update or delete on backup_parts
778         for each row execute procedure backup_backup_parts_check();
779