File Coverage

blib/lib/App/Yabsm/Command/Find.pm
Criterion Covered Total %
statement 282 351 80.3
branch 42 102 41.1
condition 13 22 59.0
subroutine 44 48 91.6
pod 0 16 0.0
total 381 539 70.6


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 functionality for finding snapshots via a snapshot finding DSL.
6             #
7             # See t/Yabsm/Snapshot.pm for this libraries tests.
8              
9 1     1   509 use strict;
  1         2  
  1         24  
10 1     1   6 use warnings;
  1         1  
  1         18  
11 1     1   8 use v5.16.3;
  1         4  
12              
13             package App::Yabsm::Command::Find;
14              
15 1     1   363 use App::Yabsm::Tools qw( :ALL );
  1         2  
  1         163  
16 1     1   474 use App::Yabsm::Config::Query qw ( :ALL );
  1         2  
  1         364  
17 1     1   429 use App::Yabsm::Config::Parser qw(parse_config_or_die);
  1         2  
  1         51  
18 1     1   388 use App::Yabsm::Backup::SSH;
  1         3  
  1         75  
19 1         75 use App::Yabsm::Snapshot qw(nums_to_snapshot_name
20             snapshot_name_nums
21             current_time_snapshot_name
22             sort_snapshots
23             is_snapshot_name
24             snapshots_eq
25             snapshot_newer
26             snapshot_older
27             snapshot_newer_or_eq
28             snapshot_older_or_eq
29 1     1   7 );
  1         2  
30              
31 1     1   5 use Feature::Compat::Try;
  1         2  
  1         5  
32 1     1   91 use Net::OpenSSH;
  1         2  
  1         17  
33 1     1   4 use Time::Piece;
  1         2  
  1         27  
34 1     1   69 use File::Basename qw(basename);
  1         20  
  1         41  
35 1     1   5 use Carp qw(confess);
  1         11  
  1         36  
36 1     1   5 use POSIX ();
  1         1  
  1         12  
37              
38 1     1   4 use Parser::MGC;
  1         8  
  1         25  
39 1     1   4 use base qw(Parser::MGC);
  1         1  
  1         4232  
