| 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 |  | 586 | use strict; | 
|  | 1 |  |  |  |  | 2 |  | 
|  | 1 |  |  |  |  | 27 |  | 
| 10 | 1 |  |  | 1 |  | 4 | use warnings; | 
|  | 1 |  |  |  |  | 2 |  | 
|  | 1 |  |  |  |  | 19 |  | 
| 11 | 1 |  |  | 1 |  | 7 | use v5.16.3; | 
|  | 1 |  |  |  |  | 3 |  | 
| 12 |  |  |  |  |  |  |  | 
| 13 |  |  |  |  |  |  | package App::Yabsm::Command::Find; | 
| 14 |  |  |  |  |  |  |  | 
| 15 | 1 |  |  | 1 |  | 365 | use App::Yabsm::Tools qw( :ALL ); | 
|  | 1 |  |  |  |  | 2 |  | 
|  | 1 |  |  |  |  | 162 |  | 
| 16 | 1 |  |  | 1 |  | 613 | use App::Yabsm::Config::Query qw ( :ALL ); | 
|  | 1 |  |  |  |  | 2 |  | 
|  | 1 |  |  |  |  | 360 |  | 
| 17 | 1 |  |  | 1 |  | 435 | use App::Yabsm::Config::Parser qw(parse_config_or_die); | 
|  | 1 |  |  |  |  | 2 |  | 
|  | 1 |  |  |  |  | 52 |  | 
| 18 | 1 |  |  | 1 |  | 372 | use App::Yabsm::Backup::SSH; | 
|  | 1 |  |  |  |  | 3 |  | 
|  | 1 |  |  |  |  | 71 |  | 
| 19 | 1 |  |  |  |  | 70 | 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 |  | 6 | use Feature::Compat::Try; | 
|  | 1 |  |  |  |  | 2 |  | 
|  | 1 |  |  |  |  | 7 |  | 
| 32 | 1 |  |  | 1 |  | 97 | use Net::OpenSSH; | 
|  | 1 |  |  |  |  | 2 |  | 
|  | 1 |  |  |  |  | 18 |  | 
| 33 | 1 |  |  | 1 |  | 5 | use Time::Piece; | 
|  | 1 |  |  |  |  | 1 |  | 
|  | 1 |  |  |  |  | 31 |  | 
| 34 | 1 |  |  | 1 |  | 73 | use File::Basename qw(basename); | 
|  | 1 |  |  |  |  | 19 |  | 
|  | 1 |  |  |  |  | 42 |  | 
| 35 | 1 |  |  | 1 |  | 5 | use Carp qw(confess); | 
|  | 1 |  |  |  |  | 2 |  | 
|  | 1 |  |  |  |  | 39 |  | 
| 36 | 1 |  |  | 1 |  | 6 | use POSIX (); | 
|  | 1 |  |  |  |  | 1 |  | 
|  | 1 |  |  |  |  | 14 |  | 
| 37 |  |  |  |  |  |  |  | 
| 38 | 1 |  |  | 1 |  | 3 | use Parser::MGC; | 
|  | 1 |  |  |  |  | 1 |  | 
|  | 1 |  |  |  |  | 28 |  | 
| 39 | 1 |  |  | 1 |  | 6 | use base qw(Parser::MGC); | 
|  | 1 |  |  |  |  | 1 |  | 
|  | 1 |  |  |  |  | 4442 |  | 
| 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 | 289 | arg_count_or_die(1, 1, @_); | 
| 211 |  |  |  |  |  |  |  | 
| 212 | 2 |  |  |  |  | 3 | my @newest; | 
| 213 |  |  |  |  |  |  |  | 
| 214 | 2 |  |  |  |  | 5 | push @newest, shift->[0]; | 
| 215 |  |  |  |  |  |  |  | 
| 216 | 2 | 50 |  |  |  | 15 | 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 | 488 | arg_count_or_die(1, 1, @_); | 
| 226 |  |  |  |  |  |  |  | 
| 227 | 2 |  |  |  |  | 3 | my @oldest; | 
| 228 |  |  |  |  |  |  |  | 
| 229 | 2 |  |  |  |  | 5 | push @oldest, shift->[-1]; | 
| 230 |  |  |  |  |  |  |  | 
| 231 | 2 | 50 |  |  |  | 12 | 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 | 431 | arg_count_or_die(2, 2, @_); | 
| 241 |  |  |  |  |  |  |  | 
| 242 | 2 |  |  |  |  | 3 | my $target    = shift; | 
| 243 | 2 |  |  |  |  | 3 | my @snapshots = @{+shift}; | 
|  | 2 |  |  |  |  | 4 |  | 
| 244 |  |  |  |  |  |  |  | 
| 245 | 2 |  |  |  |  | 4 | my @after; | 
| 246 |  |  |  |  |  |  |  | 
| 247 | 2 |  |  |  |  | 4 | foreach my $this_snapshot (@snapshots) { | 
| 248 | 5 | 100 |  |  |  | 13 | if (snapshot_newer($this_snapshot, $target)) { | 
| 249 | 3 |  |  |  |  | 8 | push @after, $this_snapshot; | 
| 250 |  |  |  |  |  |  | } | 
| 251 |  |  |  |  |  |  | else { | 
| 252 | 2 |  |  |  |  | 4 | last; | 
| 253 |  |  |  |  |  |  | } | 
| 254 |  |  |  |  |  |  | } | 
| 255 |  |  |  |  |  |  |  | 
| 256 | 2 | 50 |  |  |  | 15 | 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 | 460 | arg_count_or_die(2, 2, @_); | 
| 266 |  |  |  |  |  |  |  | 
| 267 | 2 |  |  |  |  | 3 | my $target    = shift; | 
| 268 | 2 |  |  |  |  | 2 | my @snapshots = @{+shift}; | 
|  | 2 |  |  |  |  | 30 |  | 
| 269 |  |  |  |  |  |  |  | 
| 270 | 2 |  |  |  |  | 11 | my @before; | 
| 271 |  |  |  |  |  |  |  | 
| 272 | 2 |  |  |  |  | 10 | for (my $i = $#snapshots; $i >= 0; $i--) { | 
| 273 | 4 |  |  |  |  | 5 | my $this_snapshot = $snapshots[$i]; | 
| 274 | 4 | 100 |  |  |  | 10 | if (snapshot_older($this_snapshot, $target)) { | 
| 275 | 2 |  |  |  |  | 9 | unshift @before, $this_snapshot; | 
| 276 |  |  |  |  |  |  | } | 
| 277 |  |  |  |  |  |  | else { | 
| 278 | 2 |  |  |  |  | 4 | last; | 
| 279 |  |  |  |  |  |  | } | 
| 280 |  |  |  |  |  |  | } | 
| 281 |  |  |  |  |  |  |  | 
| 282 | 2 | 50 |  |  |  | 15 | 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 | 436 | arg_count_or_die(3, 3, @_); | 
| 292 |  |  |  |  |  |  |  | 
| 293 | 3 |  |  |  |  | 3 | my $newer     = shift; | 
| 294 | 3 |  |  |  |  | 5 | my $older     = shift; | 
| 295 | 3 |  |  |  |  | 5 | my @snapshots = @{+shift}; | 
|  | 3 |  |  |  |  | 7 |  | 
| 296 |  |  |  |  |  |  |  | 
| 297 | 3 | 100 |  |  |  | 8 | ($newer, $older) = ($older, $newer) if snapshot_newer($older, $newer); | 
| 298 |  |  |  |  |  |  |  | 
| 299 | 3 |  |  |  |  | 7 | 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 |  |  |  |  | 9 | for (my $j = $i; $j <= $#snapshots; $j++) { | 
| 304 | 9 |  |  |  |  | 16 | my $this_snapshot = $snapshots[$j]; | 
| 305 | 9 | 100 |  |  |  | 14 | if (snapshot_newer_or_eq($this_snapshot, $older)) { | 
| 306 | 6 |  |  |  |  | 15 | push @between, $this_snapshot; | 
| 307 |  |  |  |  |  |  | } | 
| 308 |  |  |  |  |  |  | else { | 
| 309 | 3 |  |  |  |  | 6 | last; | 
| 310 |  |  |  |  |  |  | } | 
| 311 |  |  |  |  |  |  | } | 
| 312 | 3 |  |  |  |  | 3 | last; | 
| 313 |  |  |  |  |  |  | } | 
| 314 |  |  |  |  |  |  | } | 
| 315 |  |  |  |  |  |  |  | 
| 316 | 3 | 50 |  |  |  | 24 | 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 | 451 | arg_count_or_die(2, 2, @_); | 
| 325 |  |  |  |  |  |  |  | 
| 326 | 7 |  |  |  |  | 10 | my $target    = shift; | 
| 327 | 7 |  |  |  |  | 19 | my @snapshots = @{+shift}; | 
|  | 7 |  |  |  |  | 16 |  | 
| 328 |  |  |  |  |  |  |  | 
| 329 | 7 |  |  |  |  | 8 | my @closest; | 
| 330 |  |  |  |  |  |  |  | 
| 331 | 7 |  |  |  |  | 19 | for (my $i = 0; $i <= $#snapshots; $i++) { | 
| 332 | 15 |  |  |  |  | 22 | my $this_snapshot = $snapshots[$i]; | 
| 333 | 15 | 100 |  |  |  | 30 | if (snapshot_older_or_eq($this_snapshot, $target)) { | 
|  |  | 100 |  |  |  |  |  | 
| 334 | 5 | 100 |  |  |  | 12 | 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 |  |  |  |  | 8 | my $last_snapshot = $snapshots[$i - 1]; | 
| 342 | 3 |  |  |  |  | 52 | my $target_epoch = Time::Piece->strptime(join('/', snapshot_name_nums(basename($target))), '%Y/%m/%d/%H/%M')->epoch; | 
| 343 | 3 |  |  |  |  | 261 | my $this_epoch = Time::Piece->strptime(join('/', snapshot_name_nums(basename($this_snapshot))), '%Y/%m/%d/%H/%M')->epoch; | 
| 344 | 3 |  |  |  |  | 268 | my $last_epoch = Time::Piece->strptime(join('/', snapshot_name_nums(basename($last_snapshot))), '%Y/%m/%d/%H/%M')->epoch; | 
| 345 | 3 |  |  |  |  | 165 | my $last_target_diff = abs($last_epoch - $target_epoch); | 
| 346 | 3 |  |  |  |  | 4 | my $this_target_diff = abs($this_epoch - $target_epoch); | 
| 347 | 3 | 100 |  |  |  | 8 | if ($last_target_diff <= $this_target_diff) { | 
| 348 | 2 |  |  |  |  | 5 | @closest = ($last_snapshot); | 
| 349 |  |  |  |  |  |  | } | 
| 350 |  |  |  |  |  |  | else { | 
| 351 | 1 |  |  |  |  | 2 | @closest = ($this_snapshot); | 
| 352 |  |  |  |  |  |  | } | 
| 353 |  |  |  |  |  |  | } | 
| 354 | 5 |  |  |  |  | 11 | last; | 
| 355 |  |  |  |  |  |  | } | 
| 356 |  |  |  |  |  |  | elsif ($i == $#snapshots) { | 
| 357 | 1 |  |  |  |  | 5 | @closest = ($this_snapshot); | 
| 358 |  |  |  |  |  |  | } | 
| 359 |  |  |  |  |  |  | } | 
| 360 |  |  |  |  |  |  |  | 
| 361 | 7 | 50 |  |  |  | 46 | 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 | 14001 | arg_count_or_die(1, 1, @_); | 
| 374 |  |  |  |  |  |  |  | 
| 375 | 34 |  |  |  |  | 205 | my $query = shift =~ s/^\s+|\s+$//gr; | 
| 376 |  |  |  |  |  |  |  | 
| 377 | 34 |  |  |  |  | 109 | my $query_parser = __PACKAGE__->new( toplevel => 'query_parser' ); | 
| 378 |  |  |  |  |  |  |  | 
| 379 | 34 |  |  |  |  | 1328 | my $query_production = do { | 
| 380 |  |  |  |  |  |  | try { $query_parser->from_string($query) } | 
| 381 | 34 |  |  |  |  | 67 | catch ($e) { | 
| 382 |  |  |  |  |  |  | $e =~ s/on line \d+ //g; | 
| 383 |  |  |  |  |  |  | die "yabsm: query error: $e"; | 
| 384 |  |  |  |  |  |  | } | 
| 385 |  |  |  |  |  |  | }; | 
| 386 |  |  |  |  |  |  |  | 
| 387 | 17 |  |  |  |  | 541 | return $query_production; | 
| 388 |  |  |  |  |  |  | } | 
| 389 |  |  |  |  |  |  |  | 
| 390 |  |  |  |  |  |  | sub query_parser { | 
| 391 |  |  |  |  |  |  |  | 
| 392 |  |  |  |  |  |  | # Top level parser | 
| 393 |  |  |  |  |  |  |  | 
| 394 | 34 |  |  | 34 | 0 | 412 | arg_count_or_die(1, 1, @_); | 
| 395 |  |  |  |  |  |  |  | 
| 396 | 34 |  |  |  |  | 40 | my $self = shift; | 
| 397 |  |  |  |  |  |  |  | 
| 398 |  |  |  |  |  |  | # return this | 
| 399 | 34 |  |  |  |  | 41 | my %query; | 
| 400 |  |  |  |  |  |  |  | 
| 401 |  |  |  |  |  |  | my $type = $self->any_of( | 
| 402 |  |  |  |  |  |  | sub { | 
| 403 | 34 |  |  | 34 |  | 446 | $self->expect( 'all' ); | 
| 404 | 2 |  |  |  |  | 148 | $query{type} = 'all'; | 
| 405 |  |  |  |  |  |  | }, | 
| 406 |  |  |  |  |  |  | sub { | 
| 407 | 32 |  |  | 32 |  | 2625 | $self->expect( 'newest' ); | 
| 408 | 2 |  |  |  |  | 97 | $query{type} = 'newest'; | 
| 409 |  |  |  |  |  |  | }, | 
| 410 |  |  |  |  |  |  | sub { | 
| 411 | 30 |  |  | 30 |  | 1997 | $self->expect( 'oldest' ); | 
| 412 | 2 |  |  |  |  | 82 | $query{type} = 'oldest'; | 
| 413 |  |  |  |  |  |  | }, | 
| 414 |  |  |  |  |  |  | sub { | 
| 415 | 28 |  |  | 28 |  | 1922 | $self->expect( 'before' ); | 
| 416 | 4 |  |  |  |  | 161 | $self->commit; | 
| 417 | 4 |  |  |  |  | 29 | $self->skip_ws; | 
| 418 | 4 |  |  |  |  | 42 | $query{type} = 'before'; | 
| 419 | 4 |  |  |  |  | 7 | $query{target} = $self->time_abbreviation_parser; | 
| 420 |  |  |  |  |  |  | }, | 
| 421 |  |  |  |  |  |  | sub { | 
| 422 | 24 |  |  | 24 |  | 1540 | $self->expect( 'after' ); | 
| 423 | 4 |  |  |  |  | 156 | $self->commit; | 
| 424 | 4 |  |  |  |  | 354 | $self->skip_ws; | 
| 425 | 4 |  |  |  |  | 44 | $query{type} = 'after'; | 
| 426 | 4 |  |  |  |  | 8 | $query{target} = $self->time_abbreviation_parser; | 
| 427 |  |  |  |  |  |  | }, | 
| 428 |  |  |  |  |  |  | sub { | 
| 429 | 20 |  |  | 20 |  | 1411 | $self->expect( 'between' ); | 
| 430 | 4 |  |  |  |  | 164 | $self->commit; | 
| 431 | 4 |  |  |  |  | 76 | $self->skip_ws; | 
| 432 | 4 |  |  |  |  | 49 | $query{type} = 'between'; | 
| 433 | 4 |  |  |  |  | 8 | $query{target1} = $self->time_abbreviation_parser; | 
| 434 | 3 |  |  |  |  | 9 | $self->commit; | 
| 435 | 3 |  |  |  |  | 20 | $self->skip_ws; | 
| 436 | 3 |  |  |  |  | 43 | $query{target2} = $self->time_abbreviation_parser; | 
| 437 |  |  |  |  |  |  | }, | 
| 438 |  |  |  |  |  |  | sub { | 
| 439 | 16 |  |  | 16 |  | 1008 | my $time = $self->time_abbreviation_parser; | 
| 440 | 10 |  |  |  |  | 18 | $query{type} = 'closest'; | 
| 441 | 10 |  |  |  |  | 37 | $query{target} = $time; | 
| 442 |  |  |  |  |  |  | }, | 
| 443 |  |  |  |  |  |  | sub { | 
| 444 | 6 |  |  | 6 |  | 231 | $self->commit; | 
| 445 | 6 |  |  |  |  | 36 | $self->skip_ws; | 
| 446 | 6 |  |  |  |  | 54 | $self->fail(q(expected  or one of 'all', 'newest', 'oldest', 'before', 'after', 'between')) | 
| 447 |  |  |  |  |  |  | } | 
| 448 | 34 |  |  |  |  | 271 | ); | 
| 449 |  |  |  |  |  |  |  | 
| 450 | 24 |  |  |  |  | 201 | 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 | 81 | arg_count_or_die(1, 1, @_); | 
| 458 |  |  |  |  |  |  |  | 
| 459 | 31 |  |  |  |  | 35 | 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 |  | 1059 | $self->commit; | 
| 466 | 10 |  |  |  |  | 71 | $self->skip_ws; | 
| 467 | 10 |  |  |  |  | 103 | $self->fail('expected time abbreviation'); | 
| 468 |  |  |  |  |  |  | } | 
| 469 | 31 |  |  |  |  | 105 | ); | 
| 470 |  |  |  |  |  |  |  | 
| 471 | 21 |  |  |  |  | 79 | 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 | 365 | arg_count_or_die(1, 1, @_); | 
| 482 |  |  |  |  |  |  |  | 
| 483 | 31 |  |  |  |  | 37 | my $self = shift; | 
| 484 |  |  |  |  |  |  |  | 
| 485 | 31 |  |  |  |  | 92 | $self->expect( qr/b(ack)?/ ); | 
| 486 | 11 |  |  |  |  | 418 | $self->expect('-'); | 
| 487 | 11 |  |  |  |  | 441 | my $amount = $self->expect(qr/[1-9][0-9]*/); | 
| 488 | 11 |  |  |  |  | 379 | $self->expect('-'); | 
| 489 | 11 |  |  |  |  | 342 | my $unit = $self->expect(qr/days|d|hours|hrs|h|minutes|mins|m/); | 
| 490 |  |  |  |  |  |  |  | 
| 491 | 11 |  |  |  |  | 629 | 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 | 1363 | arg_count_or_die(1, 1, @_); | 
| 499 |  |  |  |  |  |  |  | 
| 500 | 20 |  |  |  |  | 20 | my $self = shift; | 
| 501 |  |  |  |  |  |  |  | 
| 502 | 20 |  |  |  |  | 62 | my $yr; | 
| 503 |  |  |  |  |  |  | my $mon; | 
| 504 | 20 |  |  |  |  | 0 | my $day; | 
| 505 | 20 |  |  |  |  | 0 | my $hr; | 
| 506 | 20 |  |  |  |  | 0 | my $min; | 
| 507 |  |  |  |  |  |  |  | 
| 508 | 20 |  |  |  |  | 111 | 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 |  | 221 | my $yr_ = $self->expect($time_regex{yr}); | 
| 518 | 9 |  |  |  |  | 364 | $self->expect('_'); | 
| 519 | 9 |  |  |  |  | 325 | my $mon_ = $self->expect($time_regex{mon}); | 
| 520 | 9 |  |  |  |  | 424 | $self->expect('_'); | 
| 521 | 7 |  |  |  |  | 224 | my $day_ = $self->expect($time_regex{day}); | 
| 522 | 7 |  |  |  |  | 288 | $self->expect('_'); | 
| 523 | 5 |  |  |  |  | 162 | my $hr_ = $self->expect($time_regex{hr}); | 
| 524 | 5 |  |  |  |  | 215 | $self->expect(':'); | 
| 525 | 4 |  |  |  |  | 145 | my $min_ = $self->expect($time_regex{min}); | 
| 526 |  |  |  |  |  |  | $self->any_of( | 
| 527 | 4 |  |  |  |  | 53 | sub { $self->expect(qr/[ ]+/) }, | 
| 528 | 4 | 50 |  |  |  | 265 | sub { $self->at_eos or $self->fail; } | 
| 529 | 4 |  |  |  |  | 176 | ); | 
| 530 |  |  |  |  |  |  |  | 
| 531 | 4 |  |  |  |  | 87 | $yr  = $yr_; | 
| 532 | 4 |  |  |  |  | 6 | $mon = $mon_; | 
| 533 | 4 |  |  |  |  | 5 | $day = $day_; | 
| 534 | 4 |  |  |  |  | 6 | $hr  = $hr_; | 
| 535 | 4 |  |  |  |  | 10 | $min = $min_; | 
| 536 |  |  |  |  |  |  | }, | 
| 537 |  |  |  |  |  |  |  | 
| 538 |  |  |  |  |  |  | sub { # yr_mon_day | 
| 539 | 16 |  |  | 16 |  | 1027 | my $yr_ = $self->expect($time_regex{yr}); | 
| 540 | 5 |  |  |  |  | 175 | $self->expect('_'); | 
| 541 | 5 |  |  |  |  | 191 | my $mon_ = $self->expect($time_regex{mon}); | 
| 542 | 5 |  |  |  |  | 202 | $self->expect('_'); | 
| 543 | 3 |  |  |  |  | 90 | my $day_ = $self->expect($time_regex{day}); | 
| 544 |  |  |  |  |  |  | $self->any_of( | 
| 545 | 3 |  |  |  |  | 46 | sub { $self->expect(qr/[ ]+/) }, | 
| 546 | 3 | 100 |  |  |  | 198 | sub { $self->at_eos or $self->fail; } | 
| 547 | 3 |  |  |  |  | 126 | ); | 
| 548 |  |  |  |  |  |  |  | 
| 549 | 1 |  |  |  |  | 23 | $yr  = $yr_; | 
| 550 | 1 |  |  |  |  | 2 | $mon = $mon_; | 
| 551 | 1 |  |  |  |  | 3 | $day = $day_; | 
| 552 |  |  |  |  |  |  | }, | 
| 553 |  |  |  |  |  |  |  | 
| 554 |  |  |  |  |  |  | sub { # mon_day_hr:min | 
| 555 | 15 |  |  | 15 |  | 820 | my $mon_ = $self->expect($time_regex{mon}); | 
| 556 | 10 |  |  |  |  | 444 | $self->expect('_'); | 
| 557 | 3 |  |  |  |  | 109 | my $day_ = $self->expect($time_regex{day}); | 
| 558 | 3 |  |  |  |  | 134 | $self->expect('_'); | 
| 559 | 2 |  |  |  |  | 72 | my $hr_ = $self->expect($time_regex{hr}); | 
| 560 | 2 |  |  |  |  | 86 | $self->expect(':'); | 
| 561 | 1 |  |  |  |  | 37 | my $min_ = $self->expect($time_regex{min}); | 
| 562 |  |  |  |  |  |  | $self->any_of( | 
| 563 | 1 |  |  |  |  | 15 | sub { $self->expect(qr/[ ]+/) }, | 
| 564 | 1 | 50 |  |  |  | 67 | sub { $self->at_eos or $self->fail; } | 
| 565 | 1 |  |  |  |  | 54 | ); | 
| 566 |  |  |  |  |  |  |  | 
| 567 | 1 |  |  |  |  | 23 | $mon = $mon_; | 
| 568 | 1 |  |  |  |  | 1 | $day = $day_; | 
| 569 | 1 |  |  |  |  | 2 | $hr  = $hr_; | 
| 570 | 1 |  |  |  |  | 3 | $min = $min_; | 
| 571 |  |  |  |  |  |  | }, | 
| 572 |  |  |  |  |  |  |  | 
| 573 |  |  |  |  |  |  | sub { # mon_day_hr | 
| 574 | 14 |  |  | 14 |  | 901 | my $mon_ = $self->expect($time_regex{mon}); | 
| 575 | 9 |  |  |  |  | 363 | $self->expect('_'); | 
| 576 | 2 |  |  |  |  | 67 | my $day_ = $self->expect($time_regex{day}); | 
| 577 | 2 |  |  |  |  | 77 | $self->expect('_'); | 
| 578 | 1 |  |  |  |  | 31 | my $hr_ = $self->expect($time_regex{hr}); | 
| 579 |  |  |  |  |  |  | $self->any_of( | 
| 580 | 1 |  |  |  |  | 28 | sub { $self->expect(qr/[ ]+/) }, | 
| 581 | 1 | 50 |  |  |  | 71 | sub { $self->at_eos or $self->fail; } | 
| 582 | 1 |  |  |  |  | 44 | ); | 
| 583 |  |  |  |  |  |  |  | 
| 584 | 1 |  |  |  |  | 23 | $mon = $mon_; | 
| 585 | 1 |  |  |  |  | 3 | $day = $day_; | 
| 586 | 1 |  |  |  |  | 2 | $hr  = $hr_; | 
| 587 |  |  |  |  |  |  | }, | 
| 588 |  |  |  |  |  |  |  | 
| 589 |  |  |  |  |  |  | sub { # mon_day | 
| 590 | 13 |  |  | 13 |  | 725 | my $mon_ = $self->expect($time_regex{mon}); | 
| 591 | 8 |  |  |  |  | 316 | $self->expect('_'); | 
| 592 | 1 |  |  |  |  | 32 | my $day_ = $self->expect($time_regex{day}); | 
| 593 |  |  |  |  |  |  | $self->any_of( | 
| 594 | 1 |  |  |  |  | 24 | sub { $self->expect(qr/[ ]+/) }, | 
| 595 | 1 | 50 |  |  |  | 72 | sub { $self->at_eos or $self->fail; } | 
| 596 | 1 |  |  |  |  | 41 | ); | 
| 597 |  |  |  |  |  |  |  | 
| 598 | 1 |  |  |  |  | 23 | $mon = $mon_; | 
| 599 | 1 |  |  |  |  | 3 | $day = $day_; | 
| 600 |  |  |  |  |  |  | }, | 
| 601 |  |  |  |  |  |  |  | 
| 602 |  |  |  |  |  |  | sub { # day_hr:min | 
| 603 | 12 |  |  | 12 |  | 654 | my $day_ = $self->expect($time_regex{day}); | 
| 604 | 7 |  |  |  |  | 292 | $self->expect('_'); | 
| 605 | 1 |  |  |  |  | 33 | my $hr_ = $self->expect($time_regex{hr}); | 
| 606 | 1 |  |  |  |  | 42 | $self->expect(':'); | 
| 607 | 1 |  |  |  |  | 37 | my $min_ = $self->expect($time_regex{min}); | 
| 608 |  |  |  |  |  |  | $self->any_of( | 
| 609 | 1 |  |  |  |  | 14 | sub { $self->expect(qr/[ ]+/) }, | 
| 610 | 1 | 50 |  |  |  | 62 | sub { $self->at_eos or $self->fail; } | 
| 611 | 1 |  |  |  |  | 39 | ); | 
| 612 |  |  |  |  |  |  |  | 
| 613 | 1 |  |  |  |  | 22 | $day = $day_; | 
| 614 | 1 |  |  |  |  | 2 | $hr  = $hr_; | 
| 615 | 1 |  |  |  |  | 2 | $min = $min_; | 
| 616 |  |  |  |  |  |  | }, | 
| 617 |  |  |  |  |  |  |  | 
| 618 |  |  |  |  |  |  | sub { # hr:min | 
| 619 | 11 |  |  | 11 |  | 698 | my $hr_ = $self->expect($time_regex{hr}); | 
| 620 | 6 |  |  |  |  | 287 | $self->expect(':'); | 
| 621 | 1 |  |  |  |  | 41 | my $min_ = $self->expect($time_regex{min}); | 
| 622 |  |  |  |  |  |  | $self->any_of( | 
| 623 | 1 |  |  |  |  | 14 | sub { $self->expect(qr/[ ]+/) }, | 
| 624 | 1 | 50 |  |  |  | 65 | sub { $self->at_eos or $self->fail; } | 
| 625 | 1 |  |  |  |  | 41 | ); | 
| 626 |  |  |  |  |  |  |  | 
| 627 | 1 |  |  |  |  | 23 | $hr  = $hr_; | 
| 628 | 1 |  |  |  |  | 3 | $min = $min_; | 
| 629 |  |  |  |  |  |  | } | 
| 630 | 20 |  |  |  |  | 225 | ); | 
| 631 |  |  |  |  |  |  |  | 
| 632 | 10 |  |  |  |  | 125 | my $t = localtime; | 
| 633 |  |  |  |  |  |  |  | 
| 634 | 10 |  | 66 |  |  | 568 | $yr  //= $t->year; | 
| 635 | 10 |  | 66 |  |  | 43 | $mon //= $t->mon; | 
| 636 | 10 |  | 66 |  |  | 26 | $day //= $t->mday; | 
| 637 | 10 |  | 100 |  |  | 25 | $hr  //= 0; | 
| 638 | 10 |  | 100 |  |  | 31 | $min //= 0; | 
| 639 |  |  |  |  |  |  |  | 
| 640 | 10 |  |  |  |  | 30 | 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 | 737 | arg_count_or_die(2, 2, @_); | 
| 652 |  |  |  |  |  |  |  | 
| 653 | 27 |  |  |  |  | 35 | my $n    = shift; | 
| 654 | 27 |  |  |  |  | 39 | my $unit = shift; | 
| 655 |  |  |  |  |  |  |  | 
| 656 | 27 | 100 | 100 |  |  | 157 | unless ($n =~ /^\d+$/ && $n > 0) { | 
| 657 | 2 |  |  |  |  | 22 | confess "yabsm: internal error: '$n' is not a positive integer"; | 
| 658 |  |  |  |  |  |  | } | 
| 659 |  |  |  |  |  |  |  | 
| 660 | 25 |  |  |  |  | 37 | my $seconds_per_unit; | 
| 661 |  |  |  |  |  |  |  | 
| 662 | 25 | 100 |  |  |  | 75 | if    ($unit =~ /^(?:minutes|mins|m)$/) { $seconds_per_unit = 60    } | 
|  | 13 | 100 |  |  |  | 16 |  | 
|  |  | 100 |  |  |  |  |  | 
| 663 | 9 |  |  |  |  | 15 | elsif ($unit =~ /^(?:hours|hrs|h)$/   ) { $seconds_per_unit = 3600  } | 
| 664 | 2 |  |  |  |  | 3 | elsif ($unit =~ /^(?:days|d)$/        ) { $seconds_per_unit = 86400 } | 
| 665 |  |  |  |  |  |  | else { | 
| 666 | 1 |  |  |  |  | 16 | confess "yabsm: internal error: '$unit' is not a valid time unit"; | 
| 667 |  |  |  |  |  |  | } | 
| 668 |  |  |  |  |  |  |  | 
| 669 | 24 |  |  |  |  | 55 | my $t = localtime; | 
| 670 |  |  |  |  |  |  |  | 
| 671 | 24 |  |  |  |  | 1509 | my ($yr, $mon, $day, $hr, $min) = ($t->year, $t->mon, $t->mday, $t->hour, $t->min); | 
| 672 |  |  |  |  |  |  |  | 
| 673 | 24 |  |  |  |  | 387 | my $tp = Time::Piece->strptime("$yr/$mon/$day/$hr/$min", '%Y/%m/%d/%H/%M'); | 
| 674 |  |  |  |  |  |  |  | 
| 675 | 24 |  |  |  |  | 1173 | $tp -= $n * $seconds_per_unit; | 
| 676 |  |  |  |  |  |  |  | 
| 677 | 24 |  |  |  |  | 1080 | return nums_to_snapshot_name($tp->year, $tp->mon, $tp->mday, $tp->hour, $tp->min); | 
| 678 |  |  |  |  |  |  | } | 
| 679 |  |  |  |  |  |  |  | 
| 680 |  |  |  |  |  |  | 1; |