File Coverage

blib/lib/DiaColloDB/Upgrade/Base.pm
Criterion Covered Total %
statement 18 90 20.0
branch 0 46 0.0
condition 0 44 0.0
subroutine 6 19 31.5
pod 13 13 100.0
total 37 212 17.4


line stmt bran cond sub pod time code
1             ## -*- Mode: CPerl -*-
2             ##
3             ## File: DiaColloDB::Upgrade::Base.pm
4             ## Author: Bryan Jurish <moocow@cpan.org>
5             ## Description: DiaColloDB utilities: auto-magic upgrade: base class / API
6              
7             package DiaColloDB::Upgrade::Base;
8 1     1   6 use DiaColloDB::Logger;
  1         3  
  1         30  
9 1     1   5 use DiaColloDB::Utils qw(:time);
  1         2  
  1         53  
10 1     1   223 use File::Path qw(remove_tree);
  1         2  
  1         18  
11 1     1   96 use Carp;
  1         2  
  1         20  
12 1     1   151 use version;
  1         2  
  1         7  
13 1     1   76 use strict;
  1         3  
  1         1390  
14             our @ISA = qw(DiaColloDB::Logger);
15              
16             ##==============================================================================
17             ## API
18              
19             ## $up = $CLASS_OR_OBJECT->new($dbdir?, %opts)
20             ## + create a new upgrader for local DB directory $dbdir
21             ## + if $dbdir is specified, it is stored in $up->{dbdir} and its header is loaded to $up->{hdr}
22             ## + common %opts, %$up:
23             ## (
24             ## backup=>$bool, ##-- perform auto-backup? (default=1)
25             ## keep => $bool, ##-- keep temporary files? (default=0)
26             ## timestamp=>$stamp, ##-- timestamp of this upgrade operation (default:DiaColloDB::Utils::timestamp(time))
27             ## )
28             sub new {
29 0     0 1   my $that = shift;
30 0 0         my $dbdir = scalar(@_)%2==0 ? undef : shift;
31 0   0       my $up = bless({
32             dbdir=>$dbdir,
33             backup=>1,
34             keep=>0,
35             timestamp=>DiaColloDB::Utils::timestamp(time),
36             @_,
37             }, ref($that)||$that);
38              
39             ##-- load header if available
40 0 0         $up->{hdr} = $up->dbheader($up->{dbdir}) if (defined($up->{dbdir}));
41 0           return $up;
42             }
43              
44             ## $pkg = $CLASS_OR_OBJECT->label()
45             ## + returns upgrade package name
46             sub label {
47 0   0 0 1   return ref($_[0])||$_[0];
48             }
49              
50             ## $version = $up->toversion()
51             ## + (reccommonded): returns default target version; default just returns $DiaColloDB::VERSION
52             sub toversion {
53 0     0 1   return $DiaColloDB::VERSION;
54             }
55              
56             ## $bool = $up->needed()
57             ## + returns true iff local index in $up->{dbdir} needs upgrade
58             ## + default implementation returns true iff $coldb->{version} is less than $CLASS_OR_OBJECT->toversion()
59             sub needed {
60 0     0 1   my $up = shift;
61 0           my $header = $up->dbheader();
62 0   0       return version->parse($header->{version}//'0.0.0') < version->parse($up->toversion);
63             }
64              
65             ## $bool = $up->upgrade()
66             ## + performs upgrade in-place on $up->{dbdir}
67             sub upgrade {
68 0     0 1   $_[0]->logconfess("ugprade() method not implemented");
69             }
70              
71              
72             ##==============================================================================
73             ## Backup & Revert
74              
75             ## $bool = $up->backup()
76             ## + perform backup any files we expect to change to $up->backupdir()
77             ## + subclasses should call this from $up->upgrade()
78             ## + default implementation just backs up "$dbdir/header.json", emitting a warning
79             ## if $up->{backup} is false
80             sub backup {
81 0     0 1   my $up = shift;
82              
83             ##-- were backups requested?
84 0 0         if (!$up->{backup}) {
85 0           $up->warn("backup(): backups disabled by user request");
86 0           return 1;
87             }
88              
89             ##-- backup db-header
90 0           my $dbdir = $up->{dbdir};
91 0           my $backd = $up->backupdir;
92 0           $up->info("using backup directory $backd/");
93 0           $up->info("backing up $dbdir/header.json");
94 0 0         DiaColloDB::Utils::copyto_a("$dbdir/header.json", $backd)
95             or $up->logconfess("updateHeader(): failed to backup header to $backd/: $!");
96              
97 0           return 1;
98             }
99              
100             ## $dir = $up->backupdir()
101             ## + returns name of a backup directory for this upgrade
102             sub backupdir {
103 0     0 1   my $up = shift;
104 0           my ($dbdir,$stamp) = @$up{qw(dbdir timestamp)};
105 0           $stamp =~ s/\W//g;
106 0           $stamp =~ s/T/_/;
107 0           (my $suffix = $up->label."_$stamp") =~ s/^DiaColloDB::Upgrade:://;
108 0           return "$dbdir/upgrade_${suffix}.d";
109             }
110              
111             ## $bool = $up->revert()
112             ## + rolls back a previous upgrade on $up->{dbdir}
113             ## + default implementation deletes files returned by $up->files_created()
114             ## and copies files returned by $up->files_updated() from $up->backupdir
115             sub revert {
116 0     0 1   my $up = shift;
117              
118             ##-- get backup directory
119 0           my $dbdir = $up->{dbdir};
120 0           my $backd = $up->backupdir;
121              
122             ##-- sanity check(s)
123 0 0         $up->logconfess("revert(): no DBDIR specified")
124             if (!$dbdir);
125 0 0         $up->logconfess("revert(): no backup specified")
126             if (!$backd);
127 0 0         $up->logconfess("revert(): backup directory $backd/ not found")
128             if (!-d $backd);
129 0 0 0       $up->logconfess("revert(): required method revert_created() not implemented")
130             if (($up->can('revert_created')//\&revert_created) eq \&revert_created);
131 0 0 0       $up->logconfess("revert(): required method revert_updated() not implemented")
132             if (($up->can('revert_updated')//\&revert_updated) eq \&revert_updated);
133              
134             ##-- get file-lists
135 0           my @created = $up->revert_created;
136 0           my @updated = $up->revert_updated;
137              
138             ##-- ensure backups exist
139 0           foreach (@updated) {
140 0 0         $up->logconfess("revert(): no backup found for updated file '$_'") if (!-e "$backd/$_");
141             }
142              
143             ##-- unlink updated and restored files
144 0           foreach (@created,@updated) {
145 0           $up->trace("revert(): removing $dbdir/$_");
146 0 0 0       !-e "$dbdir/$_"
147             or CORE::unlink("$dbdir/$_")
148             or $up->logconfess("revert(): failed to remove $dbdir/$_: $!");
149             }
150              
151             ##-- restore backup files
152 0           foreach (@updated) {
153 0           $up->trace("revert(): restoring $dbdir/$_");
154 0 0         DiaColloDB::Utils::copyto_a("$backd/$_", $dbdir, from=>$backd)
155             or $up->logconfess("revert(): failed to restore $dbdir/$_: $!");
156             }
157              
158             ##-- remove backup directory
159 0           $up->trace("revert(): removing backup directory $backd");
160 0 0         remove_tree($backd)
161             or $up->logconfess("revert(): failed to remove backup directory $backd: $!");
162              
163 0           return 1;
164             }
165              
166             ## @files = $up->revert_created()
167             ## + returns list of files created by this upgrade, for use with default revert() implementation
168             sub revert_created {
169 0     0 1   $_[0]->logconfess("revert_created() method not implemented");
170             }
171              
172             ## @files = $up->revert_updated()
173             ## + returns list of files updated by this upgrade, for use with default revert() implementation
174             sub revert_updated {
175 0     0 1   $_[0]->logconfess("revert_updated() method not implemented");
176             }
177              
178             ##==============================================================================
179             ## Utilities
180              
181             ## \%hdr = $CLASS_OR_OBJECT->dbheader($dbdir?)
182             ## + reads $dbdir/header.json
183             ## + default uses cached $CLASS_OR_OBJECT->{hdr} if available
184             sub dbheader {
185 0     0 1   my ($up,$dbdir) = @_;
186 0 0 0       $dbdir //= $up->{dbdir} if (ref($up));
187             return $up->{hdr}
188 0 0 0       if (ref($up) && defined($up->{hdr}) && ($up->{dbdir}//'') eq $dbdir);
      0        
      0        
189 0 0         my $hdr = DiaColloDB::Utils::loadJsonFile("$dbdir/header.json")
190             or $up->logconfess("dbheader(): failed to read header $dbdir/header.json: $!");
191 0           return $hdr;
192             }
193              
194             ## \%uinfo = $up->uinfo($dbdir?,%info)
195             ## + returns a default upgrade-info structure for %info
196             ## + conventional keys %uinfo =
197             ## (
198             ## version_from => $vfrom, ##-- source version (default='unknown')
199             ## version_to => $vto, ##-- target version (default=$CLASS_OR_OBJECT->_toversion)
200             ## timestamp => $time, ##-- timestamp (default=$up->{timestamp} || DiaColloDB::Utils::timestamp(time))
201             ## by => $who, ##-- user or script-name (default=$CLASS)
202             ## )
203             sub uinfo {
204 0     0 1   my $up = shift;
205 0 0 0       my $dbdir = ((scalar(@_)%2)==0 ? undef : shift) // $up->{hdr};
206 0 0 0       my $header = $up->{hdr} // ($dbdir ? $up->dbheader($dbdir) : {});
207             return {
208             version_from=>($header->{version} // 'unknown'),
209             version_to=>$up->toversion,
210 0   0       timestamp=>($up->{timestamp} || DiaColloDB::Utils::timestamp(time)),
      0        
211             by=>$up->label,
212             @_
213             };
214             }
215              
216             ## $bool = $up->updateHeader(\%extra_uinfo, \%extra_header_data)
217             ## + updates header $dbdir/header.json, creating backup if requested
218             sub updateHeader {
219 0     0 1   my ($up,$xinfo,$xhdr) = @_;
220 0           my $dbdir = $up->{dbdir};
221              
222             ##-- backup old header if requested
223             !$up->{backup}
224 0 0 0       or DiaColloDB::Utils::copyto_a("$dbdir/header.json", $up->backupdir)
225             or $up->logconfess("updateHeader(): failed to backup header to ".$up->backupdir.": $!");
226              
227             ##-- get upgrade info
228 0   0       my $uinfo = $up->uinfo($dbdir, %{$xinfo//{}});
  0            
229 0 0         return if (!defined($uinfo)); ##-- silent upgrade
230              
231 0           my $header = $up->dbheader($dbdir);
232 0   0       my $upgraded = ($header->{upgraded} //= []);
233 0           unshift(@$upgraded, $uinfo);
234 0 0         $header->{version} = $uinfo->{version_to} if ($uinfo->{version_to});
235 0 0         @$header{keys %$xhdr} = values %$xhdr if ($xhdr);
236 0 0         DiaColloDB::Utils::saveJsonFile($header, "$dbdir/header.json")
237             or $up->logconfess("updateHeader(): failed to save header data to $dbdir/header.json: $!");
238 0           return $up;
239             }
240              
241              
242             ##==============================================================================
243             ## Footer
244             1; ##-- be happy