File Coverage

blib/lib/App/MBUtiny/Storage/SFTP.pm
Criterion Covered Total %
statement 43 242 17.7
branch 1 106 0.9
condition 1 46 2.1
subroutine 12 22 54.5
pod 7 7 100.0
total 64 423 15.1


line stmt bran cond sub pod time code
1             package App::MBUtiny::Storage::SFTP; # $Id: SFTP.pm 121 2019-07-01 19:51:50Z abalama $
2 2     2   20 use strict;
  2         4  
  2         64  
3 2     2   11 use utf8;
  2         4  
  2         10  
4              
5             =encoding utf-8
6              
7             =head1 NAME
8              
9             App::MBUtiny::Storage::SFTP - App::MBUtiny::Storage subclass for SFTP storage support
10              
11             =head1 VIRSION
12              
13             Version 1.00
14              
15             =head1 SYNOPSIS
16              
17            
18            
19             FixUP on
20             URL sftp://user@example.com:22/path/to/backup/dir1
21             URL sftp://user@example.com:22/path/to/backup/dir2
22             Set key_path /path/to/private/file.key
23             Comment SFTP storage said blah-blah-blah # Optional for collector
24            
25              
26             # . . .
27              
28            
29              
30             =head1 DESCRIPTION
31              
32             App::MBUtiny::Storage subclass for SFTP storage support
33              
34             B For initialization SSH connection please run follow commands first:
35              
36             ssh-keygen -t rsa
37             ssh-copy-id -i /path/to/private/file.pub user@example.com
38              
39             =head2 del
40              
41             Removes the specified file.
42             This is backend method of L
43              
44             =head2 get
45              
46             Gets the backup file from storage and saves it to specified path.
47             This is backend method of L
48              
49             =head2 init
50              
51             The method performs initialization of storage.
52             This is backend method of L
53              
54             =head2 list
55              
56             Gets backup file list on storage.
57             This is backend method of L
58              
59             =head2 sftp_storages
60              
61             my @list = $storage->sftp_storages;
62              
63             Returns list of SFTP storage nodes
64              
65             =head2 put
66              
67             Sends backup file to storage.
68             This is backend method of L
69              
70             =head2 test
71              
72             Storage testing.
73             This is backend method of L
74              
75             =head1 HISTORY
76              
77             See C file
78              
79             =head1 TO DO
80              
81             See C file
82              
83             =head1 BUGS
84              
85             * none noted
86              
87             =head1 SEE ALSO
88              
89             L
90              
91             =head1 AUTHOR
92              
93             Serż Minus (Sergey Lepenkov) L Eabalama@cpan.orgE
94              
95             =head1 COPYRIGHT
96              
97             Copyright (C) 1998-2019 D&D Corporation. All Rights Reserved
98              
99             =head1 LICENSE
100              
101             This program is free software; you can redistribute it and/or
102             modify it under the same terms as Perl itself.
103              
104             See C file and L
105              
106             =cut
107              
108 2     2   81 use vars qw/ $VERSION /;
  2         15  
  2         98  
109             $VERSION = '1.00';
110              
111 2     2   22 use Storable qw/dclone/;
  2         5  
  2         103  
112 2     2   13 use Fcntl ':mode';
  2         4  
  2         509  
113 2     2   15 use URI;
  2         5  
  2         54  
114 2     2   11 use List::Util qw/uniq/;
  2         5  
  2         168  
115 2     2   16 use CTK::ConfGenUtil;
  2         4  
  2         474  
116 2     2   15 use CTK::TFVals qw/ :ALL /;
  2         5  
  2         435  
117 2     2   15 use App::MBUtiny::Util qw/ node2anode set2attr hide_password filesize /;
  2         4  
  2         257  
118              
119             my $NET_SFTP_FOREIGN = 1; # Loaded!
120             my $NET_SFTP_FOREIGN_MESSAGE = "SFTP storage type is available";
121              
122             eval { require Net::SFTP::Foreign; 1 } or do {
123             $NET_SFTP_FOREIGN = 0;
124             $NET_SFTP_FOREIGN_MESSAGE = "SFTP storage type is not available, Net::SFTP::Foreign is not installed or failed to load: $@";
125             };
126              
127             use constant {
128 2         4768 STORAGE_SIGN => 'SFTP',
129 2     2   14 };
  2         10  
