archive_backup renamed to archive_backup_parts
[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 "...\n";
253
254         $dbh->commit;
255
256 }
257
258 ## delete data before inseting ##
259 if ($opt->delete) {
260         print "deleting ";
261         foreach my $table (qw(files dvds backups shares hosts)) {
262                 print "$table ";
263                 $dbh->do(qq{ DELETE FROM $table });
264         }
265         print " done...\n";
266
267         $dbh->commit;
268 }
269
270 ## insert new values ##
271
272 # get hosts
273 $hosts = $bpc->HostInfoRead();
274 my $hostID;
275 my $shareID;
276
277 my $sth;
278
279 $sth->{insert_hosts} = $dbh->prepare(qq{
280 INSERT INTO hosts (name, IP) VALUES (?,?)
281 });
282
283 $sth->{hosts_by_name} = $dbh->prepare(qq{
284 SELECT id FROM hosts WHERE name=?
285 });
286
287 $sth->{backups_count} = $dbh->prepare(qq{
288 SELECT COUNT(*)
289 FROM backups
290 WHERE hostID=? AND num=? AND shareid=?
291 });
292
293 $sth->{insert_backups} = $dbh->prepare(qq{
294 INSERT INTO backups (hostID, num, date, type, shareid, size)
295 VALUES (?,?,?,?,?,-1)
296 });
297
298 $sth->{update_backups_size} = $dbh->prepare(qq{
299 UPDATE backups SET size = ?
300 WHERE hostID = ? and num = ? and date = ? and type =? and shareid = ?
301 });
302
303 $sth->{insert_files} = $dbh->prepare(qq{
304 INSERT INTO files
305         (shareID, backupNum, name, path, date, type, size)
306         VALUES (?,?,?,?,?,?,?)
307 });
308
309 my @hosts = keys %{$hosts};
310 my $host_nr = 0;
311
312 foreach my $host_key (@hosts) {
313
314         my $hostname = $hosts->{$host_key}->{'host'} || die "can't find host for $host_key";
315
316         next if $opt->host && ! grep { m/^$hostname$/ } @{ $opt->host };
317
318         $sth->{hosts_by_name}->execute($hostname);
319
320         unless (($hostID) = $sth->{hosts_by_name}->fetchrow_array()) {
321                 $sth->{insert_hosts}->execute(
322                         $hosts->{$host_key}->{'host'},
323                         $hosts->{$host_key}->{'ip'}
324                 );
325
326                 $hostID = $dbh->last_insert_id(undef,undef,'hosts',undef);
327         }
328
329         $host_nr++;
330         # get backups for a host
331         my @backups = $bpc->BackupInfoRead($hostname);
332         my $incs = scalar @backups;
333
334         my $host_header = sprintf("host %s [%d/%d]: %d increments\n",
335                 $hosts->{$host_key}->{'host'},
336                 $host_nr,
337                 ($#hosts + 1),
338                 $incs
339         );
340         print $host_header unless $opt->quiet;
341  
342         my $inc_nr = 0;
343         $beenThere = {};
344
345         foreach my $backup (@backups) {
346
347                 $inc_nr++;
348                 last if defined $opt->max && $inc_nr > $opt->max;
349
350                 my $backupNum = $backup->{'num'};
351                 my @backupShares = ();
352
353                 my $share_header = sprintf("%-10s %2d/%-2d #%-2d %s %5s/%5s files (date: %s dur: %s)\n", 
354                         $hosts->{$host_key}->{'host'},
355                         $inc_nr, $incs, $backupNum, 
356                         $backup->{type} || '?',
357                         $backup->{nFilesNew} || '?', $backup->{nFiles} || '?',
358                         strftime($t_fmt,localtime($backup->{startTime})),
359                         fmt_time($backup->{endTime} - $backup->{startTime})
360                 );
361                 print $share_header unless $opt->quiet;
362                 status "$hostname $backupNum $share_header";
363
364                 my $files = BackupPC::View->new($bpc, $hostname, \@backups, { only_increment => 1 });
365
366                 foreach my $share ($files->shareList($backupNum)) {
367
368                         my $t = time();
369
370                         $shareID = getShareID($share, $hostID, $hostname);
371                 
372                         $sth->{backups_count}->execute($hostID, $backupNum, $shareID);
373                         my ($count) = $sth->{backups_count}->fetchrow_array();
374                         # skip if allready in database!
375                         next if ($count > 0);
376
377                         # dump host and share header for -q
378                         if ( $opt->quiet ) {
379                                 if ($host_header) {
380                                         print $host_header;
381                                         $host_header = undef;
382                                 }
383                                 print $share_header;
384                         }
385
386                         # dump some log
387                         print curr_time," ", $share;
388
389                         $sth->{insert_backups}->execute(
390                                 $hostID,
391                                 $backupNum,
392                                 $backup->{'endTime'},
393                                 substr($backup->{'type'},0,4),
394                                 $shareID,
395                         );
396
397                         my ($f, $nf, $d, $nd, $size) = recurseDir($bpc, $hostname, $files, $backupNum, $share, "", $shareID);
398
399                         eval {
400                                 $sth->{update_backups_size}->execute(
401                                         $size,
402                                         $hostID,
403                                         $backupNum,
404                                         $backup->{'endTime'},
405                                         substr($backup->{'type'},0,4),
406                                         $shareID,
407                                 );
408                                 print " commit";
409                                 $dbh->commit();
410                         };
411                         if ($@) {
412                                 print " rollback";
413                                 $dbh->rollback();
414                         }
415
416                         my $dur = (time() - $t) || 1;
417                         my $status = sprintf("%d/%d files %d/%d dirs %0.2f MB [%.2f/s dur: %s]",
418                                 $nf, $f, $nd, $d,
419                                 ($size / 1024 / 1024),
420                                 ( ($f+$d) / $dur ),
421                                 fmt_time($dur)
422                         );
423                         print " $status\n";
424                         status "$hostname $backupNum $status";
425
426                         if ($nf + $nd > 0) {
427                                 status "$hostname $backupNum full-text | indexing";
428                                 #eval { hest_update($hostID, $shareID, $backupNum) };
429                                 #warn "ERROR: $@" if $@;
430                                 hest_update($hostID, $shareID, $backupNum);
431                                 # eval breaks our re-try logic
432                         }
433                 }
434
435         }
436 }
437 undef $sth;
438 $dbh->commit();
439 $dbh->disconnect();
440
441 print "total duration: ",fmt_time(time() - $start_t),"\n";
442
443 $pidfile->remove;
444
445 sub getShareID() {
446
447         my ($share, $hostID, $hostname) = @_;
448
449         $sth->{share_id} ||= $dbh->prepare(qq{
450                 SELECT ID FROM shares WHERE hostID=? AND name=?
451         });
452
453         $sth->{share_id}->execute($hostID,$share);
454
455         my ($id) = $sth->{share_id}->fetchrow_array();
456
457         return $id if (defined($id));
458
459         $sth->{insert_share} ||= $dbh->prepare(qq{
460                 INSERT INTO shares 
461                         (hostID,name,share) 
462                 VALUES (?,?,?)
463         });
464
465         my $drop_down = $hostname . '/' . $share;
466         $drop_down =~ s#//+#/#g;
467
468         $sth->{insert_share}->execute($hostID,$share, $drop_down);
469         return $dbh->last_insert_id(undef,undef,'shares',undef);
470 }
471
472 sub found_in_db {
473
474         my @data = @_;
475         shift @data;
476
477         my ($key, $shareID,undef,$name,$path,$date,undef,$size) = @_;
478
479         return $beenThere->{$key} if (defined($beenThere->{$key}));
480
481         $sth->{file_in_db} ||= $dbh->prepare(qq{
482                 SELECT 1 FROM files
483                 WHERE shareID = ? and
484                         path = ? and 
485                         size = ? and
486                         ( date = ? or date = ? or date = ? )
487                 LIMIT 1
488         });
489
490         my @param = ($shareID,$path,$size,$date, $date-$dst_offset, $date+$dst_offset);
491         $sth->{file_in_db}->execute(@param);
492         my $rows = $sth->{file_in_db}->rows;
493         print STDERR "## found_in_db($shareID,$path,$date,$size) ",( $rows ? '+' : '-' ), join(" ",@param), "\n" if ($debug >= 3);
494
495         $beenThere->{$key}++;
496
497         $sth->{'insert_files'}->execute(@data) unless ($rows);
498         return $rows;
499 }
500
501 ####################################################
502 # recursing through filesystem structure and       #
503 # and returning flattened files list               #
504 ####################################################
505 sub recurseDir($$$$$$$$) {
506
507         my ($bpc, $hostname, $files, $backupNum, $share, $dir, $shareID) = @_;
508
509         print STDERR "\nrecurse($hostname,$backupNum,$share,$dir,$shareID)\n" if ($debug >= 1);
510
511         my ($nr_files, $new_files, $nr_dirs, $new_dirs, $size) = (0,0,0,0,0);
512
513         { # scope
514                 my @stack;
515
516                 print STDERR "# dirAttrib($backupNum, $share, $dir)\n" if ($debug >= 2);
517                 my $filesInBackup = $files->dirAttrib($backupNum, $share, $dir);
518
519                 # first, add all the entries in current directory
520                 foreach my $path_key (keys %{$filesInBackup}) {
521                         print STDERR "# file ",Dumper($filesInBackup->{$path_key}),"\n" if ($debug >= 3);
522                         my @data = (
523                                 $shareID,
524                                 $backupNum,
525                                 $path_key,
526                                 $filesInBackup->{$path_key}->{'relPath'},
527                                 $filesInBackup->{$path_key}->{'mtime'},
528                                 $filesInBackup->{$path_key}->{'type'},
529                                 $filesInBackup->{$path_key}->{'size'}
530                         );
531
532                         my $key = join(" ", (
533                                 $shareID,
534                                 $dir,
535                                 $path_key,
536                                 $filesInBackup->{$path_key}->{'mtime'},
537                                 $filesInBackup->{$path_key}->{'size'}
538                         ));
539
540                         my $key_dst_prev = join(" ", (
541                                 $shareID,
542                                 $dir,
543                                 $path_key,
544                                 $filesInBackup->{$path_key}->{'mtime'} - $dst_offset,
545                                 $filesInBackup->{$path_key}->{'size'}
546                         ));
547
548                         my $key_dst_next = join(" ", (
549                                 $shareID,
550                                 $dir,
551                                 $path_key,
552                                 $filesInBackup->{$path_key}->{'mtime'} + $dst_offset,
553                                 $filesInBackup->{$path_key}->{'size'}
554                         ));
555
556                         my $found;
557                         if (
558                                 ! defined($beenThere->{$key}) &&
559                                 ! defined($beenThere->{$key_dst_prev}) &&
560                                 ! defined($beenThere->{$key_dst_next}) &&
561                                 ! ($found = found_in_db($key, @data))
562                         ) {
563                                 print STDERR "# key: $key [", $beenThere->{$key},"]" if ($debug >= 2);
564
565                                 if ($filesInBackup->{$path_key}->{'type'} == BPC_FTYPE_DIR) {
566                                         $new_dirs++ unless ($found);
567                                         print STDERR " dir\n" if ($debug >= 2);
568                                 } else {
569                                         $new_files++ unless ($found);
570                                         print STDERR " file\n" if ($debug >= 2);
571                                 }
572                                 $size += $filesInBackup->{$path_key}->{'size'} || 0;
573                         }
574
575                         if ($filesInBackup->{$path_key}->{'type'} == BPC_FTYPE_DIR) {
576                                 $nr_dirs++;
577
578                                 my $full_path = $dir . '/' . $path_key;
579                                 push @stack, $full_path;
580                                 print STDERR "### store to stack: $full_path\n" if ($debug >= 3);
581
582 #                               my ($f,$nf,$d,$nd) = recurseDir($bpc, $hostname, $backups, $backupNum, $share, $path_key, $shareID) unless ($beenThere->{$key});
583 #
584 #                               $nr_files += $f;
585 #                               $new_files += $nf;
586 #                               $nr_dirs += $d;
587 #                               $new_dirs += $nd;
588
589                         } else {
590                                 $nr_files++;
591                         }
592                 }
593
594                 print STDERR "## STACK ",join(", ", @stack),"\n" if ($debug >= 2);
595
596                 while ( my $dir = shift @stack ) {
597                         my ($f,$nf,$d,$nd, $s) = recurseDir($bpc, $hostname, $files, $backupNum, $share, $dir, $shareID);
598                         print STDERR "# $dir f: $f nf: $nf d: $d nd: $nd\n" if ($debug >= 1);
599                         $nr_files += $f;
600                         $new_files += $nf;
601                         $nr_dirs += $d;
602                         $new_dirs += $nd;
603                         $size += $s;
604                 }
605         }
606
607         return ($nr_files, $new_files, $nr_dirs, $new_dirs, $size);
608 }
609
610 __DATA__
611
612 create table hosts (
613         ID      SERIAL          PRIMARY KEY,
614         name    VARCHAR(30)     NOT NULL,
615         IP      VARCHAR(15)
616 );            
617
618 create table shares (
619         ID      SERIAL          PRIMARY KEY,
620         hostID  INTEGER         NOT NULL references hosts(id),
621         name    VARCHAR(30)     NOT NULL,
622         share   VARCHAR(200)    NOT NULL
623 );            
624
625 create table dvds (
626         ID      SERIAL          PRIMARY KEY, 
627         num     INTEGER         NOT NULL,
628         name    VARCHAR(255)    NOT NULL,
629         mjesto  VARCHAR(255)
630 );
631
632 create table backups (
633         id      serial,
634         hostID  INTEGER         NOT NULL references hosts(id),
635         num     INTEGER         NOT NULL,
636         date    integer         NOT NULL, 
637         type    CHAR(4)         not null,
638         shareID integer         not null references shares(id),
639         size    bigint          not null,
640         inc_size bigint         not null default -1,
641         inc_deleted boolean     default false,
642         parts   integer         not null default 0,
643         PRIMARY KEY(id)
644 );            
645
646 create table backup_parts (
647         id serial,
648         backup_id int references backups(id),
649         part_nr int not null check (part_nr > 0),
650         tar_size bigint not null check (tar_size > 0),
651         size bigint not null check (size > 0),
652         md5 text not null,
653         items int not null check (items > 0),
654         date timestamp default now(),
655         filename text not null,
656         primary key(id)
657 );
658
659 create table files (
660         ID              SERIAL,
661         shareID         INTEGER NOT NULL references shares(id),
662         backupNum       INTEGER NOT NULL,
663         name            VARCHAR(255) NOT NULL,
664         path            VARCHAR(255) NOT NULL,
665         date            integer NOT NULL,
666         type            INTEGER NOT NULL,
667         size            bigint  NOT NULL,
668         primary key(id)
669 );
670
671 create sequence dvd_nr;
672
673 create table archive (
674         id              serial,
675         dvd_nr          int not null,
676         total_size      bigint default -1,
677         note            text,
678         username        varchar(20) not null,
679         date            timestamp default now(),
680         primary key(id)
681 );      
682
683 create table archive_parts (
684         archive_id      int not null references archive(id) on delete cascade,
685         backup_part_id  int not null references backup_parts(id),
686         primary key(archive_id, backup_part_id)
687 );
688
689 create table archive_burned (
690         archive_id      int references archive(id),
691         date            timestamp default now(),
692         part            int not null default 1,
693         copy            int not null default 1,
694         iso_size bigint default -1
695 );
696
697 -- report backups and corresponding dvd
698
699 --create view backups_on_dvds as
700 --select
701 --      backups.id as id,
702 --      hosts.name || ':' || shares.name as share,
703 --      backups.num as num,
704 --      backups.type as type,
705 --      abstime(backups.date) as backup_date,
706 --      backups.size as size,
707 --      backups.inc_size as gzip_size,
708 --      archive.id as archive_id,
709 --      archive.dvd_nr
710 --from backups
711 --join shares on backups.shareid=shares.id
712 --join hosts on shares.hostid = hosts.id
713 --left outer join archive_backup_parts on backups.id = archive_backup_parts.backup_id
714 --left outer join archive on archive_backup_parts.archive_id = archive.id
715 --where backups.parts > 0 and size > 0
716 --order by backups.date
717 --;
718
719
720 -- used by BackupPC_ASA_BurnArchiveMedia
721 CREATE VIEW archive_backup_parts AS
722 SELECT
723         backup_parts.backup_id,
724         archive_id,
725         dvd_nr,
726         backup_part_id,
727         hosts.name as host,
728         shares.name as share,
729         backups.num as num,
730         backups.date as date,
731         backup_parts.part_nr as part_nr,
732         backups.parts as parts,
733         backup_parts.size as size,
734         backup_parts.md5 as md5,
735         backup_parts.items,
736         backup_parts.filename
737 FROM backup_parts
738 JOIN archive_parts ON backup_parts.id = backup_part_id
739 JOIN archive       ON archive_id = archive.id
740 JOIN backups       ON backup_id = backups.id
741 JOIN hosts         ON hostid = hosts.id
742 JOIN shares        ON shareid = shares.id
743 ORDER BY backup_id, part_nr
744 ;
745
746
747 create or replace function backup_parts_check() returns trigger as '
748 declare
749         b_parts integer;
750         b_counted integer;
751         b_id    integer;
752 begin
753         -- raise notice ''old/new parts %/% backup_id %/%'', old.parts, new.parts, old.id, new.id;
754         if (TG_OP=''UPDATE'') then
755                 b_id := new.id;
756                 b_parts := new.parts;
757         elsif (TG_OP = ''INSERT'') then
758                 b_id := new.id;
759                 b_parts := new.parts;
760         end if;
761         b_counted := (select count(*) from backup_parts where backup_id = b_id);
762         -- raise notice ''backup % parts %'', b_id, b_parts;
763         if ( b_parts != b_counted ) then
764                 raise exception ''Update of backup % aborted, requested % parts and there are really % parts'', b_id, b_parts, b_counted;
765         end if;
766         return null;
767 end;
768 ' language plpgsql;
769
770 create trigger do_backup_parts_check
771         after insert or update or delete on backups
772         for each row execute procedure backup_parts_check();
773
774 create or replace function backup_backup_parts_check() returns trigger as '
775 declare
776         b_id            integer;
777         my_part_nr      integer;
778         calc_part       integer;
779 begin
780         if (TG_OP = ''INSERT'') then
781                 -- raise notice ''trigger: % backup_id %'', TG_OP, new.backup_id;
782                 b_id = new.backup_id;
783                 my_part_nr = new.part_nr;
784                 execute ''update backups set parts = parts + 1 where id = '' || b_id;
785         elsif (TG_OP = ''DELETE'') then
786                 -- raise notice ''trigger: % backup_id %'', TG_OP, old.backup_id;
787                 b_id = old.backup_id;
788                 my_part_nr = old.part_nr;
789                 execute ''update backups set parts = parts - 1 where id = '' || b_id;
790         end if;
791         calc_part := (select count(part_nr) from backup_parts where backup_id = b_id);
792         if ( my_part_nr != calc_part ) then
793                 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;
794         end if;
795         return null;
796 end;
797 ' language plpgsql;
798
799 create trigger do_backup_backup_parts_check
800         after insert or update or delete on backup_parts
801         for each row execute procedure backup_backup_parts_check();
802