File Coverage

blib/lib/App/Yabsm/Backup/Local.pm
Criterion Covered Total %
statement 26 95 27.3
branch 0 26 0.0
condition 0 9 0.0
subroutine 9 13 69.2
pod 0 4 0.0
total 35 147 23.8


line stmt bran cond sub pod time code
1             # Author: Nicholas Hubbard
2             # WWW: https://github.com/NicholasBHubbard/yabsm
3             # License: MIT
4              
5             # Provides the &do_local_backup subroutine, which performs a single local_backup
6             # This is a top-level subroutine that is directly scheduled to be run by the
7             # daemon.
8              
9 1     1   425 use strict;
  1         5  
  1         29  
10 1     1   5 use warnings;
  1         1  
  1         18  
11 1     1   14 use v5.16.3;
  1         3  
12              
13             package App::Yabsm::Backup::Local;
14              
15 1         70 use App::Yabsm::Backup::Generic qw(take_tmp_snapshot
16             take_bootstrap_snapshot
17             the_local_bootstrap_snapshot
18             bootstrap_lock_file
19             create_bootstrap_lock_file
20 1     1   335 );
  1         3  
21 1     1   6 use App::Yabsm::Snapshot qw(delete_snapshot sort_snapshots is_snapshot_name);
  1         2  
  1         47  
22 1     1   5 use App::Yabsm::Tools qw( :ALL );
  1         3  
  1         186  
23 1     1   20 use App::Yabsm::Config::Query qw( :ALL );
  1         3  
  1         384  
24              
25 1     1   7 use File::Basename qw(basename);
  1         2  
  1         37  
26              
27 1     1   7 use Exporter qw(import);
  1         2  
  1         774  
