File Coverage

blib/lib/App/Yabsm/Backup/Generic.pm
Criterion Covered Total %
statement 32 143 22.3
branch 0 40 0.0
condition 0 6 0.0
subroutine 11 20 55.0
pod 0 9 0.0
total 43 218 19.7


line stmt bran cond sub pod time code
1             # Author: Nicholas Hubbard
2             # WWW: https://github.com/NicholasBHubbard/yabsm
3             # License: MIT
4              
5             # Functions needed for both SSH and local backups.
6              
7 4     4   612 use strict;
  4         13  
  4         102  
8 4     4   18 use warnings;
  4         7  
  4         86  
9 4     4   34 use v5.16.3;
  4         11  
10              
11             package App::Yabsm::Backup::Generic;
12              
13 4     4   745 use App::Yabsm::Tools qw( :ALL );
  4         9  
  4         667  
14 4     4   1120 use App::Yabsm::Config::Query qw( :ALL );
  4         22  
  4         1589  
15              
16 4         253 use App::Yabsm::Snapshot qw(take_snapshot
17             delete_snapshot
18             current_time_snapshot_name
19             is_snapshot_name
20 4     4   986 );
  4         10  
21              
22 4     4   24 use Carp q(confess);
  4         6  
  4         161  
23 4     4   2639 use File::Temp;
  4         66463  
  4         314  
24 4     4   28 use File::Basename qw(basename);
  4         8  
  4         147  
25 4     4   22 use Feature::Compat::Try;
  4         7  
  4         34  
26              
27 4     4   374 use Exporter 'import';
  4         9  
  4         5767  