130              
131             sub init {
132 1     1 1 12 my $self = shift;
133 1         4 $self->maybe::next::method();
134 1         3 $self->storage_status(STORAGE_SIGN, -1);
135 1         2 my $usesftp = 0;
136              
137 1         4 my $sftp_nodes = dclone(node2anode(node($self->{host}, 'sftp')));
138             #print explain($ftp_nodes), "\n";
139              
140 1         3 my %sftp_storages;
141 1         3 foreach my $sftp_node (@$sftp_nodes) {
142 0   0     0 my $urls = array($sftp_node, 'url') || [];
143 0   0     0 my $gattr = set2attr($sftp_node),
144             my $cmnt = value($sftp_node, 'comment') || "";
145 0         0 foreach my $url (@$urls) {
146 0         0 my $uri = new URI($url);
147 0         0 my $url_wop = hide_password($url, 2);
148 0         0 my $attr = dclone($gattr);
149 0         0 $attr->{host} = $uri->host;
150 0 0       0 $attr->{port} = $uri->port if $uri->port;
151 0 0       0 $attr->{user} = $uri->user if $uri->user;
152 0 0       0 $attr->{password} = $uri->password if $uri->password;
153             $sftp_storages{$url} = {
154             url => $url,
155             url_wop => $url_wop,
156             path => $uri->path,
157             attr => $attr,
158 0 0       0 comment => join("\n", grep {$_} ($url_wop, $cmnt)),
  0         0  
159             fixup => value($sftp_node, 'fixup') ? 1 : 0,
160             };
161 0         0 $usesftp++;
162             }
163             }
164 1         4 $self->{sftp_storages} = [(values(%sftp_storages))];
165              
166 1 50 33     4 $self->storage_status(STORAGE_SIGN, $usesftp) if $usesftp && $NET_SFTP_FOREIGN;
167             #print explain($self->{sftp_storages}), "\n";
168 1         3 return $self;
169             }
170             sub sftp_storages {
171 0     0 1   my $self = shift;
172 0   0       my $storages = $self->{sftp_storages} || [];
173 0           return @$storages;
174             }
175             sub test {
176 0     0 1   my $self = shift;
177 0           my %params = @_; $self->maybe::next::method(%params);
  0            
178 0           my $sign = STORAGE_SIGN;
179 0 0 0       return $self->storage_status($sign)
180             if is_void($self->sftp_storages()) && $self->storage_status($sign) <= 0; # SKIP
181 0 0         unless ($NET_SFTP_FOREIGN) { # Not loaded
182 0           $self->{test}->{$sign} = [[$NET_SFTP_FOREIGN_MESSAGE, -1]];
183 0           return $self->storage_status($sign);
184             }
185              
186 0           my @test = ();
187 0           foreach my $storage ($self->sftp_storages) {
188 0           my $url_wop = $storage->{url_wop};
189 0           my $attr = $storage->{attr};
190              
191             # Create object
192 0           my $sftp = new Net::SFTP::Foreign(%$attr);
193 0 0         if ($sftp->error) {
194 0           $self->storage_status($sign, 0);
195 0           push @test, [0, $url_wop, sprintf("Can't connect to %s: %s", $url_wop, $sftp->error)];
196 0           next;
197             }
198              
199             # Change dir
200 0 0         if (my $path = $storage->{path}) {
201 0 0         $sftp->setcwd($path) or do {
202 0           $self->storage_status($sign, 0);
203 0           push @test, [0, $url_wop, sprintf("Can't change directory %s on %s: %s", $path, $url_wop, $sftp->error)];
204 0           next;
205             };
206             }
207              
208             # Get pwd
209 0           my $pwd = $sftp->cwd;
210 0 0         if ($sftp->error) {
211 0           $self->storage_status($sign, 0);
212 0           push @test, [0, $url_wop, sprintf("Can't get cwd on %s: %s", $url_wop, $sftp->error)];
213 0           next;
214             }
215              
216             # Disconnect
217 0           $sftp->disconnect;
218              
219 0           push @test, [1, $url_wop];
220             }
221              
222 0           $self->{test}->{$sign} = [@test];
223 0           return 1;
224             }
225             sub put {
226 0     0 1   my $self = shift;
227 0           my %params = @_; $self->maybe::next::method(%params);
  0            
228 0 0         return $self->storage_status(STORAGE_SIGN, -1) if $self->storage_status(STORAGE_SIGN) <= 0; # SKIP and set SKIP
229 0           my $status = 1;
230 0           my $name = $params{name}; # File name only
231 0           my $file = $params{file}; # Path to local file
232 0   0       my $src_size = $params{size} || 0;
233              
234 0           foreach my $storage ($self->sftp_storages) {
235 0           my $url_wop = $storage->{url_wop};
236 0           my $attr = $storage->{attr};
237 0   0       my $comment = $storage->{comment} || "";
238 0           my $path = $storage->{path};
239 0           my $ostat = 1;
240              
241             # Create object
242 0           my $sftp = new Net::SFTP::Foreign(%$attr);
243 0 0         if ($sftp->error) {
244 0           $self->error(sprintf("Can't connect to %s: %s", $url_wop, $sftp->error));
245 0           $ostat = 0;
246             }
247              
248             # Change dir
249 0 0 0       if ($ostat && length($path)) {
250 0 0         $sftp->setcwd($path) or do {
251 0           $self->error(sprintf("Can't change directory %s on %s: %s", $path, $url_wop, $sftp->error));
252 0           $ostat = 0;
253             };
254             }
255              
256             # Put file
257 0 0         if ($ostat) {
258 0 0         $sftp->put($file, $name) or do {
259 0           $self->error(sprintf("Can't put file %s to %s: %s", $name, $url_wop, $sftp->error));
260 0           $ostat = 0;
261             };
262             }
263              
264             # Get file size
265 0 0         if ($ostat) {
266             my $ls = $sftp->ls(wanted => sub {
267 0     0     my $foreign = shift;
268 0           my $entry = shift;
269 0 0         S_ISREG($entry->{a}->perm) && $entry->{filename} eq $name;
270 0           });
271             #print explain($ls), "\n";
272 0 0 0       if ($sftp->error) {
    0          
273 0           $self->error(sprintf("The ls failed: %s", $sftp->error));
274 0           $ostat = 0;
275             } elsif ($ls && is_array($ls)) {
276 0           my $file = shift(@$ls);
277 0   0       my $dst_size = $file->{a}->size || 0;
278 0 0         unless ($src_size == $dst_size) {
279 0           $self->error(sprintf("An error occurred while sending data to %s. Sizes are different: SRC=%d; DST=%d", $url_wop, $src_size, $dst_size));
280 0           $ostat = 0;
281             }
282             }
283             }
284              
285             # Disconnect
286 0           $sftp->disconnect;
287              
288             # Fixup!
289 0 0         $self->fixup("put", $ostat, $comment) if $storage->{fixup};
290 0 0         $status = 0 unless $ostat;
291             }
292              
293 0 0         $self->storage_status(STORAGE_SIGN, 0) unless $status;
294             }
295             sub get {
296 0     0 1   my $self = shift;
297 0           my %params = @_;
298 0 0         if ($self->storage_status(STORAGE_SIGN) <= 0) { # SKIP and set SKIP
299 0           $self->maybe::next::method(%params);
300 0           return $self->storage_status(STORAGE_SIGN, -1);
301             }
302 0           my $name = $params{name}; # archive name
303 0           my $file = $params{file}; # destination archive file path
304              
305 0           foreach my $storage ($self->sftp_storages) {
306 0           my $url_wop = $storage->{url_wop};
307 0           my $attr = $storage->{attr};
308 0           my $path = $storage->{path};
309              
310             # Create object
311 0           my $sftp = new Net::SFTP::Foreign(%$attr);
312 0 0         if ($sftp->error) {
313 0           $self->error(sprintf("Can't connect to %s: %s", $url_wop, $sftp->error));
314 0           next;
315             }
316              
317             # Change dir
318 0 0         if (length($path)) {
319 0 0         $sftp->setcwd($path) or do {
320 0           $self->error(sprintf("Can't change directory %s on %s: %s", $path, $url_wop, $sftp->error));
321 0           $sftp->disconnect;
322 0           next;
323             };
324             }
325              
326             # Get file size
327 0           my $src_size = 0;
328             my $ls = $sftp->ls(wanted => sub {
329 0     0     my $foreign = shift;
330 0           my $entry = shift;
331 0 0         S_ISREG($entry->{a}->perm) && $entry->{filename} eq $name;
332 0           });
333 0 0 0       if ($sftp->error) {
    0          
334 0           $self->error(sprintf("The ls failed: %s", $sftp->error));
335 0           $sftp->disconnect;
336 0           next;
337             } elsif ($ls && is_array($ls)) {
338 0           my $file = shift(@$ls);
339 0   0       $src_size = $file->{a}->size || 0;
340             }
341              
342             # Get file
343 0 0         $sftp->get($name, $file) or do {
344 0           $self->error(sprintf("Can't get file %s from %s: %s", $name, $url_wop, $sftp->error));
345 0           $sftp->disconnect;
346 0           next;
347             };
348              
349             # Check size
350 0   0       my $dst_size = filesize($file) // 0;
351 0 0         unless ($src_size == $dst_size) {
352 0           $self->error(sprintf("An error occurred while sending data to %s. Sizes are different: SRC=%d; DST=%d", $url_wop, $src_size, $dst_size));
353 0           $sftp->disconnect;
354 0           next;
355             }
356              
357             # Disconnect
358 0           $sftp->disconnect;
359              
360             # Validate
361 0 0         unless ($self->validate($file)) { # FAIL validation!
362 0           $self->error(sprintf("SFTP storage %s failed: file %s is not valid!", $url_wop, $file));
363             next
364 0           }
365              
366             # Done!
367 0           return $self->storage_status(STORAGE_SIGN, 1);
368             }
369              
370 0           $self->storage_status(STORAGE_SIGN, 0);
371 0           $self->maybe::next::method(%params);
372             }
373             sub del {
374 0     0 1   my $self = shift;
375 0           my $name = shift;
376 0           $self->maybe::next::method($name);
377 0 0         return $self->storage_status(STORAGE_SIGN, -1) if $self->storage_status(STORAGE_SIGN) <= 0; # SKIP and set SKIP
378 0           my $status = 1;
379              
380 0           foreach my $storage ($self->sftp_storages) {
381 0           my $url_wop = $storage->{url_wop};
382 0           my $attr = $storage->{attr};
383 0           my $path = $storage->{path};
384 0           my $ostat = 1;
385              
386             # Create object
387 0           my $sftp = new Net::SFTP::Foreign(%$attr);
388 0 0         if ($sftp->error) {
389 0           $self->error(sprintf("Can't connect to %s: %s", $url_wop, $sftp->error));
390 0           $ostat = 0;
391             }
392              
393             # Change dir
394 0 0 0       if ($ostat && length($path)) {
395 0 0         $sftp->setcwd($path) or do {
396 0           $self->error(sprintf("Can't change directory %s on %s: %s", $path, $url_wop, $sftp->error));
397 0           $ostat = 0;
398             };
399             }
400              
401             # Delete file
402 0 0         if ($ostat) {
403             my $ls = $sftp->ls(wanted => sub {
404 0     0     my $foreign = shift;
405 0           my $entry = shift;
406 0 0         S_ISREG($entry->{a}->perm) && $entry->{filename} eq $name;
407 0           });
408             #print explain($ls), "\n";
409 0 0 0       if ($sftp->error) {
    0 0        
410 0           $self->error(sprintf("The ls failed: %s", $sftp->error));
411 0           $ostat = 0;
412             } elsif ($ls && is_array($ls) && @$ls) {
413 0 0         $sftp->remove($name) or do {
414 0           $self->error(sprintf("Can't delete file %s from %s: %s", $name, $url_wop, $sftp->error));
415 0           $ostat = 0;
416             };
417             }
418             }
419              
420             # Disconnect
421 0           $sftp->disconnect;
422              
423             # Fixup!
424 0 0         $self->fixup("del", $name) if $storage->{fixup};
425 0 0         $status = 0 unless $ostat;
426             }
427 0 0         $self->storage_status(STORAGE_SIGN, 0) unless $status;
428             }
429             sub list {
430 0     0 1   my $self = shift;
431 0           my %params = @_; $self->maybe::next::method(%params);
  0            
432 0 0         return $self->storage_status(STORAGE_SIGN, -1) if $self->storage_status(STORAGE_SIGN) <= 0; # SKIP and set SKIP
433 0           my $sign = STORAGE_SIGN;
434              
435 0           my @list = ();
436 0           foreach my $storage ($self->sftp_storages) {
437 0           my $url_wop = $storage->{url_wop};
438 0           my $attr = $storage->{attr};
439 0           my $path = $storage->{path};
440 0           my $ostat = 1;
441              
442             # Create object
443 0           my $sftp = new Net::SFTP::Foreign(%$attr);
444 0 0         if ($sftp->error) {
445 0           $self->error(sprintf("Can't connect to %s: %s", $url_wop, $sftp->error));
446 0           $ostat = 0;
447             }
448              
449             # Get file list
450 0 0         if ($ostat) {
451             my $ls = $sftp->ls($path, wanted => sub {
452 0     0     my $foreign = shift;
453 0           my $entry = shift;
454             S_ISREG($entry->{a}->perm)
455             #$entry->{filename} eq $name;
456 0           });
  0            
457 0 0 0       if ($sftp->error) {
    0          
458 0           $self->error(sprintf("The ls failed: %s", $sftp->error));
459 0           $ostat = 0;
460             } elsif ($ls && is_array($ls)) {
461 0           foreach (@$ls) {
462 0           my $f = $_->{filename};
463 0 0 0       push @list, $f if defined($f) && length($f);
464             }
465             }
466             }
467              
468             # Disconnect
469 0           $sftp->disconnect;
470             }
471              
472 0           $self->{list}->{$sign} = [uniq(@list)];
473 0           return 1;
474             }
475              
476             1;
477              
478             __END__