File Coverage

blib/lib/Yars/Client.pm
Criterion Covered Total %
statement 263 337 78.0
branch 77 146 52.7
condition 25 55 45.4
subroutine 37 43 86.0
pod 3 13 23.0
total 405 594 68.1


line stmt bran cond sub pod time code
1             package Yars::Client;
2              
3             # ABSTRACT: Yet Another RESTful-Archive Service Client
4             our $VERSION = '1.28'; # VERSION
5              
6 24     24   1067557 use strict;
  24         57  
  24         629  
7 24     24   115 use warnings;
  24         46  
  24         527  
8 24     24   378 use 5.010;
  24         73  
9 24     24   1040 use Clustericious::Client;
  24         10710116  
  24         168  
10 24     24   4755 use Clustericious::Client::Command;
  24         109324  
  24         726  
11 24     24   206 use Clustericious::Config;
  24         61  
  24         535  
12 24     24   132 use Mojo::Asset::File;
  24         59  
  24         402  
13 24     24   601 use Mojo::ByteStream 'b';
  24         57  
  24         1067  
14 24     24   142 use Mojo::URL;
  24         53  
  24         198  
15 24     24   544 use Mojo::Base '-base';
  24         54  
  24         90  
16 24     24   4014 use File::Basename;
  24         61  
  24         1357  
17 24     24   137 use File::Spec;
  24         55  
  24         530  
18 24     24   114 use Log::Log4perl qw(:easy);
  24         54  
  24         170  
19 24     24   18040 use Digest::file qw/digest_file_hex/;
  24         4500  
  24         1056  
20 24     24   140 use Data::Dumper;
  24         49  
  24         1057  
21 24     24   1267 use IO::Uncompress::Gunzip qw(gunzip $GunzipError);
  24         557227  
  24         2554  
22 24     24   722 use Number::Bytes::Human qw( format_bytes parse_bytes );
  24         8429  
  24         1226  
23 24     24   159 use File::Temp qw( tempdir );
  24         70  
  24         1464  
24 24     24   153 use File::HomeDir;
  24         59  
  24         927  
25 24     24   144 use YAML::XS;
  24         53  
  24         1120  
26 24     24   147 use Carp qw( carp );
  24         54  
  24         979  
27 24     24   149 use JSON::MaybeXS qw( encode_json );
  24         51  
  24         78913  
