File Coverage

blib/lib/Yars/Client.pm
Criterion Covered Total %
statement 266 340 78.2
branch 78 148 52.7
condition 25 55 45.4
subroutine 38 44 86.3
pod 3 13 23.0
total 410 600 68.3


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