40              
41             sub usage {
42 0     0 0 0 arg_count_or_die(0, 0, @_);
43 0         0 return 'usage: yabsm [--help] [ ]'."\n";
44             }
45              
46             sub help {
47 0 0   0 0 0 @_ == 0 or die usage();
48 0         0 my $usage = usage();
49 0         0 $usage =~ s/\s+$//;
50 0         0 print <<"END_HELP";
51             $usage
52              
53             see the section "Finding Snapshots" in 'man yabsm' for a detailed explanation on
54             how to find snapshots and backups.
55              
56             examples:
57             yabsm find home_snap back-10-hours
58             yabsm f root_ssh_backup newest
59             yabsm f home_local_backup oldest
60             yabsm f home_snap 'between b-10-mins 15:45'
61             yabsm f root_snap 'after back-2-days'
62             yabsm f root_local_backup 'before b-14-d'
63             END_HELP
64             }
65              
66             ####################################
67             # MAIN #
68             ####################################
69              
70             sub main {
71              
72 0 0   0 0 0 if (@_ == 1) {
    0          
73 0 0       0 shift =~ /^(-h|--help)$/ or die usage();
74 0         0 help();
75             }
76              
77             elsif (@_ == 2) {
78              
79 0         0 my $thing = shift;
80 0         0 my $query = shift;
81              
82 0         0 my $config_ref = parse_config_or_die();
83              
84 0 0 0     0 unless (snap_exists($thing, $config_ref) || ssh_backup_exists($thing, $config_ref) || local_backup_exists($thing, $config_ref)) {
      0        
85 0         0 die "yabsm: error: no such snap, ssh_backup, or local_backup named '$thing'\n";
86             }
87              
88 0         0 my @snapshots = answer_query($thing, parse_query_or_die($query), $config_ref);
89              
90 0         0 say for @snapshots;
91             }
92              
93             else {
94 0         0 die usage()
95             }
96             }
97              
98             ####################################
99             # QUERY ANSWERING #
100             ####################################
101              
102             sub answer_query {
103              
104             # Return a subset of all the snapshots/backups of $thing that satisfy
105             # $query.
106              
107 0     0 0 0 arg_count_or_die(3, 3, @_);
108              
109 0         0 my $thing = shift;
110 0         0 my %query = %{+shift};
  0         0  
111 0         0 my $config_ref = shift;
112              
113 0         0 my @snapshots;
114              
115 0 0       0 if (snap_exists($thing, $config_ref)) {
    0          
    0          
116 0         0 for my $tframe (snap_timeframes($thing, $config_ref)) {
117 0         0 my $dir = snap_dest($thing, $tframe, $config_ref);
118 0 0       0 unless (-r $dir) {
119 0         0 die "yabsm: error: do not have read permission on '$dir'\n";
120             }
121 0 0       0 opendir my $dh, $dir or confess "yabsm: internal error: could not opendir '$dir'";
122 0         0 push @snapshots, map { $_ = "$dir/$_" } grep { is_snapshot_name($_) } readdir($dh);
  0         0  
  0         0  
123 0         0 closedir $dh;
124             }
125             }
126              
127             elsif (ssh_backup_exists($thing, $config_ref)) {
128              
129 0 0       0 die 'yabsm: error: permission denied'."\n" unless i_am_root();
130              
131 0 0       0 my $yabsm_uid = getpwnam('yabsm') or die q(yabsm: error: no user named 'yabsm')."\n";
132              
133 0         0 POSIX::setuid($yabsm_uid);
134              
135 0         0 my $ssh = App::Yabsm::Backup::SSH::new_ssh_conn($thing, $config_ref);
136              
137 0         0 my $ssh_dest = ssh_backup_ssh_dest($thing, $config_ref);
138              
139 0 0       0 if ($ssh->error) {
140 0         0 die "yabsm: ssh error: $ssh_dest: ".$ssh->error."\n";
141             }
142 0         0 for my $tframe (ssh_backup_timeframes($thing, $config_ref)) {
143 0         0 my $dir = ssh_backup_dir($thing, $tframe, $config_ref);
144 0 0       0 unless ($ssh->system("[ -r '$dir' ]")) {
145 0         0 die "yabsm: ssh error: $ssh_dest: remote user does not have read permission on '$dir'\n";
146             }
147 0         0 push @snapshots, grep { chomp $_; is_snapshot_name($_) } App::Yabsm::Backup::SSH::ssh_system_or_die($ssh, "ls -1 '$dir'");
  0         0  
  0         0  
148 0         0 map { $_ = "$dir/$_" } @snapshots;
  0         0  
149             }
150             }
151              
152             elsif (local_backup_exists($thing, $config_ref)) {
153 0         0 for my $tframe (local_backup_timeframes($thing, $config_ref)) {
154 0         0 my $dir = local_backup_dir($thing, $tframe, $config_ref);
155 0 0       0 unless (-r $dir) {
156 0         0 die "yabsm: error: do not have read permission on '$dir'\n";
157             }
158 0 0       0 opendir my $dh, $dir or confess "yabsm: internal error: could not opendir '$dir'";
159 0         0 push @snapshots, map { $_ = "$dir/$_" } grep { is_snapshot_name($_) } readdir($dh);
  0         0  
  0         0  
160 0         0 closedir $dh;
161             }
162             }
163              
164             else {
165 0         0 die "yabsm: internal error: no such snap, ssh_backup, or local_backup named '$thing'";
166             }
167              
168 0         0 @snapshots = sort_snapshots(\@snapshots);
169              
170 0 0       0 if ($query{type} eq 'all') {
    0          
    0          
    0          
    0          
    0          
    0          
171             ;
172             }
173              
174             elsif ($query{type} eq 'newest') {
175 0         0 @snapshots = answer_newest_query(\@snapshots);
176             }
177              
178             elsif ($query{type} eq 'oldest') {
179 0         0 @snapshots = answer_oldest_query(\@snapshots);
180             }
181              
182             elsif ($query{type} eq 'after') {
183 0         0 @snapshots = answer_after_query($query{target}, \@snapshots);
184             }
185              
186             elsif ($query{type} eq 'before') {
187 0         0 @snapshots = answer_before_query($query{target}, \@snapshots);
188             }
189              
190             elsif ($query{type} eq 'between') {
191 0         0 @snapshots = answer_between_query($query{target1}, $query{target2}, \@snapshots);
192             }
193              
194             elsif ($query{type} eq 'closest') {
195 0         0 @snapshots = answer_closest_query($query{target}, \@snapshots);
196             }
197              
198             else {
199 0         0 confess("yabsm: internal error: no such query type $query{type}");
200             }
201              
202 0 0       0 return wantarray ? @snapshots : \@snapshots;
203             }
204              
205             sub answer_newest_query {
206              
207             # Return the newest snapshot in @snapshots. Because @snapshots is assumed to
208             # be sorted from newest to oldest we know the newest snapshot is the first
209             # snapshot in @snapshots.
210              
211 2     2 0 373 arg_count_or_die(1, 1, @_);
212              
213 2         3 my @newest;
214              
215 2         4 push @newest, shift->[0];
216              
217 2 50       14 return wantarray ? @newest : \@newest;
218             }
219              
220             sub answer_oldest_query {
221              
222             # Return the oldest snapshot in @snapshots. Because @snapshots is assumed to
223             # be sorted from newest to oldest we know the oldest snapshot is the last
224             # snapshot in @snapshots.
225              
226 2     2 0 452 arg_count_or_die(1, 1, @_);
227              
228 2         2 my @oldest;
229              
230 2         4 push @oldest, shift->[-1];
231              
232 2 50       15 return wantarray ? @oldest : \@oldest;
233             }
234              
235             sub answer_after_query {
236              
237             # Return all snapshots in @snapshots that are newer than the target snapshot
238             # $target. This subroutine assumes that @snapshots is sorted from newest to
239             # oldest.
240              
241 2     2 0 440 arg_count_or_die(2, 2, @_);
242              
243 2         4 my $target = shift;
244 2         2 my @snapshots = @{+shift};
  2         5  
245              
246 2         4 my @after;
247              
248 2         4 foreach my $this_snapshot (@snapshots) {
249 5 100       11 if (snapshot_newer($this_snapshot, $target)) {
250 3         6 push @after, $this_snapshot;
251             }
252             else {
253 2         4 last;
254             }
255             }
256              
257 2 50       15 return wantarray ? @after : \@after;
258             }
259              
260             sub answer_before_query {
261              
262             # Return all snapshots in @snapshots that are older than the target snapshot
263             # $target. This subroutine assumes that @snapshots is sorted from newest to
264             # oldest.
265              
266 2     2 0 457 arg_count_or_die(2, 2, @_);
267              
268 2         3 my $target = shift;
269 2         3 my @snapshots = @{+shift};
  2         14  
270              
271 2         5 my @before;
272              
273 2         7 for (my $i = $#snapshots; $i >= 0; $i--) {
274 4         5 my $this_snapshot = $snapshots[$i];
275 4 100       11 if (snapshot_older($this_snapshot, $target)) {
276 2         7 unshift @before, $this_snapshot;
277             }
278             else {
279 2         4 last;
280             }
281             }
282              
283 2 50       13 return wantarray ? @before : \@before;
284             }
285              
286             sub answer_between_query {
287              
288             # Return all snapshots in @snapshots that are between $newer and $older
289             # (inclusive). This subroutine assumes that @snapshots is sorted from newest
290             # to oldest.
291              
292 3     3 0 438 arg_count_or_die(3, 3, @_);
293              
294 3         5 my $newer = shift;
295 3         4 my $older = shift;
296 3         4 my @snapshots = @{+shift};
  3         7  
297              
298 3 100       7 ($newer, $older) = ($older, $newer) if snapshot_newer($older, $newer);
299              
300 3         6 my @between;
301              
302 3         7 for (my $i = 0; $i <= $#snapshots; $i++) {
303 5 100       12 if (snapshot_older_or_eq($snapshots[$i], $newer)) {
304 3         7 for (my $j = $i; $j <= $#snapshots; $j++) {
305 9         11 my $this_snapshot = $snapshots[$j];
306 9 100       19 if (snapshot_newer_or_eq($this_snapshot, $older)) {
307 6         15 push @between, $this_snapshot;
308             }
309             else {
310 3         5 last;
311             }
312             }
313 3         4 last;
314             }
315             }
316              
317 3 50       23 return wantarray ? @between : \@between;
318             }
319              
320             sub answer_closest_query {
321              
322             # Return the snapshot in @snapshots that is closest to the snapshot $target.
323             # This subroutine assumes that @snapshots is sorted from newest to oldest.
324              
325 7     7 0 519 arg_count_or_die(2, 2, @_);
326              
327 7         10 my $target = shift;
328 7         9 my @snapshots = @{+shift};
  7         15  
329              
330 7         10 my @closest;
331              
332 7         16 for (my $i = 0; $i <= $#snapshots; $i++) {
333 15         17 my $this_snapshot = $snapshots[$i];
334 15 100       31 if (snapshot_older_or_eq($this_snapshot, $target)) {
    100          
335 5 100       12 if (snapshots_eq($this_snapshot, $target)) {
    100          
336 1         3 @closest = ($this_snapshot);
337             }
338             elsif ($i == 0) {
339 1         3 @closest = ($this_snapshot);
340             }
341             else {
342 3         7 my $last_snapshot = $snapshots[$i - 1];
343 3         54 my $target_epoch = Time::Piece->strptime(join('/', snapshot_name_nums(basename($target))), '%Y/%m/%d/%H/%M')->epoch;
344 3         253 my $this_epoch = Time::Piece->strptime(join('/', snapshot_name_nums(basename($this_snapshot))), '%Y/%m/%d/%H/%M')->epoch;
345 3         226 my $last_epoch = Time::Piece->strptime(join('/', snapshot_name_nums(basename($last_snapshot))), '%Y/%m/%d/%H/%M')->epoch;
346 3         160 my $last_target_diff = abs($last_epoch - $target_epoch);
347 3         4 my $this_target_diff = abs($this_epoch - $target_epoch);
348 3 100       6 if ($last_target_diff <= $this_target_diff) {
349 2         4 @closest = ($last_snapshot);
350             }
351             else {
352 1         3 @closest = ($this_snapshot);
353             }
354             }
355 5         9 last;
356             }
357             elsif ($i == $#snapshots) {
358 1         4 @closest = ($this_snapshot);
359             }
360             }
361              
362 7 50       44 return wantarray ? @closest : \@closest;
363             }
364              
365             ####################################
366             # QUERY PARSER #
367             ####################################
368              
369             sub parse_query_or_die {
370              
371             # Parse $query into a query production or die with a useful error message
372             # about about what is wrong with the query.
373              
374 34     34 0 14221 arg_count_or_die(1, 1, @_);
375              
376 34         205 my $query = shift =~ s/^\s+|\s+$//gr;
377              
378 34         115 my $query_parser = __PACKAGE__->new( toplevel => 'query_parser' );
379              
380 34         1328 my $query_production = do {
381             try { $query_parser->from_string($query) }
382 34         61 catch ($e) {
383             $e =~ s/on line \d+ //g;
384             die "yabsm: query error: $e";
385             }
386             };
387              
388 17         552 return $query_production;
389             }
390              
391             sub query_parser {
392              
393             # Top level parser
394              
395 34     34 0 423 arg_count_or_die(1, 1, @_);
396              
397 34         42 my $self = shift;
398              
399             # return this
400 34         45 my %query;
401              
402             my $type = $self->any_of(
403             sub {
404 34     34   418 $self->expect( 'all' );
405 2         153 $query{type} = 'all';
406             },
407             sub {
408 32     32   2596 $self->expect( 'newest' );
409 2         97 $query{type} = 'newest';
410             },
411             sub {
412 30     30   2017 $self->expect( 'oldest' );
413 2         84 $query{type} = 'oldest';
414             },
415             sub {
416 28     28   1774 $self->expect( 'before' );
417 4         160 $self->commit;
418 4         29 $self->skip_ws;
419 4         43 $query{type} = 'before';
420 4         8 $query{target} = $self->time_abbreviation_parser;
421             },
422             sub {
423 24     24   1526 $self->expect( 'after' );
424 4         158 $self->commit;
425 4         26 $self->skip_ws;
426 4         43 $query{type} = 'after';
427 4         8 $query{target} = $self->time_abbreviation_parser;
428             },
429             sub {
430 20     20   1281 $self->expect( 'between' );
431 4         158 $self->commit;
432 4         82 $self->skip_ws;
433 4         49 $query{type} = 'between';
434 4         8 $query{target1} = $self->time_abbreviation_parser;
435 3         10 $self->commit;
436 3         22 $self->skip_ws;
437 3         42 $query{target2} = $self->time_abbreviation_parser;
438             },
439             sub {
440 16     16   1011 my $time = $self->time_abbreviation_parser;
441 10         21 $query{type} = 'closest';
442 10         33 $query{target} = $time;
443             },
444             sub {
445 6     6   227 $self->commit;
446 6         37 $self->skip_ws;
447 6         55 $self->fail(q(expected or one of 'all', 'newest', 'oldest', 'before', 'after', 'between'))
448             }
449 34         275 );
450              
451 24         202 return \%query;
452             }
453              
454             sub time_abbreviation_parser {
455              
456             # A time abbreviation is either a relative time or an immediate time.
457              
458 31     31 0 71 arg_count_or_die(1, 1, @_);
459              
460 31         37 my $self = shift;
461              
462             my $snapshot_name =
463             $self->any_of( 'relative_time_abbreviation_parser'
464             , 'immediate_time_abbreviation_parser'
465             , sub {
466 10     10   1101 $self->commit;
467 10         72 $self->skip_ws;
468 10         106 $self->fail('expected time abbreviation');
469             }
470 31         112 );
471              
472 21         83 return $snapshot_name;
473             }
474              
475             sub relative_time_abbreviation_parser {
476              
477             # A relative time comes in the form where
478             # AMOUNT is a positive integer and UNIT is one of 'days', 'hours',
479             # or 'minutes' (or one of their abbreviations). 'back' can always
480             # be abbreviated to 'b'.
481              
482 31     31 0 369 arg_count_or_die(1, 1, @_);
483              
484 31         38 my $self = shift;
485              
486 31         100 $self->expect( qr/b(ack)?/ );
487 11         434 $self->expect('-');
488 11         391 my $amount = $self->expect(qr/[1-9][0-9]*/);
489 11         381 $self->expect('-');
490 11         343 my $unit = $self->expect(qr/days|d|hours|hrs|h|minutes|mins|m/);
491              
492 11         665 return n_units_ago_snapshot_name($amount, $unit);
493             }
494              
495             sub immediate_time_abbreviation_parser {
496              
497             # An immediate time
498              
499 20     20 0 1307 arg_count_or_die(1, 1, @_);
500              
501 20         24 my $self = shift;
502              
503 20         60 my $yr;
504             my $mon;
505 20         0 my $day;
506 20         0 my $hr;
507 20         0 my $min;
508              
509 20         116 my %time_regex = ( yr => qr/2[0-9]{3}/
510             , mon => qr/[1][0-2]|0?[1-9]/
511             , day => qr/3[01]|[12][0-9]|0?[1-9]/
512             , hr => qr/2[123]|1[0-9]|0?[0-9]/
513             , min => qr/[1-5][0-9]|0?[0-9]/
514             );
515              
516             $self->any_of(
517             sub { # yr_mon_day_hr:min
518 20     20   221 my $yr_ = $self->expect($time_regex{yr});
519 9         386 $self->expect('_');
520 9         323 my $mon_ = $self->expect($time_regex{mon});
521 9         440 $self->expect('_');
522 7         233 my $day_ = $self->expect($time_regex{day});
523 7         295 $self->expect('_');
524 5         156 my $hr_ = $self->expect($time_regex{hr});
525 5         219 $self->expect(':');
526 4         160 my $min_ = $self->expect($time_regex{min});
527             $self->any_of(
528 4         66 sub { $self->expect(qr/[ ]+/) },
529 4 50       269 sub { $self->at_eos or $self->fail; }
530 4         169 );
531              
532 4         89 $yr = $yr_;
533 4         5 $mon = $mon_;
534 4         5 $day = $day_;
535 4         6 $hr = $hr_;
536 4         8 $min = $min_;
537             },
538              
539             sub { # yr_mon_day
540 16     16   1049 my $yr_ = $self->expect($time_regex{yr});
541 5         195 $self->expect('_');
542 5         160 my $mon_ = $self->expect($time_regex{mon});
543 5         197 $self->expect('_');
544 3         92 my $day_ = $self->expect($time_regex{day});
545             $self->any_of(
546 3         38 sub { $self->expect(qr/[ ]+/) },
547 3 100       193 sub { $self->at_eos or $self->fail; }
548 3         127 );
549              
550 1         22 $yr = $yr_;
551 1         2 $mon = $mon_;
552 1         2 $day = $day_;
553             },
554              
555             sub { # mon_day_hr:min
556 15     15   818 my $mon_ = $self->expect($time_regex{mon});
557 10         457 $self->expect('_');
558 3         111 my $day_ = $self->expect($time_regex{day});
559 3         136 $self->expect('_');
560 2         74 my $hr_ = $self->expect($time_regex{hr});
561 2         88 $self->expect(':');
562 1         37 my $min_ = $self->expect($time_regex{min});
563             $self->any_of(
564 1         15 sub { $self->expect(qr/[ ]+/) },
565 1 50       80 sub { $self->at_eos or $self->fail; }
566 1         57 );
567              
568 1         23 $mon = $mon_;
569 1         2 $day = $day_;
570 1         2 $hr = $hr_;
571 1         2 $min = $min_;
572             },
573              
574             sub { # mon_day_hr
575 14     14   921 my $mon_ = $self->expect($time_regex{mon});
576 9         393 $self->expect('_');
577 2         68 my $day_ = $self->expect($time_regex{day});
578 2         79 $self->expect('_');
579 1         33 my $hr_ = $self->expect($time_regex{hr});
580             $self->any_of(
581 1         14 sub { $self->expect(qr/[ ]+/) },
582 1 50       69 sub { $self->at_eos or $self->fail; }
583 1         66 );
584              
585 1         23 $mon = $mon_;
586 1         1 $day = $day_;
587 1         3 $hr = $hr_;
588             },
589              
590             sub { # mon_day
591 13     13   732 my $mon_ = $self->expect($time_regex{mon});
592 8         345 $self->expect('_');
593 1         35 my $day_ = $self->expect($time_regex{day});
594             $self->any_of(
595 1         16 sub { $self->expect(qr/[ ]+/) },
596 1 50       68 sub { $self->at_eos or $self->fail; }
597 1         60 );
598              
599 1         22 $mon = $mon_;
600 1         3 $day = $day_;
601             },
602              
603             sub { # day_hr:min
604 12     12   672 my $day_ = $self->expect($time_regex{day});
605 7         309 $self->expect('_');
606 1         33 my $hr_ = $self->expect($time_regex{hr});
607 1         45 $self->expect(':');
608 1         37 my $min_ = $self->expect($time_regex{min});
609             $self->any_of(
610 1         14 sub { $self->expect(qr/[ ]+/) },
611 1 50       64 sub { $self->at_eos or $self->fail; }
612 1         40 );
613              
614 1         23 $day = $day_;
615 1         2 $hr = $hr_;
616 1         3 $min = $min_;
617             },
618              
619             sub { # hr:min
620 11     11   752 my $hr_ = $self->expect($time_regex{hr});
621 6         286 $self->expect(':');
622 1         38 my $min_ = $self->expect($time_regex{min});
623             $self->any_of(
624 1         14 sub { $self->expect(qr/[ ]+/) },
625 1 50       64 sub { $self->at_eos or $self->fail; }
626 1         41 );
627              
628 1         23 $hr = $hr_;
629 1         3 $min = $min_;
630             }
631 20         241 );
632              
633 10         133 my $t = localtime;
634              
635 10   66     588 $yr //= $t->year;
636 10   66     47 $mon //= $t->mon;
637 10   66     23 $day //= $t->mday;
638 10   100     25 $hr //= 0;
639 10   100     21 $min //= 0;
640              
641 10         32 return nums_to_snapshot_name($yr, $mon, $day, $hr, $min);
642             }
643              
644             ####################################
645             # TIME FUNCTIONS #
646             ####################################
647              
648             sub n_units_ago_snapshot_name {
649              
650             # Return a snapshot name representing the time $n $unit's ago from now.
651              
652 27     27 0 661 arg_count_or_die(2, 2, @_);
653              
654 27         45 my $n = shift;
655 27         33 my $unit = shift;
656              
657 27 100 100     157 unless ($n =~ /^\d+$/ && $n > 0) {
658 2         22 confess "yabsm: internal error: '$n' is not a positive integer";
659             }
660              
661 25         34 my $seconds_per_unit;
662              
663 25 100       77 if ($unit =~ /^(?:minutes|mins|m)$/) { $seconds_per_unit = 60 }
  13 100       18  
    100          
664 9         12 elsif ($unit =~ /^(?:hours|hrs|h)$/ ) { $seconds_per_unit = 3600 }
665 2         5 elsif ($unit =~ /^(?:days|d)$/ ) { $seconds_per_unit = 86400 }
666             else {
667 1         17 confess "yabsm: internal error: '$unit' is not a valid time unit";
668             }
669              
670 24         52 my $t = localtime;
671              
672 24         1397 my ($yr, $mon, $day, $hr, $min) = ($t->year, $t->mon, $t->mday, $t->hour, $t->min);
673              
674 24         398 my $tp = Time::Piece->strptime("$yr/$mon/$day/$hr/$min", '%Y/%m/%d/%H/%M');
675              
676 24         1193 $tp -= $n * $seconds_per_unit;
677              
678 24         1114 return nums_to_snapshot_name($tp->year, $tp->mon, $tp->mday, $tp->hour, $tp->min);
679             }
680              
681             1;