File Coverage

blib/lib/App/Yabsm/Snapshot.pm
Criterion Covered Total %
statement 93 118 78.8
branch 16 20 80.0
condition 0 6 0.0
subroutine 21 25 84.0
pod 0 16 0.0
total 130 185 70.2


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 functions for taking and cycling snapshots based off of
6             # the user config.
7             #
8             # See t/Snapshot.t for this libraries tests.
9              
10 6     6   513 use strict;
  6         16  
  6         168  
11 6     6   26 use warnings;
  6         7  
  6         144  
12 6     6   56 use v5.16.3;
  6         18  
13              
14             package App::Yabsm::Snapshot;
15              
16 6     6   376 use App::Yabsm::Tools qw( :ALL );
  6         10  
  6         980  
17 6     6   480 use App::Yabsm::Config::Query qw( :ALL );
  6         16  
  6         2461  
18              
19 6     6   49 use Carp qw(confess);
  6         9  
  6         283  
20 6     6   31 use File::Basename qw(basename);
  6         8  
  6         288  
21 6     6   34 use Time::Piece;
  6         9  
  6         37  
22              
23 6     6   417 use Exporter qw(import);
  6         9  
  6         7036  
24             our @EXPORT_OK = qw(take_snapshot
25             delete_snapshot
26             is_snapshot_name
27             is_snapshot_name_or_die
28             is_bootstrap_snapshot_name
29             is_yabsm_snapshot
30             is_yabsm_snapshot_or_die
31             snapshot_name_nums
32             nums_to_snapshot_name
33             current_time_snapshot_name
34             sort_snapshots
35             cmp_snapshots
36             snapshots_eq
37             snapshot_newer
38             snapshot_older
39             snapshot_newer_or_eq
40             snapshot_older_or_eq
41             );
42              
43             ####################################
44             # SUBROUTINES #
45             ####################################
46              
47             sub take_snapshot {
48              
49             # This is the lowest level function for taking a snapshot. Given the path to
50             # a btrfs subvolume ($subvolume) and the destination path for the snapshot
51             # ($dest), takes a snapshot of $subvolume, names it after the current time
52             # (or an inputted name), and places it in $dest. Returns the path of the new
53             # snapshot.
54             #
55             # Performs sanity checking and dies unless $subvolume is a btrfs subvolume,
56             # $dest is a directory residing on a btrfs filesystem, and the current user
57             # can call the btrfs program using sudo without the need for password
58             # authentication.
59              
60 0     0 0 0 arg_count_or_die(2, 3, @_);
61              
62 0         0 my $subvolume = shift;
63 0         0 my $dest = shift;
64 0   0     0 my $snapshot_name = shift // current_time_snapshot_name();
65              
66 0         0 is_btrfs_subvolume_or_die($subvolume);
67 0         0 is_btrfs_dir_or_die($dest);
68 0         0 is_snapshot_name_or_die($snapshot_name, ALLOW_BOOTSTRAP => 1);
69 0         0 have_sudo_access_to_btrfs_or_die();
70              
71 0         0 my $snapshot = "$dest/" . $snapshot_name;
72              
73 0         0 system_or_die('sudo', '-n', 'btrfs', 'subvolume', 'snapshot', '-r', $subvolume, $snapshot);
74              
75 0         0 return $snapshot;
76             }
77              
78             sub delete_snapshot {
79              
80             # This is the lowest level function for deleting a snapshot. Takes the path
81             # to a yabsm snapshot ($snapshot), deletes it and returns it back.
82             #
83             # Performs sanity checking and dies unless $snapshot is a yabsm snapshot,
84             # and the current user can call the btrfs program with sudo without the need
85             # for password authentication.
86              
87 0     0 0 0 arg_count_or_die(1, 1, @_);
88              
89 0         0 my $snapshot = shift;
90              
91 0         0 is_yabsm_snapshot_or_die($snapshot);
92 0         0 have_sudo_access_to_btrfs_or_die();
93              
94 0         0 system_or_die('sudo', '-n', 'btrfs', 'subvolume', 'delete', $snapshot);
95              
96 0         0 return $snapshot;
97             }
98              
99             sub is_snapshot_name {
100              
101             # Return 1 if passed a valid yabsm snapshot name and return 0 otherwise. Does
102             # checking to ensure that the denoted date is a valid date.
103             #
104             # Optionally pass 'ALLOW_BOOTSTRAP => 1' to accept bootstrap snapshot names
105             # and 'ONLY_BOOTSTRAP => 1' to only accept bootstrap snapshot names.
106             #
107             # It is important to note that this function rejects directory paths even if
108             # their basename is a valid snapshot name.
109              
110 177     177 0 640 arg_count_or_die(1, 5, @_);
111              
112 177         209 my $snapshot_name = shift;
113 177         418 my %opts = (ALLOW_BOOTSTRAP => 0, ONLY_BOOTSTRAP => 0, @_);
114              
115 177         229 my $rx = do {
116 177         186 my $base = 'yabsm-(\d{4})_(\d{2})_(\d{2})_(\d{2}):(\d{2})';
117 177         181 my $prefix = '';
118 177 100       290 if ($opts{ALLOW_BOOTSTRAP}) {
119 163         185 $prefix = '(?:\.BOOTSTRAP-)?';
120             }
121 177 100       256 if ($opts{ONLY_BOOTSTRAP}) {
122 3         4 $prefix = '(?:\.BOOTSTRAP-)';
123             }
124 177         836 qr/^$prefix$base$/;
125             };
126              
127 177 100       1132 return 0 unless my @date_nums = $snapshot_name =~ $rx;
128              
129 166 100       358 return 0 unless nums_denote_valid_date(@date_nums);
130              
131 163         493 return 1;
132             }
133              
134             sub is_snapshot_name_or_die {
135              
136             # Wrapper around &is_snapshot_name that will Carp::confess if it returns
137             # false.
138              
139 164     164 0 3594 arg_count_or_die(1, 5, @_);
140              
141 164 100       225 unless (is_snapshot_name(@_)) {
142 6         60 confess q(yabsm: internal error: ').shift(@_).q(' is not a valid yabsm snapshot name);
143             }
144              
145 158         199 return 1;
146             }
147              
148             sub is_yabsm_snapshot {
149              
150             # Return 1 if $snapshot is a yabsm snapshot (including bootstrap) and return
151             # 0 otherwise.
152              
153 0     0 0 0 my $snapshot = shift;
154              
155 0   0     0 return is_btrfs_subvolume($snapshot) && is_snapshot_name(basename($snapshot), ALLOW_BOOTSTRAP => 1);
156             }
157              
158             sub is_yabsm_snapshot_or_die {
159              
160             # Wrapper around is_yabsm_snapshot_name() that Carp::Confess's if it returns
161             # false.
162              
163 0     0 0 0 my $snapshot = shift;
164              
165 0 0       0 unless ( is_btrfs_subvolume($snapshot) ) {
166 0         0 confess("yabsm: internal error: '$snapshot' is not a btrfs subvolume");
167             }
168              
169 0 0       0 unless ( is_snapshot_name(basename($snapshot), ALLOW_BOOTSTRAP => 1) ) {
170 0         0 confess("yabsm: internal error: '$snapshot' does not have a valid yabsm snapshot name");
171             }
172              
173 0         0 return 1;
174             }
175              
176             sub snapshot_name_nums {
177              
178             # Take a snapshot name and return a list containing, in order, the
179             # corresponding year, month, day, hour, and minute. Kill program if
180             # $snapshot_name is not a valid yabsm snapshot name.
181              
182 159     159 0 1193 arg_count_or_die(1, 1, @_);
183              
184 159         186 my $snapshot_name = shift;
185              
186 159         280 is_snapshot_name_or_die($snapshot_name, ALLOW_BOOTSTRAP => 1);
187              
188 156         546 my ($yr, $mon, $day, $hr, $min) = map { 0 + $_ } $snapshot_name =~ /^yabsm-(\d{4})_(\d{2})_(\d{2})_(\d{2}):(\d{2})$/;
  780         1194  
189              
190 156         377 return ($yr, $mon, $day, $hr, $min);
191             }
192              
193             sub nums_to_snapshot_name {
194              
195             # Take 5 integer arguments representing in order the year, month,
196             # day, hour, and minute and return a snapshot name of the
197             # corresponding time.
198              
199 37     37 0 1211 arg_count_or_die(5, 5, @_);
200              
201 37         62 my ($yr, $mon, $day, $hr, $min) = map { sprintf '%02d', $_ } @_;
  185         450  
202              
203 37         154 nums_denote_valid_date_or_die($yr, $mon, $day, $hr, $min);
204              
205 36         86 my $snapshot_name = "yabsm-${yr}_${mon}_${day}_$hr:$min";
206              
207 36         296 return $snapshot_name;
208             }
209              
210             sub current_time_snapshot_name {
211              
212             # Return a snapshot name corresponding to the current time.
213              
214 1     1 0 934 arg_count_or_die(0, 0, @_);
215              
216 1         3 my $t = localtime();
217              
218 1         51 return nums_to_snapshot_name($t->year, $t->mon, $t->mday, $t->hour, $t->min);
219             }
220              
221             sub sort_snapshots {
222              
223             # Takes a reference to an array of snapshots and returns a list of the
224             # snapshots sorted from newest to oldest. This function works with both
225             # paths to snapshots and plain snapshots names.
226             #
227             # If called in list context returns list of sorted snapshots. If called in
228             # scalar context returns a reference to the list of sorted snapshots.
229              
230 4     4 0 1510 arg_count_or_die(1, 1, @_);
231              
232 4         5 my @sorted = sort { cmp_snapshots($a, $b) } @{ +shift };
  10         14  
  4         13  
233              
234 3 100       18 return wantarray ? @sorted : \@sorted;
235             }
236              
237             sub cmp_snapshots {
238              
239             # Compare two yabsm snapshots based off their times. Works with both a path
240             # to a snapshot and just a snapshot name.
241             #
242             # Return -1 if $snapshot1 is newer than $snapshot2
243             # Return 1 if $snapshot1 is older than $snapshot2
244             # Return 0 if $snapshot1 and $snapshot2 are the same
245              
246 75     75 0 773 arg_count_or_die(2, 2, @_);
247              
248 75         87 my $snapshot1 = shift;
249 75         76 my $snapshot2 = shift;
250              
251 75         1590 my @nums1 = snapshot_name_nums(basename($snapshot1));
252 73         1223 my @nums2 = snapshot_name_nums(basename($snapshot2));
253              
254 73         147 for (my $i = 0; $i <= $#nums1; $i++) {
255 164 100       344 return -1 if $nums1[$i] > $nums2[$i];
256 133 100       321 return 1 if $nums1[$i] < $nums2[$i];
257             }
258              
259 15         70 return 0;
260             }
261              
262             sub snapshots_eq {
263              
264             # Return 1 if $snapshot1 and $snapshot2 denote the same time and return 0
265             # otherwise.
266              
267 7     7 0 990 arg_count_or_die(2, 2, @_);
268              
269 7         10 my $snapshot1 = shift;
270 7         9 my $snapshot2 = shift;
271              
272 7         13 return 0+(0 == cmp_snapshots($snapshot1, $snapshot2));
273             }
274              
275             sub snapshot_newer {
276              
277             # Return 1 if $snapshot1 is newer than $snapshot2 and return 0 otherwise.
278              
279 11     11 0 593 arg_count_or_die(2, 2, @_);
280              
281 11         15 my $snapshot1 = shift;
282 11         13 my $snapshot2 = shift;
283              
284 11         22 return 0+(-1 == cmp_snapshots($snapshot1, $snapshot2));
285             }
286              
287             sub snapshot_older {
288              
289             # Return 1 if $snapshot1 is older than $snapshot2 and return 0 otherwise.
290              
291 7     7 0 579 arg_count_or_die(2, 2, @_);
292              
293 7         8 my $snapshot1 = shift;
294 7         9 my $snapshot2 = shift;
295              
296 7         15 return 0+(1 == cmp_snapshots($snapshot1, $snapshot2));
297             }
298              
299             sub snapshot_newer_or_eq {
300              
301             # Return 1 if $snapshot1 is newer or equal to $snapshot2 and return 0
302             # otherwise.
303              
304 12     12 0 581 arg_count_or_die(2, 2, @_);
305              
306 12         15 my $snapshot1 = shift;
307 12         14 my $snapshot2 = shift;
308              
309 12         19 return 0+(cmp_snapshots($snapshot1, $snapshot2) <= 0);
310             }
311              
312             sub snapshot_older_or_eq {
313              
314             # Return 1 if $snapshot1 is newer or equal to $snapshot2 and return 0
315             # otherwise.
316              
317 23     23 0 638 arg_count_or_die(2, 2, @_);
318              
319 23         28 my $snapshot1 = shift;
320 23         29 my $snapshot2 = shift;
321              
322 23         36 return 0+(cmp_snapshots($snapshot1, $snapshot2) >= 0);
323             }
324              
325             1;