File Coverage

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


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