File Coverage

bin/sibs
Criterion Covered Total %
statement 137 186 73.6
branch 54 104 51.9
condition 10 25 40.0
subroutine 21 25 84.0
pod n/a
total 222 340 65.2


line stmt bran cond sub pod time code
1             #!/usr/bin/perl
2 3     3   2445 use strict;
  3         8  
  3         123  
3 3     3   17 use warnings;
  3         7  
  3         123  
4 3     3   19 use Fcntl qw( O_CREAT O_EXCL );
  3         6  
  3         216  
5 3     3   3124 use POSIX ();
  3         23940  
  3         94  
6 3     3   3062 use URI;
  3         19295  
  3         9759  
7              
8             our @DEFAULT_EXCLUDE = qw( .cache .cpanm .gvfs Downloads Dropbox Trash );
9             our $SSH = 'ssh';
10             our $SSH_KEYGEN = 'ssh-keygen';
11             our $LOCK;
12              
13             $ENV{HOME} ||= 'ENVIRONMENT_HOME_IS_NOT_SET';
14              
15             END {
16 1 50   1   866 unlink $LOCK if $LOCK;
17             }
18              
19             sub run_rsync {
20 0     0   0 my $self = shift;
21 0         0 my $uri = $self->{destination};
22 0         0 my $lock = sprintf '%s.backup.lock', $self->{config};
23 0         0 my @options = qw( -az --delete-after --numeric-ids --relative );
24              
25 0 0       0 push @options, map { qq(--exclude=$_) } @{ $self->{exclude} || [] };
  0         0  
  0         0  
26 0 0       0 push @options, '--verbose' if $self->{verbose};
27 0         0 push @options, @{ $self->{source} };
  0         0  
28 0         0 push @options, sprintf '%s@%s:%s/incoming', $uri->userinfo, $self->remote_host, $uri->path;
29              
30 0 0       0 IO::File->new->open($lock, O_CREAT | O_EXCL) or die "Already backing up. ($lock)\n";
31 0         0 $LOCK = $lock;
32 0         0 $self->_system(rsync => @options);
33             }
34              
35             sub create_sibs_config {
36 1     1   540 my $self = shift;
37 1         5 my $tmp = sprintf '%s.tmp', $self->{config};
38              
39 1 50       83 open my $CONFIG, '>', $tmp or die "Cannot write $tmp: $!\n";
40 1         2 local $_;
41 1   33     8 $self->_log($@ || "Creating '$self->{config}' from user input...");
42              
43 1         13 print $CONFIG "{\n";
44 1 50       4 printf $CONFIG " email => '%s',\n", $_ if $self->_read('email');
45 1 50       14 printf $CONFIG " exclude => [qw( %s )],\n", $_ if $self->_read('exclude');
46 1 50       7 printf $CONFIG " source => [qw( %s )],\n", $_ if $self->_read('source');
47 1 50       8 printf $CONFIG " destination => '%s',\n", $_ if $self->_read('destination');
48 1         6 print $CONFIG "}\n";
49              
50 1 50       44 close $CONFIG or die "Could not write '$tmp': $!\n";
51 1 50       65 rename $tmp, $self->{config} or die "Could not write '$self->{config}: $!'\n";
52             }
53              
54             sub add_backup_host_to_ssh_config {
55 2     2   5188 my $self = shift;
56 2         7 my $moniker = $self->remote_host;
57 2         8 my $file = $self->ssh_file('config');
58              
59 2 100       57 if(-r $file) {
60 1 50       35 open my $CONFIG, '<', $file or die "Could not open $file: $!";
61 1         43 while(<$CONFIG>) {
62 2 100       29 next unless /Host\s+$moniker/;
63 1         13 $self->_log("Host $moniker exists in $file.");
64 1         15 return 1;
65             }
66             }
67              
68 1         17 $self->_log("Adding $moniker to $file");
69 1 50       109 open my $CONFIG, '>>', $file or die "Cannot write to $file: $!";
70 1         3 printf $CONFIG "\nHost %s\n", $self->remote_host;
71 1         5 printf $CONFIG " Hostname %s\n", $self->{destination}->host;
72 1         24 printf $CONFIG " IdentityFile %s\n", $self->ssh_file('sibs_dsa');
73 1         68 close $CONFIG;
74             }
75              
76             sub create_identity_file {
77 1     1   14687 my $self = shift;
78 1         7 my $file = $self->ssh_file('sibs_dsa');
79 1         2 my $identity;
80              
81 1 50       19 if(-r $file) {
82 0         0 $self->_log("Identity file '$file' exists");
83             }
84             else {
85 1         18 $self->_log("Creating $file with empty password using ssh-keygen ...");
86 1         14 $self->_system($SSH_KEYGEN => -P => '', -t => 'dsa', -f => $file);
87             }
88              
89 1         190 $self->_log("Copying pub key to remote host ...");
90 1 50       352 open my $IDENTITY, '<', "$file.pub" or die "Cannot read $file.pub: $!";
91 1     1   31 $self->run_sibs_remote(sub { readline $IDENTITY }, 'remote-init');
  1         41  
92             }
93              
94             sub remote_add_pub_key {
95 2     2   12055 my($self, $key) = @_;
96 2         11 my $file = $self->ssh_file('authorized_keys');
97              
98 2 100       63 if(-r $file) {
99 1         4 my $match = quotemeta $key;
100 1 50       38 open my $AUTHORIZED_KEYS, '<', $file or die "Could not open $file: $!";
101 1         36 while(<$AUTHORIZED_KEYS>) {
102 1 50       34 next unless /$match/;
103 1         4 $self->_log("Remote host has pub key");
104 1         14 return 0;
105             }
106             }
107              
108 1 50       134 open my $AUTHORIZED_KEYS, '>>', $file or die "Could not append to $file: $!\n";
109 1         6 print $AUTHORIZED_KEYS $key;
110 1 50       16 print $AUTHORIZED_KEYS "\n" unless $key =~ /\n$/;
111 1         61 close $AUTHORIZED_KEYS;
112 1         10 $self->_log("Pub key added to remote authorized_keys.");
113 1         4 return 1;
114             }
115              
116             sub remote_host {
117 4     4   988 my $self = shift;
118 4         39 my $moniker = 'sibs-' .$self->{destination}->host;
119              
120 4         161 $moniker =~ s/\./-/g;
121 4         32 $moniker;
122             }
123              
124             sub load_config {
125 1     1   4 my $self = shift;
126 1         1 my $config;
127              
128 1 50       30 open my $CONFIG, '<', $self->{config} or die "Cannot read $self->{config}: $! Run '$0 setup'\n";
129 1         21 $config = join '', <$CONFIG>;
130 1     1   72 $config = eval <<" CONFIG";
  1     1   8  
  1     1   2  
  1         42  
  1         4  
  1         2  
  1         31  
  1         5  
  1         188  
  1         118  
131             use strict;
132             use warnings;
133             use File::Basename;
134             $config
135             CONFIG
136              
137 1 50       5 $config or die "Invalid config file: ($@)\n";
138 1   50     4 $config->{exclude} ||= [ @DEFAULT_EXCLUDE ];
139 1   50     5 $config->{source} ||= [ $ENV{HOME} ];
140 1   50     11 $config->{destination} = URI->new($config->{destination} || '');
141              
142 1         15203 @{$self}{keys %$config} = values %$config;
  1         10  
143              
144 1         4 for my $m (qw( scheme host path userinfo )) {
145 4 50       325 next if $config->{destination}->$m;
146 0         0 die "[$self->{config}] Missing '$m' part for 'destination' URI\n";
147             }
148              
149 1 50       27 $config->{destination}->scheme eq 'rsync' or die "[$self->{config}] Only rsync:// is supported for 'destination' URI\n";
150             }
151              
152             sub run_sibs_remote {
153 1     1   11 my($self, @args) = @_;
154 1 50   0   16 my $stdin = ref $args[0] eq 'CODE' ? shift @args : sub { '' };
  0         0  
155 1         63 my @remote = ( $SSH => '-l' => $self->{destination}->userinfo, $self->remote_host );
156              
157 1 50       22 unshift @args, '--silent' if $self->{silent};
158 1 50       11 unshift @args, '--verbose' if $self->{verbose};
159 1         10 push @remote, qq(perl - @args);
160              
161 1 50       7078 open my $SSH, '|-', @remote or die "Cannot start 'sibs @args' remote: $!";
162 1 50       110 open my $SELF, '<', __FILE__ or die "Cannot read $0: $!";
163 1         1798 print $SSH $_ while <$SELF>;
164 1         9 print $SSH "\n__DATA__\n";
165 1         20 print $SSH $self->$stdin;
166 1         7181 close $SSH; # TODO: do i need to wait?
167             }
168              
169             sub ssh_file {
170 7     7   1687 my($self, $file) = @_;
171              
172 7 100       40 if(!$self->{ssh_dir}) {
173 2 50 50     243 mkdir "$ENV{HOME}/.ssh" or die "Could not mkdir $ENV{HOME}/.ssh: $!" unless -d "$ENV{HOME}/.ssh";
174 2         55 chmod 0700, "$ENV{HOME}/.ssh";
175 2         9 $self->{ssh_dir} = "$ENV{HOME}/.ssh";
176             }
177              
178 7 50       19 return $self->{ssh_dir} unless $file;
179 7         29 return join '/', $self->{ssh_dir}, $file;
180             }
181              
182             sub _backup_name {
183 0   0 0   0 POSIX::strftime($_[0]->{format} || '%d-%H', localtime);
184             }
185              
186             sub _log {
187 8     8   19 my $self = shift;
188 8         515 my $min = (localtime)[1];
189 8         145 my $hour = (localtime)[2];
190              
191 8 50       154 return if $self->{silent};
192 0         0 warn sprintf "[%02s:%02s] %s\n", $hour, $min, join ' ', @_;
193             }
194              
195             sub _read {
196 0     0   0 my($self, $k) = @_;
197 0         0 my $v = $self->{$k};
198 0 0       0 $v = join ' ', @$v if ref $v eq 'ARRAY';
199 0         0 local $| = 1;
200 0         0 print $k;
201 0 0       0 printf " ($v)", if $v;
202 0         0 print ": ";
203 0         0 $_ = ;
204 0         0 chomp;
205 0   0     0 $_ ||= $v;
206             }
207              
208             sub _system {
209 1     1   5 my($self, $program, @options) = @_;
210              
211 1         4 for my $path (qw( /bin /usr/bin /usr/local/bin )) {
212 3 50       110 next unless -x "$path/$program";
213 0         0 $program = "$path/$program";
214 0         0 last;
215             }
216              
217 1 100       5 $self->_log(join ' ', map { length $_ ? $_ : '""' } $program, @options);
  7         24  
218 1         8550 system $program => @options;
219             }
220              
221             sub run {
222 4     4   31 my($self, @args) = @_;
223 4         6 my $action = 'help';
224 4         8 my $i = 0;
225              
226 4         19 while($i < @args) {
227 6 100 50     78 $self->{config} = splice @args, $i, 1, () and next if -f $args[$i];
228 5 100 50     31 $self->{verbose} = splice @args, $i, 1, () and next if $args[$i] =~ /^--?v/;
229 3 100 50     23 $self->{silent} = splice @args, $i, 1, () and next if $args[$i] =~ /^--?s/;
230 1         4 $i++;
231             }
232              
233 4 100       13 $action = shift @args if @args;
234 4   66     30 $self->{config} ||= "$ENV{HOME}/.sibs.conf";
235              
236 4 50       34 if($action eq 'setup') {
    50          
    50          
    50          
    50          
    50          
    50          
237 0         0 $self->create_sibs_config until eval { $self->load_config };
  0         0  
238 0         0 $self->_log("Created $self->{config}");
239 0         0 $self->add_backup_host_to_ssh_config;
240 0         0 $self->create_identity_file;
241             }
242             elsif($action eq 'backup') {
243 0         0 $self->load_config;
244 0         0 $self->run_rsync;
245 0         0 $self->run_sibs_remote('remote-archive', $self->{destination}->path, $self->_backup_name);
246             }
247             elsif($action eq 'man') {
248 0         0 exec perldoc => 'App::sibs';
249             }
250             elsif($action eq 'remote-init') {
251 0         0 $self->remote_add_pub_key(eval 'do { local $/; }');
252             }
253             elsif($action eq 'remote-archive') {
254 0         0 my($dir, $name) = @args;
255 0 0       0 chdir $dir or die "Cannot chdir $dir: $!\n";
256 0 0       0 $self->_system(rm => -r => $name) if -d $name;
257 0         0 $self->_system(cp => "-al" => "incoming" => $name);
258 0         0 $self->_system(touch => $name);
259             }
260             elsif($action eq 'version') {
261 0         0 require App::sibs;
262 0         0 print App::sibs->VERSION, "\n";
263             }
264             elsif(!$ENV{HARNESS_IS_VERBOSE}) {
265 0         0 print <<' HELP';
266             sibs man
267             sibs setup
268             sibs backup
269             sibs version
270             HELP
271             }
272              
273 4         15 return 0;
274             }
275              
276             exit +(bless {})->run(@ARGV) unless defined wantarray;
277             bless {};