28             our @EXPORT_OK = qw(take_tmp_snapshot
29             tmp_snapshot_dir
30             take_bootstrap_snapshot
31             maybe_take_bootstrap_snapshot
32             bootstrap_snapshot_dir
33             the_local_bootstrap_snapshot
34             bootstrap_lock_file
35             create_bootstrap_lock_file
36             is_backup_type_or_die
37             );
38              
39             ####################################
40             # SUBROUTINES #
41             ####################################
42              
43             sub take_tmp_snapshot {
44              
45             # Take a tmp snapshot for $backup. The tmp snapshot is the snapshot that is
46             # actually replicated in an incremental backup with 'btrfs send -p'.
47              
48 0     0 0   arg_count_or_die(4, 4, @_);
49              
50 0           my $backup = shift;
51 0           my $backup_type = shift;
52 0           my $tframe = shift;
53 0           my $config_ref = shift;
54              
55 0           my $tmp_snapshot_dir = tmp_snapshot_dir(
56             $backup,
57             $backup_type,
58             $tframe,
59             $config_ref,
60             DIE_UNLESS_EXISTS => 1
61             );
62              
63             # Remove any old tmp snapshots that were never deleted because of a failed
64             # incremental backup attempt.
65 0 0         opendir my $dh, $tmp_snapshot_dir or confess("yabsm: internal error: cannot opendir '$tmp_snapshot_dir'");
66 0           my @tmp_snapshots = grep { is_snapshot_name($_, ALLOW_BOOTSTRAP => 0) } readdir($dh);
  0            
67 0           closedir $dh;
68 0           map { $_ = "$tmp_snapshot_dir/$_" } @tmp_snapshots;
  0            
69              
70             # The old tmp snapshot may be in the process of being sent which will cause
71             # the deletion to fail. In this case we can just ignore the failure.
72 0           for (@tmp_snapshots) {
73             try {
74             delete_snapshot($_);
75             }
76 0           catch ($e) {
77             ; # do nothing
78             }
79             }
80              
81 0           my $mountpoint;
82              
83 0 0         if ($backup_type eq 'ssh') {
    0          
84 0           $mountpoint = ssh_backup_mountpoint($backup, $config_ref);
85             }
86             elsif ($backup_type eq 'local') {
87 0           $mountpoint = local_backup_mountpoint($backup, $config_ref);
88             }
89 0           else { is_backup_type_or_die($backup_type) }
90              
91 0           return take_snapshot($mountpoint, $tmp_snapshot_dir);
92             }
93              
94             sub tmp_snapshot_dir {
95              
96             # Return path to $backup's tmp snapshot directory. If passed
97             # 'DIE_UNLESS_EXISTS => 1' # then die unless the directory exists and is
98             # readable+writable for the current user.
99              
100 0     0 0   arg_count_or_die(4, 6, @_);
101              
102 0           my $backup = shift;
103 0           my $backup_type = shift;
104 0           my $tframe = shift;
105 0           my $config_ref = shift;
106 0           my %die_unless_exists = (DIE_UNLESS_EXISTS => 0, @_);
107              
108 0           is_timeframe_or_die($tframe);
109              
110 0 0         if ($backup_type eq 'ssh') {
    0          
111 0           ssh_backup_exists_or_die($backup, $config_ref);
112             }
113             elsif ($backup_type eq 'ssh') {
114 0           local_backup_exists_or_die($backup, $config_ref);
115             }
116 0           else { is_backup_type_or_die($backup_type) }
117              
118 0           my $tmp_snapshot_dir = yabsm_dir($config_ref) . "/.yabsm-var/${backup_type}_backups/$backup/tmp-snapshot/$tframe";
119              
120 0 0         if ($die_unless_exists{DIE_UNLESS_EXISTS}) {
121 0 0 0       unless (-d $tmp_snapshot_dir && -r $tmp_snapshot_dir) {
122 0           my $username = getpwuid $<;
123 0           die "yabsm: error: no directory '$tmp_snapshot_dir' that is readable by user '$username'. This directory should have been initialized when the daemon started.\n";
124             }
125             }
126              
127 0           return $tmp_snapshot_dir;
128             }
129              
130             sub take_bootstrap_snapshot {
131              
132             # Take a btrfs bootstrap snapshot of $backup and return its path.
133             # If there is already a bootstrap snapshot for $backup then delete
134             # it and take a new one.
135              
136 0     0 0   arg_count_or_die(3, 3, @_);
137              
138 0           my $backup = shift;
139 0           my $backup_type = shift;
140 0           my $config_ref = shift;
141              
142 0           my $mountpoint;
143              
144 0 0         if ($backup_type eq 'ssh') {
    0          
145 0           $mountpoint = ssh_backup_mountpoint($backup, $config_ref);
146             }
147             elsif ($backup_type eq 'local') {
148 0           $mountpoint = local_backup_mountpoint($backup, $config_ref);
149             }
150 0           else { is_backup_type_or_die($backup_type) }
151              
152 0 0         if (my $bootstrap_snapshot = the_local_bootstrap_snapshot($backup, $backup_type, $config_ref)) {
153 0           delete_snapshot($bootstrap_snapshot);
154             }
155              
156 0           my $bootstrap_dir = bootstrap_snapshot_dir($backup, $backup_type, $config_ref, DIE_UNLESS_EXISTS => 1);
157 0           my $snapshot_name = '.BOOTSTRAP-' . current_time_snapshot_name();
158              
159 0           return take_snapshot($mountpoint, $bootstrap_dir, $snapshot_name);
160             }
161              
162             sub maybe_take_bootstrap_snapshot {
163              
164             # If $backup does not already have a bootstrap snapshot then take
165             # a bootstrap snapshot and return its path. Otherwise return the
166             # path of the existing bootstrap snapshot.
167              
168 0     0 0   arg_count_or_die(3, 3, @_);
169              
170 0           my $backup = shift;
171 0           my $backup_type = shift;
172 0           my $config_ref = shift;
173              
174 0 0         if (my $boot_snap = the_local_bootstrap_snapshot($backup, $backup_type, $config_ref)) {
175 0           return $boot_snap;
176             }
177              
178 0           return take_bootstrap_snapshot($backup, $backup_type, $config_ref);
179             }
180              
181             sub bootstrap_snapshot_dir {
182              
183             # Return the path to $ssh_backup's bootstrap snapshot directory.
184             # Logdie if the bootstrap snapshot directory does not exist.
185              
186 0     0 0   arg_count_or_die(3, 5, @_);
187              
188 0           my $backup = shift;
189 0           my $backup_type = shift;
190 0           my $config_ref = shift;
191 0           my %or_die = (DIE_UNLESS_EXISTS => 0, @_);
192              
193 0           is_backup_type_or_die($backup_type);
194              
195 0 0         if ($backup_type eq 'ssh') {
196 0           ssh_backup_exists_or_die($backup, $config_ref);
197             }
198 0 0         if ($backup_type eq 'local') {
199 0           local_backup_exists_or_die($backup, $config_ref);
200             }
201              
202 0           my $bootstrap_dir = yabsm_dir($config_ref) . "/.yabsm-var/${backup_type}_backups/$backup/bootstrap-snapshot";
203              
204 0 0         if ($or_die{DIE_UNLESS_EXISTS}) {
205 0 0 0       unless (-d $bootstrap_dir && -r $bootstrap_dir) {
206 0           my $username = getpwuid $<;
207 0           die "yabsm: error: no directory '$bootstrap_dir' that is readable by user '$username'. This directory should have been initialized when the daemon started.\n";
208             }
209             }
210              
211 0           return $bootstrap_dir;
212             }
213              
214             sub the_local_bootstrap_snapshot {
215              
216             # Return the local bootstrap snapshot for $backup if it exists and return
217             # undef otherwise. Die if there are multiple bootstrap snapshots.
218              
219 0     0 0   arg_count_or_die(3, 3, @_);
220              
221 0           my $backup = shift;
222 0           my $backup_type = shift;
223 0           my $config_ref = shift;
224              
225 0           my $bootstrap_dir = bootstrap_snapshot_dir(
226             $backup,
227             $backup_type,
228             $config_ref,
229             DIE_UNLESS_EXISTS => 1
230             );
231              
232 0 0         opendir my $dh, $bootstrap_dir or confess "yabsm: internal error: cannot opendir '$bootstrap_dir'";
233 0           my @boot_snaps = grep { is_snapshot_name($_, ONLY_BOOTSTRAP => 1) } readdir($dh);
  0            
234 0           map { $_ = "$bootstrap_dir/$_" } @boot_snaps;
  0            
235 0           close $dh;
236              
237 0 0         if (0 == @boot_snaps) {
    0          
238 0           return undef;
239             }
240             elsif (1 == @boot_snaps) {
241 0           return $boot_snaps[0];
242             }
243             else {
244 0           die "yabsm: error: found multiple local bootstrap snapshots for ${backup_type}_backup '$backup' in '$bootstrap_dir'\n";
245             }
246             }
247              
248             sub bootstrap_lock_file {
249              
250             # Return the path to the BOOTSTRAP-LOCK for $backup if it exists and return
251             # undef otherwise.
252              
253 0     0 0   arg_count_or_die(3, 3, @_);
254              
255 0           my $backup = shift;
256 0           my $backup_type = shift;
257 0           my $config_ref = shift;
258              
259 0           my $rx = qr/yabsm-${backup_type}_backup_${backup}_BOOTSTRAP-LOCK/;
260              
261 0           my $lock_file = [ grep /$rx/, glob('/tmp/*') ]->[0];
262              
263 0           return $lock_file;
264             }
265              
266             sub create_bootstrap_lock_file {
267              
268             # Create the bootstrap lock file for $backup. This function should be called
269             # when performing the bootstrap phase of an incremental backup after checking
270             # to make sure a lock file doesn't already exist. If a lock file already
271             # exists we die, so check beforehand!
272              
273 0     0 0   arg_count_or_die(3, 3, @_);
274              
275 0           my $backup = shift;
276 0           my $backup_type = shift;
277 0           my $config_ref = shift;
278              
279 0           backup_exists_or_die($backup, $config_ref);
280 0           is_backup_type_or_die($backup_type);
281              
282 0 0         if (my $existing_lock_file = bootstrap_lock_file($backup, $backup_type, $config_ref)) {
283 0           die "yabsm: error: ${backup_type}_backup '$backup' is already locked out of performing a bootstrap. This was determined by the existence of '$existing_lock_file'\n";
284             }
285              
286             # The file will be deleted when $tmp_fh is destroyed.
287 0           my $tmp_fh = File::Temp->new(
288             TEMPLATE => "yabsm-${backup_type}_backup_${backup}_BOOTSTRAP-LOCKXXXX",
289             DIR => '/tmp',
290             UNLINK => 1
291             );
292              
293 0           return $tmp_fh;
294             }
295              
296             sub is_backup_type_or_die {
297              
298             # Logdie unless $backup_type equals 'ssh' or 'local'.
299              
300 0     0 0   arg_count_or_die(1, 1, @_);
301              
302 0           my $backup_type = shift;
303              
304 0 0         unless ( $backup_type =~ /^(ssh|local)$/ ) {
305 0           confess("yabsm: internal error: '$backup_type' is not 'ssh' or 'local'");
306             }
307              
308 0           return 1;
309             }
310              
311             1;