28              
29             route_doc upload => "<filename> [md5]";
30             route_doc download => "<filename> <md5> [dir]";
31             route_doc remove => "<filename> <md5>";
32              
33             route 'welcome' => "GET", '/';
34             route 'bucket_map' => "GET", '/bucket_map';
35             route 'disk_usage' => "GET", '/disk/usage';
36             route 'bucket_usage' => "GET", '/bucket/usage';
37             route 'servers_status' => "GET", '/servers/status';
38             route 'get' => "GET", '/file', \"<md5> <filename>";
39             route 'check' => "HEAD", '/file', \"<md5> <filename>";
40             route 'set_status' => "POST", '/disk/status';
41             route 'check_files' => "POST", '/check/manifest';
42              
43             route_meta 'welcome' => { auto_failover => 1, dont_read_files => 1 };
44             route_meta 'bucket_map' => { auto_failover => 1, dont_read_files => 1 };
45             route_meta 'disk_usage' => { auto_failover => 1, dont_read_files => 1 };
46             route_meta 'bucket_usage' => { auto_failover => 1, dont_read_files => 1 };
47             route_meta 'servers_status' => { auto_failover => 1, dont_read_files => 1 };
48             route_meta 'get' => { auto_failover => 1, dont_read_files => 1 };
49             route_meta 'check' => { auto_failover => 1, dont_read_files => 1 };
50             route_meta 'set_status' => { auto_failover => 1, dont_read_files => 1 };
51             route_meta 'check_files' => { auto_failover => 1, dont_read_files => 1 };
52              
53             route_meta 'upload' => { dont_read_files => 1 };
54             route_meta 'download' => { dont_read_files => 1 };
55             route_meta 'check_manifest' => { dont_read_files => 1 };
56             route_meta 'check' => { dont_read_files => 1 };
57              
58             route_args send => [
59             { name => 'content', type => '=s', required => 1 },
60             { name => 'name', type => '=s' },
61             ];
62              
63             route_args retrieve => [
64             { name => 'location', type => '=s' },
65             { name => 'name', type => '=s' },
66             { name => 'md5', type => '=s' },
67             ];
68              
69             sub new {
70 13     13 1 55384 my $self = shift->SUPER::new(@_);
71 13         7018 $self->ua->max_redirects(30);
72 13   100     155 $self->ua->connect_timeout($ENV{YARS_CONNECT_TIMEOUT} // 30);
73             $self->ua->on(start => sub {
74             # tx
75 98     98   173588 $_[1]->req->headers->header('X-Yars-Skip-Verify' => 'on');
76 98         3572 $_[1]->res->max_message_size(parse_bytes($self->config->max_message_size_client(default => 53687091200)));
77             $_[1]->on(finish => sub {
78 109         381967 my $tx = shift;
79 109 100       399 if(defined $tx->res->headers->header("X-Yars-Cache"))
80             {
81 3         95 $self->bucket_map_cached(0);
82             }
83 98         35744 });
84 13         217 });
85            
86 13         242 $self;
87             }
88              
89             sub client {
90 0     0 1 0 my($self, $new) = @_;
91              
92 0 0       0 $new ? do {
93 0         0 my $caller = caller;
94 0 0       0 carp "setting a new client is deprecated" if $caller ne 'Clustericious::Client';
95 0         0 $new->max_redirects(30);
96 0   0     0 $new->connect_timeout($ENV{YARS_CONNECT_TIMEOUT} // 30);
97 0         0 $self->SUPER::client($new);
98 0         0 $new;
99             } : $self->SUPER::client;
100             }
101              
102             sub bucket_map_cached {
103 72     72 0 24040 my($self, $new) = @_;
104              
105 72         198 state $fn = File::Spec->catfile(
106             File::HomeDir->my_dist_data('Yars', { create => 1 }),
107             'bucket_map_cache.yml',
108             );
109              
110 72 100       1553 if(defined $new) {
    100          
111 19         54 $self->{bucket_map_cached} = $new;
112              
113 19 100       90 if(ref $new) {
114 16         105 YAML::XS::DumpFile($fn, $new);
115             } else {
116 3         362 unlink $fn;
117             }
118             }
119              
120             elsif(! defined $self->{bucket_map_cached})
121             {
122 12 100       219 if(-r $fn)
123             {
124 5         28 my $cache = YAML::XS::LoadFile($fn);
125              
126             # make sure the cache and config are in sync:
127             # ie, all of the urls in the config refer to
128             # hosts in the cache.
129             # NOTE: Considered doing this based on the
130             # md5 on the config, but:
131             # - doing it on the Yars.conf file requires
132             # changes to Clustericious::Config
133             # - doing it on the $self->config means that
134             # you can't randomly choose a different
135             # server in the mojo template of the config.
136             # (see https://github.com/plicease/Yars#randomizing-the-server-choices)
137 5         690 my %r = map { $_ => 1 } values %$cache;
  80         140  
138 5 100       28 if(grep { ! $r{$_} } ($self->config->url, $self->config->failover_urls( default => [] )))
  10         295  
139             {
140 1         4 $self->{bucket_map_cached} = 0;
141 1         81 unlink $fn;
142             }
143             else
144             {
145 4         16 $self->{bucket_map_cached} = $cache;
146             }
147             }
148             else
149             {
150 7         24 $self->{bucket_map_cached} = 0;
151             }
152             }
153              
154 72         4418 $self->{bucket_map_cached};
155             }
156              
157             sub _get_url {
158              
159             # Helper to create the Mojo URL objects
160 4     4   10 my ($self, $path) = @_;
161              
162 4         19 my $url = Mojo::URL->new( $self->server_url );
163 4 50       1008 $url->path($path) if $path;
164              
165 4         92 return $url;
166             }
167              
168             sub _hex2b64 {
169 25 50   25   5262 my $hex = shift or return;
170 25         254 my $b64 = b(pack 'H*', $hex)->b64_encode;
171 25         847 local $/="\n";
172 25         101 chomp $b64;
173 25         420 return $b64;
174             }
175              
176             sub _b642hex {
177 0 0   0   0 my $b64 = shift or return;
178             # Mojo::Headers apparently become array refs sometimes
179 0 0       0 $b64 = $b64->[0] if ref($b64) eq 'ARRAY';
180 0         0 return unpack 'H*', b($b64)->b64_decode;
181             }
182              
183             sub location {
184 0     0 0 0 my ($self, $filename, $md5) = @_;
185              
186 0 0       0 ( $filename, $md5 ) = ( $md5, $filename ) if $filename =~ /^[0-9a-f]{32}$/i;
187 0 0       0 LOGDIE "Can't compute location without filename" unless defined($filename);
188 0 0       0 LOGDIE "Can't compute location without md5" unless $md5;
189 0         0 $self->server_url($self->_server_for($md5));
190 0         0 return $self->_get_url("/file/$md5/$filename")->to_abs->to_string;
191             }
192              
193             sub _sleep {
194             sleep @_
195 0     0   0 }
196              
197             sub download {
198             # Downloads a file and saves it to disk.
199 12     12 0 34808 my $self = shift;
200 12         42 my ( $filename, $md5, $dest ) = @_;
201 12         28 my $abs_url;
202 12 50       53 if (@_ == 1) {
203 0         0 $abs_url = shift;
204 0         0 ($filename) = $abs_url =~ m|/([^/]+)$|;
205             }
206 12 100       63 ( $filename, $md5 ) = ( $md5, $filename ) if $filename =~ /^[0-9a-f]{32}$/i;
207              
208 12 0 33     37 if (!$md5 && !$abs_url) {
209 0         0 LOGDIE "Need either an md5 or a url: download(url) or download(filename, md5, [dir] )";
210             }
211              
212 12         29 my @hosts;
213 12 50       67 @hosts = $self->_all_hosts($self->_server_for($md5)) unless $abs_url;
214 12         34 my $tries = 0;
215 12         22 my $success = 0;
216 12         21 my $host = 0;
217 12         47 while ($tries++ < 10) {
218              
219 21 100       80 if ($tries > @hosts + 1) {
220 8         36 TRACE "Attempt $tries";
221 8         9447 WARN "Waiting $tries seconds before retrying...";
222 8         9101 _sleep $tries;
223             }
224 21         9214 my $url;
225 21 50       53 if ($abs_url) {
226 0         0 $url = $abs_url;
227             } else {
228 21 100       68 $host = 0 if $host > $#hosts;
229 21         134 $url = Mojo::URL->new($hosts[$host++]);
230 21         4777 $url->path("/file/$filename/$md5");
231             }
232 21         653 TRACE "GET $url";
233 21         23910 my $tx = $self->ua->get($url => { "Connection" => "Close", "Accept-Encoding" => "gzip" });
234             # TODO: set timeout for mojo 4.0
235 21         9399 $self->res($tx->res);
236 21         245 $self->tx($tx);
237 21 100       761 my $res = $tx->success or do {
238 11         217 my $error = $tx->error;
239 11         156 $tx->closed;
240 11 100       219 if ($error->{code}) {
241 1         8 ERROR "Yars download : $error->{code} $error->{message}";
242 1         1141 last;
243             }
244 10         110 WARN "Error (may retry) : " . encode_json($error);
245 10         11889 next;
246             };
247 10         255 DEBUG "Received asset with size ".$res->content->asset->size;
248 10         12101 TRACE "Received headers : ".$res->headers->to_string;
249              
250             # host == 0 means we tried the assigned host
251 10 50       7369 if($host == 0) {
252 0         0 my $prev = $res->previous;
253             # previous message in the chain was a 301
254             # Moved Permanently means our bucket map is
255             # wrong our out of date
256 0 0 0     0 if($prev && $prev->code == 301) {
257 0         0 $self->bucket_map_cached(0);
258             }
259             }
260              
261 10 100       120 my $out_file = $dest
    100          
262             ? File::Spec->catfile((ref $dest eq 'SCALAR' ? tempdir(CLEANUP => 1) : $dest), $filename)
263             : $filename;
264              
265 10         2379 DEBUG "Writing to $out_file";
266 10 50       11157 if (my $e = $res->headers->header("Content-Encoding")) {
267 0 0       0 LOGDIE "unsupported encoding" unless $e eq 'gzip';
268             # This violate the spec (MD5s depend on transfer-encoding
269             # not content-encoding, per
270             # http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html
271             # but we must support it.
272 0         0 TRACE "unzipping $out_file";
273 0         0 my $asset = $res->content->asset;
274             gunzip($asset->is_file ? $asset->path : \( $asset->slurp )
275 0 0       0 => $out_file) or do {
    0          
276 0         0 unlink $out_file;
277 0         0 LOGDIE "Gunzip failed : $GunzipError";
278             };
279             } else {
280 10         276 $res->content->asset->move_to($out_file);
281             }
282 10         1822 my $verify = digest_file_hex($out_file,'MD5');
283 10   33     894 $md5 ||= _b642hex($res->headers->header("Content-MD5"));
284              
285 10 50       46 unless ($md5) {
286 0         0 WARN "No md5 in response header";
287 0         0 next;
288             }
289 10 50       40 if ($verify eq $md5)
290             {
291 10 100       48 if(ref $dest eq 'SCALAR')
292             {
293 5         124 open my $fh, '<', $out_file;
294 5         23 binmode $fh;
295 5         15 $$dest = do { local $/; <$fh> };
  5         21  
  5         60  
296 5         36 close $fh;
297             }
298             }
299             else
300             {
301 0         0 WARN "Bad md5 for file (got $verify instead of $md5)";
302 0         0 WARN "Response headers : ".$res->headers->to_string;
303 0 0       0 unlink $out_file or WARN "couldn't remove $out_file : $!";
304 0         0 WARN "Removed $out_file. This is attempt $tries.";
305 0         0 next;
306             }
307              
308 10         29 $success = 1;
309 10         34 last;
310             }
311 12 100       50 ERROR "Download failed." unless $success;
312 12 100       2290 return '' unless $success;
313 10         92 return 'ok'; # return TRUE
314             }
315              
316             sub remove {
317             # Removes a file
318 4     4 0 12397 my ( $self, $filename, $md5 ) = @_;
319              
320 4 50 33     29 LOGDIE "file and md5 needed for remove"
321             unless $filename && $md5;
322              
323 4         26 my $url = $self->_get_url("/file/$md5/$filename");
324 4         23 TRACE("removing $filename $md5 from ", $url->to_string);
325              
326             # Delete the file
327 4         5794 $self->_doit(DELETE => $url);
328             }
329              
330             # Given an md5, determine the correct server
331             # using a cached list of bucket->server assignments.
332             sub _server_for {
333 36     36   96 my $self = shift;
334 36 50       141 my $md5 = shift or LOGDIE "Missing argument md5";
335 36         145 my $bucket_map = $self->bucket_map_cached;
336 36 50 66     327 unless ($bucket_map && ref($bucket_map) eq 'HASH' && keys %$bucket_map > 0) {
      66        
337 9 50       84 $bucket_map = $self->bucket_map or WARN $self->errorstring;
338 9 50 33     17910 $self->bucket_map_cached({ %$bucket_map }) if $bucket_map && ref $bucket_map && (keys %$bucket_map > 0);
      33        
339             }
340 36 50 33     396 unless ($bucket_map && ref $bucket_map && (keys %$bucket_map > 0)) {
      33        
341 0         0 WARN "Failed to retrieve bucket map, using ".$self->server_url;
342 0         0 return $self->server_url;
343             }
344 36         137 for (0..length($md5)) {
345 72         238 my $prefix = substr($md5,0,$_);
346 72 100       386 return $bucket_map->{ lc $prefix } if exists($bucket_map->{lc $prefix});
347 40 100       165 return $bucket_map->{ uc $prefix } if exists($bucket_map->{uc $prefix});
348             }
349 0         0 LOGDIE "Can't find url for $md5 in bucket map : ".Dumper($bucket_map);
350             }
351              
352             sub put {
353 2     2 0 4 my $self = shift;
354 2         5 my $remote_filename = shift;
355 2   33     7 my $content = shift || join '', <STDIN>;
356             # NB: slow for large content.
357 2         11 my $md5 = b($content)->md5_sum;
358 2         54 my $url = Mojo::URL->new($self->_server_for($md5));
359 2         469 $url->path("/file/$remote_filename");
360 2         49 TRACE "PUT $url";
361 2         2723 my $tx = $self->ua->put("$url" => { "Content-MD5" => _hex2b64($md5), "Connection" => "Close" } => $content);
362 2         1037 $self->res($tx->res);
363 2         25 $self->tx($tx);
364 2 50       75 return $tx->success ? 'ok' : '';
365             }
366              
367             sub _all_hosts {
368 32     32   71 my $self = shift;
369 32         71 my $assigned = shift;
370             # Return all the hosts, any parameter will be put first in
371             # the list.
372 32         91 my @servers = ($assigned);
373 32         163 push @servers, $self->server_url;
374 32         304 push @servers, $self->config->url;
375 32         1200 push @servers, @{ $self->config->failover_urls(default => []) };
  32         151  
376 32         1731 my %seen;
377 32         90 return grep { !$seen{$_}++ } @servers;
  116         394  
378             }
379              
380             sub upload {
381 20     20 0 94482 my $self = shift;
382 20 100       102 my $content = ref($_[-1]) eq 'SCALAR' ? pop : undef;
383              
384 20         49 my $nostash;
385             # OLD COMMENT (still valid):
386             # To avoid failover:
387             # yarsclient upload --nostash 1 foo
388             # Yars::Client->new->upload("--nostash" => 1, 'foo');
389             # This is undocumented since it is only intended to be
390             # used on a server when blanacing, not as a public interface.
391             # UPDATE (Graham Ollis 2015-11-10)
392             # This feature is only used by yars_fast_balance
393             # (see Yars::Command::yars_fast_balance for implementation).
394             # This used to match on /nostash$/ but the only place this
395             # is used it specifies it as a command line option like thing
396             # ("--nostash"), but that means that you can't upload files
397             # that end in *nostash. In truth the interface here should
398             # be a little better thought out and a little less batshitcrazy.
399 20 50 33     175 if(defined $_[0] && $_[0] eq '--nostash')
400             {
401 0         0 shift;
402 0         0 $nostash = shift;
403             }
404              
405 20         50 my $filename = shift;
406 20 100 66     116 my $md5 = defined $_[0] && $_[0] =~ /^[0-9a-f]+$/i ? lc shift : undef;
407              
408 20 50       78 if (@_) {
409 0         0 LOGDIE "unknown options to upload : @_";
410             }
411              
412 20 50       74 LOGDIE "file needed for upload" unless $filename;
413 20 100       67 if(defined $content) {
414             # intended mainly for testing only,
415             # may be ulsewise later
416 16         106 $filename = File::Spec->catfile( tempdir( CLEANUP => 1 ), $filename );
417 16         8350 open my $fh, '>', $filename;
418 16         85 binmode $fh;
419 16         237 print $fh $$content;
420 16         616 close $fh;
421             } else {
422 4         82 $filename = File::Spec->rel2abs($filename);
423             }
424 20 50       313 -r $filename or LOGDIE "Could not read " . $filename;
425              
426             # Don't read the file.
427 20         993 my $basename = basename($filename);
428 20         247 my $asset = Mojo::Asset::File->new( path => $filename );
429 20   66     313 $md5 ||= digest_file_hex($filename, 'MD5');
430              
431 20         1973 my @servers = $self->_all_hosts( $self->_server_for($md5) );
432              
433 20         79 my $tx;
434             my $code;
435 20         0 my $host;
436              
437 20   66     157 while (!$code && ($host = shift @servers)) {
438 23         4038 my $url = Mojo::URL->new($host);
439 23         5942 $url->path("/file/$basename/$md5");
440 23         629 DEBUG "Sending $md5 to $url";
441              
442 23 50       33747 my @nostash = ($nostash ? ("X-Yars-NoStash" => 1) : ());
443 23         126 $tx = $self->ua->build_tx(
444             PUT => "$url" => {
445             @nostash,
446             "Content-MD5" => _hex2b64($md5),
447             "Connection" => "Close"
448             }
449             );
450 23         6649 $tx->req->content->asset($asset);
451             # TODO: set timeout for mojo 4.0
452 23         319 $tx = $self->ua->start($tx);
453 23         12943 $code = $tx->res->code;
454 23         231 $self->res($tx->res);
455 23         339 $self->tx($tx);
456              
457 23 100       1496 if (!$tx->success) {
458 4         97 my ($error) = $tx->error;
459 4 50       135 $error = encode_json $error if ref $error;
460 4         33 INFO "PUT to $host failed : $error";
461             }
462             }
463 20         1758 $self->res($tx->res);
464 20 100 66     261 return '' if !$code || !$tx->res->is_success;
465              
466 19         488 DEBUG "Response : ".$tx->res->code." ".$tx->res->message;
467 19         25190 return 'ok';
468             }
469              
470             sub _rand_filename {
471 1     1   3 my $a = '';
472 1         27 $a .= ('a'..'z','A'..'Z',0..9)[rand 62] for 1..43;
473 1         4 return $a;
474             }
475              
476             sub send {
477 2     2 0 8157 my $self = shift;
478 2         9 my $meta = $self->meta_for;
479 2         83 my %args = $meta->process_args(@_);
480 2         1138 my $content = $args{content};
481 2   66     11 my $filename = $args{name} || $self->_rand_filename;
482 2         8 my $status = $self->put($filename, $content);
483 2 50       60 return unless $status eq 'ok';
484 2         8 return $self->res->headers->location;
485             }
486              
487             sub retrieve {
488 2     2 0 1959 my $self = shift;
489 2         11 my %args = $self->meta_for->process_args(@_);
490 2 100       1343 if (my $location = $args{location}) {
491 1         5 my $tx = $self->ua->get($location);
492 1 50       399 my $res = $tx->success or do {
493 0         0 $self->tx($tx);
494 0         0 $self->res($tx->res);
495 0         0 return;
496             };
497 1         24 return $res->body;
498             }
499 1 50       5 my $md5 = $args{md5} or LOGDIE "need md5 or location to retrieve";
500 1 50       4 my $name = $args{name} or LOGDIE "need name or location to retrieve";
501 1         5 return $self->get($md5,$name);
502             }
503              
504             sub res_md5 {
505 1     1 0 27 my $self = shift;
506 1 50       4 my $res = $self->res or return;
507 1 50       9 if (my $b64 = $res->headers->header("Content-MD5")) {
508 0         0 return _b642hex($b64);
509             }
510 1 50       17 if (my $location = $res->headers->location) {
511 1         21 my ($md5) = $location =~ m[/file/([0-9a-f]{32})/];
512 1         4 return $md5;
513             }
514 0           return;
515             }
516              
517             sub check_manifest {
518 0     0 0   my $self = shift;
519 0           my @args = @_;
520 0           my $check = 0;
521 0           my $params = "";
522 0           my $manifest;
523 0           while ($_ = shift @_) {
524 0 0         /^-c$/ and do { $check = 1; next; };
  0            
  0            
525 0 0         /^--show_corrupt$/ and do { $params = "?show_corrupt=" . shift; next; };
  0            
  0            
526 0           $manifest = $_;
527             }
528 0 0         LOGDIE "Missing manifest" unless $manifest;
529 0 0         LOGDIE "Cannot open manifest $manifest" unless -e $manifest;
530 0           my $contents = Mojo::Asset::File->new(path => $manifest)->slurp;
531 0           my $got = $self->_doit(POST => "/check/manifest$params", { manifest => $contents });
532 0 0         return $got unless $self->tx->success;
533 0 0         $got->{$manifest} = (@{$got->{missing}}==0 ? 'ok' : 'not ok');
  0            
534 0 0         return { $manifest => $got->{$manifest} } if $check;
535 0           return $got;
536             }
537              
538             sub remote {
539 0     0 1   my $self = shift;
540 0           $self->bucket_map_cached(0);
541 0           $self->SUPER::remote(@_);
542             }
543              
544             1;
545              
546             __END__
547              
548             =pod
549              
550             =encoding UTF-8
551              
552             =head1 NAME
553              
554             Yars::Client - Yet Another RESTful-Archive Service Client
555              
556             =head1 VERSION
557              
558             version 1.28
559              
560             =head1 SYNOPSIS
561              
562             my $r = Yars::Client->new;
563              
564             # Send and retrieve content.
565             my $location = $y->send(content => 'hello, world') or die $y->errorstring;
566             say $y->retrieve(location => $location);
567              
568             # Alternatively, use names and md5s explicitly.
569             my $location = $y->send(content => 'hello there', name => "greeting");
570             my $md5 = $y->res_md5;
571             say $y->retrieve(filename => 'greeting', md5 => $md5);
572              
573             # Upload a file.
574             $r->upload($filename) or die $r->errorstring;
575             print $r->res->headers->location;
576              
577             # Download a file.
578             $r->download($filename, $md5) or die $r->errorstring;
579             $r->download($filename, $md5, '/tmp'); # download it to the /tmp directory
580             $r->download("http://yars/0123456890abc/filename.txt"); # Write filename.txt to current directory.
581              
582             # More concise version of retrieve.
583             my $content = $r->get($filename,$md5);
584              
585             # Delete a file.
586             $r->remove($filename, $md5) or die $r->errorstring;
587              
588             # Compute the URL of a file based on the md5 and the buckets.
589             print $r->location($filename, $md5);
590              
591             print "Server version is ".$r->status->{server_version};
592             my $usage = $r->disk_usage(); # Returns usage for a single server.
593             my $nother_usage = Yars::Client->new(url => "http://host1:9999")->disk_usage();
594             my $status = $r->servers_status(); # return a hash of servers, disks, and their statuses
595              
596             # Mark a disk down.
597             my $ok = $r->set_status({ root => "/acps/disk/one", state => "down" });
598             my $ok = $r->set_status({ root => "/acps/disk/one", state => "down", host => "http://host2" });
599              
600             # Mark a disk up.
601             my $ok = $r->set_status({ root => "/acps/disk/one", state => "up" });
602              
603             # Check a manifest file or list of files.
604             my $details = $r->check_manifest( $filename );
605             my $check = $r->check_manifest( "-c", $filename );
606             my $check = $r->check_manifest( "--show_corrupt" => 1, $filename );
607             my $ck = $r->check_files({ files => [
608             { filename => $f1, md5 => $m1 },
609             { filename => $f2, md5 => $m2 } ] });
610              
611             =head1 DESCRIPTION
612              
613             Client for L<Yars>.
614              
615             =head1 SEE ALSO
616              
617             L<yarsclient>, L<Clustericious::Client>
618              
619             =head1 AUTHOR
620              
621             Original author: Marty Brandon
622              
623             Current maintainer: Graham Ollis E<lt>plicease@cpan.orgE<gt>
624              
625             Contributors:
626              
627             Brian Duggan
628              
629             Curt Tilmes
630              
631             =head1 COPYRIGHT AND LICENSE
632              
633             This software is copyright (c) 2013 by NASA GSFC.
634              
635             This is free software; you can redistribute it and/or modify it under
636             the same terms as the Perl 5 programming language system itself.
637              
638             =cut