File Coverage

blib/lib/App/MBUtiny/Storage/HTTP.pm
Criterion Covered Total %
statement 61 308 19.8
branch 1 122 0.8
condition 0 70 0.0
subroutine 18 34 52.9
pod 7 7 100.0
total 87 541 16.0


line stmt bran cond sub pod time code
1             package App::MBUtiny::Storage::HTTP; # $Id: HTTP.pm 121 2019-07-01 19:51:50Z abalama $
2 3     3   584 use strict;
  3         8  
  3         89  
3 3     3   19 use utf8;
  3         7  
  3         19  
4              
5             =encoding utf-8
6              
7             =head1 NAME
8              
9             App::MBUtiny::Storage::HTTP - App::MBUtiny::Storage subclass for HTTP storage support
10              
11             =head1 VIRSION
12              
13             Version 1.00
14              
15             =head1 SYNOPSIS
16              
17            
18            
19             FixUP on
20             URL https://user:password@example.com/mbuserver/foo/dir1
21             URL https://user:password@example.com/mbuserver/foo/dir2
22             Set User-Agent TestServer/1.00
23             Set X-Test Foo Bar Baz
24             Comment HTTP storage said blah-blah-blah # Optional for collector
25            
26              
27             # . . .
28              
29            
30              
31             =head1 DESCRIPTION
32              
33             App::MBUtiny::Storage subclass for HTTP storage support
34              
35             =head2 del
36              
37             Removes the specified file.
38             This is backend method of L
39              
40             =head2 get
41              
42             Gets the backup file from storage and saves it to specified path.
43             This is backend method of L
44              
45             =head2 init
46              
47             The method performs initialization of storage.
48             This is backend method of L
49              
50             =head2 list
51              
52             Gets backup file list on storage.
53             This is backend method of L
54              
55             =head2 http_storages
56              
57             my @list = $storage->http_storages;
58              
59             Returns list of HTTP storage nodes
60              
61             =head2 put
62              
63             Sends backup file to storage.
64             This is backend method of L
65              
66             =head2 test
67              
68             Storage testing.
69             This is backend method of L
70              
71             =head1 HISTORY
72              
73             See C file
74              
75             =head1 TO DO
76              
77             See C file
78              
79             =head1 BUGS
80              
81             * none noted
82              
83             =head1 SEE ALSO
84              
85             L
86              
87             =head1 AUTHOR
88              
89             Serż Minus (Sergey Lepenkov) L Eabalama@cpan.orgE
90              
91             =head1 COPYRIGHT
92              
93             Copyright (C) 1998-2019 D&D Corporation. All Rights Reserved
94              
95             =head1 LICENSE
96              
97             This program is free software; you can redistribute it and/or
98             modify it under the same terms as Perl itself.
99              
100             See C file and L
101              
102             =cut
103              
104 3     3   125 use vars qw/ $VERSION /;
  3         37  
  3         233  
105             $VERSION = '1.00';
106              
107 3     3   634 use Storable qw/dclone/;
  3         3027  
  3         162  
108 3     3   556 use URI;
  3         4509  
  3         116  
109 3     3   19 use List::Util qw/uniq/;
  3         6  
  3         193  
110 3     3   18 use CTK::ConfGenUtil;
  3         7  
  3         234  
111 3     3   20 use CTK::TFVals qw/ :ALL /;
  3         5  
  3         680  
112 3     3   454 use App::MBUtiny::Util qw/ node2anode set2attr hide_password filesize /;
  3         7  
  3         215  
113              
114             use constant {
115 3         5996 STORAGE_SIGN => 'HTTP',
116 3     3   21 };
  3         6  