28             our @EXPORT_OK = qw(do_local_backup
29             do_local_backup_bootstrap
30             maybe_do_local_backup_bootstrap
31             the_remote_bootstrap_snapshot
32             );
33              
34             ####################################
35             # SUBROUTINES #
36             ####################################
37              
38             sub do_local_backup {
39              
40             # Perform a $tframe local_backup for $local_backup.
41              
42 0     0 0   arg_count_or_die(3, 3, @_);
43              
44 0           my $local_backup = shift;
45 0           my $tframe = shift;
46 0           my $config_ref = shift;
47              
48             # We can't perform a backup if the bootstrap process is currently being
49             # performed.
50 0 0         if (bootstrap_lock_file($local_backup, 'local', $config_ref)) {
51 0           return undef;
52             }
53              
54 0           my $backup_dir = local_backup_dir($local_backup, $tframe, $config_ref);
55              
56 0 0 0       unless (is_btrfs_dir($backup_dir) && -r $backup_dir) {
57 0           my $username = getpwuid $<;
58 0           die "yabsm: error: '$backup_dir' is not a directory residing on a btrfs filesystem that is readable by user '$username'\n";
59             }
60              
61 0           my $tmp_snapshot = take_tmp_snapshot($local_backup, 'local', $tframe, $config_ref);
62 0           my $bootstrap_snapshot = maybe_do_local_backup_bootstrap($local_backup, $config_ref);
63              
64 0           system_or_die("sudo -n btrfs send -p '$bootstrap_snapshot' '$tmp_snapshot' | sudo -n btrfs receive '$backup_dir' >/dev/null 2>&1");
65              
66             # @backups is sorted from newest to oldest
67 0           my @backups = sort_snapshots(do {
68 0 0         opendir my $dh, $backup_dir or confess("yabsm: internal error: cannot opendir '$backup_dir'");
69 0           my @backups = grep { is_snapshot_name($_) } readdir($dh);
  0            
70 0           closedir $dh;
71 0           map { $_ = "$backup_dir/$_" } @backups;
  0            
72 0           \@backups;
73             });
74 0           my $num_backups = scalar @backups;
75 0           my $to_keep = local_backup_timeframe_keep($local_backup, $tframe, $config_ref);
76              
77             # There is 1 more backup than should be kept because we just performed a
78             # backup.
79 0 0         if ($num_backups == $to_keep + 1) {
    0          
80 0           my $oldest = pop @backups;
81 0           delete_snapshot($oldest);
82             }
83             # We have not reached the backup quota yet so we don't delete anything.
84             elsif ($num_backups <= $to_keep) {
85             ;
86             }
87             # User changed their settings to keep less backups than they were keeping
88             # prior.
89             else {
90 0           for (; $num_backups > $to_keep; $num_backups--) {
91 0           my $oldest = pop @backups;
92 0           delete_snapshot($oldest);
93             }
94             }
95              
96 0           return "$backup_dir/" . basename($tmp_snapshot);
97             }
98              
99             sub do_local_backup_bootstrap {
100              
101             # Perform the bootstrap phase of an incremental backup for $local_backup.
102              
103 0     0 0   arg_count_or_die(2, 2, @_);
104              
105 0           my $local_backup = shift;
106 0           my $config_ref = shift;
107              
108 0 0         if (bootstrap_lock_file($local_backup, 'local', $config_ref)) {
109 0           return undef;
110             }
111              
112             # The lock file will be deleted when $lock_fh goes out of scope (uses File::Temp).
113 0           my $lock_fh = create_bootstrap_lock_file($local_backup, 'local', $config_ref);
114              
115 0 0         if (my $local_boot_snap = the_local_bootstrap_snapshot($local_backup, 'local', $config_ref)) {
116 0           delete_snapshot($local_boot_snap);
117             }
118 0 0         if (my $remote_boot_snap = the_remote_bootstrap_snapshot($local_backup, $config_ref)) {
119 0           delete_snapshot($remote_boot_snap);
120             }
121              
122 0           my $local_boot_snap = take_bootstrap_snapshot($local_backup, 'local', $config_ref);
123              
124 0           my $backup_dir_base = local_backup_dir($local_backup, undef, $config_ref);
125              
126 0           system_or_die("sudo -n btrfs send '$local_boot_snap' | sudo -n btrfs receive '$backup_dir_base' >/dev/null 2>&1");
127              
128 0           return $local_boot_snap;
129             }
130              
131             sub maybe_do_local_backup_bootstrap {
132              
133             # Like &do_local_backup_bootstrap but only perform the bootstrap if it hasn't
134             # been performed yet.
135              
136 0     0 0   arg_count_or_die(2, 2, @_);
137              
138 0           my $local_backup = shift;
139 0           my $config_ref = shift;
140              
141 0           my $local_boot_snap = the_local_bootstrap_snapshot($local_backup, 'local', $config_ref);
142 0           my $remote_boot_snap = the_remote_bootstrap_snapshot($local_backup, $config_ref);
143              
144 0 0 0       unless ($local_boot_snap && $remote_boot_snap) {
145 0           $local_boot_snap = do_local_backup_bootstrap($local_backup, $config_ref);
146             }
147              
148 0           return $local_boot_snap;
149             }
150              
151             sub the_remote_bootstrap_snapshot {
152              
153             # Return the remote bootstrap snapshot for $local_backup if it exists and
154             # return undef otherwise. Die if we find multiple bootstrap snapshots.
155              
156 0     0 0   arg_count_or_die(2, 2, @_);
157              
158 0           my $local_backup = shift;
159 0           my $config_ref = shift;
160              
161 0           my $backup_dir_base = local_backup_dir($local_backup, undef, $config_ref);
162              
163 0 0 0       unless (-d $backup_dir_base && -r $backup_dir_base) {
164 0           my $username = getpwuid $<;
165 0           die "yabsm: error: no directory '$backup_dir_base' that is readable by user '$username'\n";
166             }
167              
168 0 0         opendir my $dh, $backup_dir_base or confess("yabsm: internal error: cannot opendir '$backup_dir_base'");
169 0           my @boot_snaps = grep { is_snapshot_name($_, ONLY_BOOTSTRAP => 1) } readdir($dh);
  0            
170 0           closedir $dh;
171              
172 0           map { $_ = "$backup_dir_base/$_" } @boot_snaps;
  0            
173              
174 0 0         if (0 == @boot_snaps) {
    0          
175 0           return undef;
176             }
177             elsif (1 == @boot_snaps) {
178 0           return $boot_snaps[0];
179             }
180             else {
181 0           die "yabsm: error: found multiple remote bootstrap snapshots for local_backup '$local_backup' in '$backup_dir_base'\n";
182             }
183             }
184              
185             1;