| 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; |