117              
118             sub init {
119 1     1 1 12 my $self = shift;
120 1         3 $self->maybe::next::method();
121 1         5 $self->storage_status(STORAGE_SIGN, -1);
122 1         3 my $usehttp = 0;
123              
124 1         4 my $http_nodes = dclone(node2anode(node($self->{host}, 'http')));
125             #print explain($http_nodes), "\n";
126              
127 1         3 my %http_storages;
128 1         4 foreach my $http_node (@$http_nodes) {
129 0   0     0 my $urls = array($http_node, 'url') || [];
130 0         0 my $attr = set2attr($http_node),
131             my $timeout = uv2zero(value($http_node, 'timeout'));
132 0   0     0 my $cmnt = value($http_node, 'comment') || "";
133 0         0 foreach my $url (@$urls) {
134 0         0 my $url_wop = hide_password($url, 2);
135             $http_storages{$url} = {
136             url => $url,
137             url_wop => $url_wop,
138             attr => dclone($attr),
139             timeout => $timeout,
140 0 0       0 comment => join("\n", grep {$_} ($url_wop, $cmnt)),
  0         0  
141             fixup => value($http_node, 'fixup') ? 1 : 0,
142             };
143 0         0 $usehttp++;
144             }
145             }
146 1         4 $self->{http_storages} = [(values(%http_storages))];
147              
148 1 50       4 $self->storage_status(STORAGE_SIGN, $usehttp) if $usehttp;
149             #print explain($self->{http_storages}), "\n";
150 1         3 return $self;
151             }
152             sub http_storages {
153 0     0 1   my $self = shift;
154 0   0       my $storages = $self->{http_storages} || [];
155 0           return @$storages;
156             }
157             sub test {
158 0     0 1   my $self = shift;
159 0           my %params = @_; $self->maybe::next::method(%params);
  0            
160 0           my $sign = STORAGE_SIGN;
161 0 0         return -1 if $self->storage_status($sign) <= 0; # SKIP
162              
163 0           my @test = ();
164 0           foreach my $storage ($self->http_storages) {
165 0           my $url = $storage->{url};
166 0           my $url_wop = $storage->{url_wop};
167 0           my $attr = $storage->{attr};
168              
169             # Create object
170             my $client = new App::MBUtiny::Storage::HTTP::Client(
171             url => $url, # Base URL
172             timeout => $storage->{timeout}, # default: 180
173 0 0 0       ($attr && isnt_void($attr)) ? (headers => $attr) : (),
174             );
175 0 0         unless ($client->status) {
176 0           $self->storage_status($sign, 0);
177 0           push @test, [0, $url_wop, sprintf("Can't connect to %s: %s", $url_wop, $client->error)];
178 0           next;
179             }
180              
181             # Check server
182 0 0         unless ($client->check) {
183 0           $self->storage_status($sign, 0);
184 0           push @test, [0, $url_wop, sprintf("Server not running or not configured (%s): %s", $url_wop, $client->error)];
185 0           next;
186             }
187              
188 0           push @test, [1, $url_wop];
189             }
190              
191 0           $self->{test}->{$sign} = [@test];
192 0           return 1;
193             }
194             sub put {
195 0     0 1   my $self = shift;
196 0           my %params = @_; $self->maybe::next::method(%params);
  0            
197 0 0         return $self->storage_status(STORAGE_SIGN, -1) if $self->storage_status(STORAGE_SIGN) <= 0; # SKIP and set SKIP
198 0           my $status = 1;
199 0           my $name = $params{name}; # File name only
200 0           my $file = $params{file}; # Path to local file
201 0   0       my $src_size = $params{size} || 0;
202              
203 0           foreach my $storage ($self->http_storages) {
204 0           my $url = $storage->{url};
205 0           my $url_wop = $storage->{url_wop};
206 0   0       my $comment = $storage->{comment} || "";
207 0           my $attr = $storage->{attr};
208 0           my $ostat = 1;
209              
210             # Create object
211             my $client = new App::MBUtiny::Storage::HTTP::Client(
212             url => $url, # Base URL
213             timeout => $storage->{timeout}, # default: 180
214 0 0 0       ($attr && isnt_void($attr)) ? (headers => $attr) : (),
215             no_check_redirect => 0,
216             );
217 0 0         unless ($client->status) {
218 0           $self->error(sprintf("Can't connect to %s: %s", $url_wop, $client->error));
219 0           $ostat = 0;
220             }
221              
222             # Upload file
223 0 0         if ($ostat) {
224 0 0         $client->upload(file => $file, name => $name) or do {
225 0           $self->error(join("\n", $client->transaction, $client->error));
226 0           $ostat = 0;
227             };
228             }
229              
230             # Get file size
231 0 0         if ($ostat) {
232 0           my %info = $client->fileinfo(name => $name);
233 0 0         unless ($client->status) {
234 0           $self->error(join("\n", $client->transaction, $client->error));
235 0           $ostat = 0;
236             }
237 0   0       my $dst_size = $info{size} || 0;
238 0 0         unless ($src_size == $dst_size) {
239 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));
240 0           $ostat = 0;
241             }
242             }
243              
244             # Fixup!
245 0 0         $self->fixup("put", $ostat, $comment) if $storage->{fixup};
246 0 0         $status = 0 unless $ostat;
247             }
248              
249 0 0         $self->storage_status(STORAGE_SIGN, 0) unless $status;
250             }
251             sub get {
252 0     0 1   my $self = shift;
253 0           my %params = @_;
254 0 0         if ($self->storage_status(STORAGE_SIGN) <= 0) { # SKIP and set SKIP
255 0           $self->maybe::next::method(%params);
256 0           return $self->storage_status(STORAGE_SIGN, -1);
257             }
258 0           my $name = $params{name}; # archive name
259 0           my $file = $params{file}; # destination archive file path
260              
261 0           foreach my $storage ($self->http_storages) {
262 0           my $url = $storage->{url};
263 0           my $url_wop = $storage->{url_wop};
264 0           my $attr = $storage->{attr};
265              
266             # Create object
267             my $client = new App::MBUtiny::Storage::HTTP::Client(
268             url => $url, # Base URL
269             timeout => $storage->{timeout}, # default: 180
270 0 0 0       ($attr && isnt_void($attr)) ? (headers => $attr) : (),
271             no_check_redirect => 0,
272             );
273 0 0         unless ($client->status) {
274 0           $self->error(sprintf("Can't connect to %s: %s", $url_wop, $client->error));
275 0           next;
276             }
277              
278             # Download file
279 0 0         $client->download(file => $file, name => $name) or do {
280 0           $self->error(join("\n", $client->transaction, $client->error));
281 0           next;
282             };
283 0           my $src_size = 0;
284 0 0         if (my $res = $client->res) {
285 0   0       $src_size = $res->content_length || 0
286             }
287 0   0       my $dst_size = filesize($file) // 0;
288 0 0         unless ($src_size == $dst_size) {
289 0           $self->error(sprintf("An error occurred while fetching data from %s. Sizes are different: SRC=%d; DST=%d", $url_wop, $src_size, $dst_size));
290 0           next;
291             }
292              
293             # Validate
294 0 0         unless ($self->validate($file)) { # FAIL validation!
295 0           $self->error(sprintf("HTTP storage %s failed: file %s is not valid!", $url_wop, $file));
296             next
297 0           }
298              
299             # Done!
300 0           return $self->storage_status(STORAGE_SIGN, 1);
301             }
302              
303 0           $self->storage_status(STORAGE_SIGN, 0);
304 0           $self->maybe::next::method(%params);
305             }
306             sub del {
307 0     0 1   my $self = shift;
308 0           my $name = shift;
309 0           $self->maybe::next::method($name);
310 0 0         return $self->storage_status(STORAGE_SIGN, -1) if $self->storage_status(STORAGE_SIGN) <= 0; # SKIP and set SKIP
311 0           my $status = 1;
312              
313 0           foreach my $storage ($self->http_storages) {
314 0           my $url = $storage->{url};
315 0           my $url_wop = $storage->{url_wop};
316 0           my $attr = $storage->{attr};
317 0           my $ostat = 1;
318              
319             # Create object
320             my $client = new App::MBUtiny::Storage::HTTP::Client(
321             url => $url, # Base URL
322             timeout => $storage->{timeout}, # default: 180
323 0 0 0       ($attr && isnt_void($attr)) ? (headers => $attr) : (),
324             );
325 0 0         unless ($client->status) {
326 0           $self->error(sprintf("Can't connect to %s: %s", $url_wop, $client->error));
327 0           $ostat = 0;
328             }
329              
330             # Get list
331 0           my @ls = ();
332 0 0         if ($ostat) {
333 0           @ls = $client->filelist(host => $self->{name});
334 0 0         unless ($client->status) {
335 0           $self->error(join("\n", $client->transaction, $client->error));
336 0           $ostat = 0;
337             }
338             }
339              
340             # Delete file
341 0 0 0       if ($ostat && grep { $_ eq $name } @ls ) {
  0            
342 0 0         $client->remove(name => $name) or do {
343 0           $self->error(join("\n", $client->transaction, $client->error));
344 0           $ostat = 0;
345             };
346             }
347              
348             # Fixup!
349 0 0         $self->fixup("del", $name) if $storage->{fixup};
350 0 0         $status = 0 unless $ostat;
351             }
352 0 0         $self->storage_status(STORAGE_SIGN, 0) unless $status;
353             }
354             sub list {
355 0     0 1   my $self = shift;
356 0           my %params = @_; $self->maybe::next::method(%params);
  0            
357 0 0         return $self->storage_status(STORAGE_SIGN, -1) if $self->storage_status(STORAGE_SIGN) <= 0; # SKIP and set SKIP
358 0           my $sign = STORAGE_SIGN;
359              
360 0           my @list = ();
361 0           foreach my $storage ($self->http_storages) {
362 0           my $url = $storage->{url};
363 0           my $url_wop = $storage->{url_wop};
364 0           my $attr = $storage->{attr};
365 0           my $ostat = 1;
366              
367             # Create object
368             my $client = new App::MBUtiny::Storage::HTTP::Client(
369             url => $url, # Base URL
370             timeout => $storage->{timeout}, # default: 180
371 0 0 0       ($attr && isnt_void($attr)) ? (headers => $attr) : (),
372             );
373 0 0         unless ($client->status) {
374 0           $self->error(sprintf("Can't connect to %s: %s", $url_wop, $client->error));
375 0           $ostat = 0;
376             }
377              
378             # Get list
379 0 0         if ($ostat) {
380 0           my @ls = $client->filelist(host => $self->{name});
381 0 0         if ($client->status) {
382 0 0         push @list, grep { defined($_) && length($_) } @ls;
  0            
383             } else {
384 0           $self->error(join("\n", $client->transaction, $client->error));
385 0           $ostat = 0;
386             }
387             }
388             }
389              
390 0           $self->{list}->{$sign} = [uniq(@list)];
391 0           return 1;
392             }
393              
394             1;
395              
396             package App::MBUtiny::Storage::HTTP::Client;
397              
398 3     3   32 use vars qw/ $VERSION /;
  3         5  
  3         202  
399             $VERSION = '1.00';
400              
401 3     3   21 use Fcntl qw/ :flock /;
  3         14  
  3         364  
402 3     3   24 use File::Basename;
  3         6  
  3         285  
403 3     3   23 use CTK::ConfGenUtil;
  3         5  
  3         231  
404 3     3   23 use CTK::Util qw/ trim /;
  3         6  
  3         153  
405              
406 3     3   20 use base qw/ WWW::MLite::Client /;
  3         7  
  3         1593  
407              
408             use constant {
409 3         3547 CONTENT_TYPE => "application/octet-stream",
410 3     3   223096 };
  3         9  
411              
412             sub new {
413 0     0     my $class = shift;
414 0           my %params = @_;
415 0   0       $params{ua_opts} ||= { agent => "MBUtiny/$VERSION" };
416 0   0       $params{content_type} ||= CONTENT_TYPE;
417 0   0       $params{no_check_redirect} //= 1;
418 0           return $class->SUPER::new(%params);
419             }
420             sub check {
421 0     0     my $self = shift;
422 0           $self->request("HEAD");
423 0           return $self->status;
424             }
425             sub filelist {
426 0     0     my $self = shift;
427 0           my %args = @_;
428 0   0       my $string_ret = $self->request(GET => $self->_merge_path_query($args{path}, $args{host})) || "";
429 0           my @array_ret = map {$_ = trim($_)} split /\s*\n+\s*/, $string_ret;
  0            
430 0 0         return wantarray ? @array_ret : $string_ret;
431             }
432             sub upload {
433 0     0     my $self = shift;
434 0           my %args = @_;
435 0   0       my $file = $args{file} || ''; # File for uploading! /path/to/file.tar.gz
436 0   0       my $name = $args{name} || basename($file); # File name! file.tar.gz
437 0 0         my $path = $args{path} ? sprintf("%s/%s", $args{path}, $name) : $name; # Path for request: /foo/bar
438             $self->request(PUT => $self->_merge_path_query($path), sub {
439 0     0     my $req = shift; # HTTP::Request object
440 0           $req->header('Content-Type', CONTENT_TYPE);
441 0 0 0       if (-e $file and -f $file) {
442 0   0       my $size = (-s $file) || 0;
443 0 0         return 0 unless $size;
444             #my $sizef = $size;
445 0           my $fh;
446             $req->content(sub {
447 0 0         unless ($fh) {
448 0 0         open($fh, "<", $file) or do {
449 0           $self->error(sprintf("Can't open file %s to read: %s", $file, $!));
450 0           return "";
451             };
452 0           binmode($fh);
453             }
454 0           my $buf = "";
455 0 0         if (my $n = read($fh, $buf, 1024)) {
456             #$sizef -= $n;
457             #printf STDERR ">>> sizef=%d; n=%d\n", $sizef, $n;
458 0           return $buf;
459             }
460 0           close($fh);
461 0           return "";
462 0           });
463 0           return $size;
464             }
465 0           return 0;
466 0           });
467 0           return $self->status;
468             }
469             sub fileinfo {
470 0     0     my $self = shift;
471 0           my %args = @_;
472 0           my $name = $args{name}; # File name! file.tar.gz
473 0 0         unless ($name) {
474 0           $self->error("The file name (name attribute) not specified!");
475 0           return ();
476             }
477 0 0         my $path = $args{path} ? sprintf("%s/%s", $args{path}, $name) : $name; # Path for request: /foo/bar
478 0           $self->request(HEAD => $self->_merge_path_query($path));
479 0 0         return () unless $self->status;
480 0           my %ret = ();
481 0           my $res = $self->res;
482 0 0         if ($res) {
483 0   0       $ret{code} = $res->code || 0;
484 0   0       $ret{message} = $res->message || '';
485 0   0       $ret{size} = $res->content_length || 0;
486 0   0       $ret{content_type} = $res->content_type || '';
487             }
488 0           return %ret;
489             }
490             sub download {
491 0     0     my $self = shift;
492 0           my %args = @_;
493 0   0       my $file = $args{file} || ''; # File for downloading! /path/to/file.tar.gz
494 0   0       my $name = $args{name} || basename($file); # File name! file.tar.gz
495 0 0         my $path = $args{path} ? sprintf("%s/%s", $args{path}, $name) : $name; # Path for request: /foo/bar
496              
497 0           my $fh;
498             my $expected_length;
499 0           my $bytes_received = 0;
500             $self->request(GET => $self->_merge_path_query($path), undef, sub {
501 0     0     my($chunk, $res) = @_;
502             #$bytes_received += length($chunk);
503 0 0         unless (defined $expected_length) {
504 0   0       $expected_length = $res->content_length || 0;
505 0 0         open($fh, ">", $file) or do {
506 0           $self->error(sprintf("Can't open file %s to write: %s", $file, $!));
507 0           return;
508             };
509 0 0         flock($fh, LOCK_EX) or do {
510 0           $self->error(stprintf("Can't lock file %s: %s", $file, $!));
511 0           return;
512             };
513 0           binmode($fh);
514             }
515 0 0 0       if ($expected_length && $fh) {
516             #printf STDERR "%d%% - ", 100 * $bytes_received / $expected_length;
517 0           print $fh $chunk;
518             }
519              
520             #print STDERR "$bytes_received bytes received\n";
521             # XXX Should really do something with the chunk itself
522             # print $chunk;
523 0           });
524 0 0         close($fh) if $fh;
525 0           return $self->status;
526             }
527             sub remove {
528 0     0     my $self = shift;
529 0           my %args = @_;
530 0           my $name = $args{name}; # File name! file.tar.gz
531 0 0         unless ($name) {
532 0           $self->error("The file name (name attribute) not specified!");
533 0           return $self->status(0);
534             }
535 0 0         my $path = $args{path} ? sprintf("%s/%s", $args{path}, $name) : $name; # Path for request: /foo/bar
536 0           $self->request(DELETE => $self->_merge_path_query($path));
537 0           return $self->status;
538             }
539              
540             sub _merge_path_query {
541 0     0     my $self = shift;
542 0           my $path = shift;
543 0           my $host = shift;
544 0           my $uri = $self->{uri}->clone;
545 0           my $path_orig = $uri->path;
546 0 0         $uri->path(sprintf("%s/%s", $path_orig, $path)) if $path;
547 0 0         $uri->query_form(host => $host) if $host;
548 0           return $uri->path_query;
549             }
550              
551             1;
552              
553             __END__