1#!/usr/bin/perl
2package MogileFS::Client;
3
4=head1 NAME
5
6MogileFS::Client - Client library for the MogileFS distributed file system.
7
8=head1 SYNOPSIS
9
10 use MogileFS::Client;
11
12 # create client object w/ server-configured namespace
13 # and IPs of trackers
14 $mogc = MogileFS::Client->new(domain => "foo.com::my_namespace",
15                               hosts  => ['10.0.0.2:7001', '10.0.0.3:7001']);
16
17 # create a file
18 # mogile is a flat namespace.  no paths.
19 $key   = "image_of_userid:$userid";
20 # must be configured on server
21 $class = "user_images";
22 $fh = $mogc->new_file($key, $class);
23
24 print $fh $data;
25
26 unless ($fh->close) {
27    die "Error writing file: " . $mogc->errcode . ": " . $mogc->errstr;
28 }
29
30 # Find the URLs that the file was replicated to.
31 # May change over time.
32 @urls = $mogc->get_paths($key);
33
34 # no longer want it?
35 $mogc->delete($key);
36
37=head1 DESCRIPTION
38
39This module is a client library for the MogileFS distributed file system. The class method 'new' creates a client object against a
40particular mogilefs tracker and domain. This object may then be used to store and retrieve content easily from MogileFS.
41
42=cut
43
44use strict;
45use Carp;
46use IO::WrapTie;
47use LWP::UserAgent;
48use fields (
49            'domain',    # scalar: the MogileFS domain (namespace).
50            'backend',   # MogileFS::Backend object
51            'readonly',  # bool: if set, client won't permit write actions/etc.  just reads.
52            'hooks',     # hash: hookname -> coderef
53            );
54use Time::HiRes ();
55use MogileFS::Backend;
56use MogileFS::NewHTTPFile;
57use MogileFS::ClientHTTPFile;
58
59our $VERSION = '1.17';
60
61our $AUTOLOAD;
62
63=head1 METHODS
64
65=head2 new
66
67  $client = MogileFS::Client->new( %OPTIONS );
68
69Creates a new MogileFS::Client object.
70
71Returns MogileFS::Client object on success, or dies on failure.
72
73OPTIONS:
74
75=over
76
77=item hosts
78
79Arrayref of 'host:port' strings to connect to as backend trackers in this client.
80
81=item domain
82
83String representing the mogile domain which this MogileFS client is associated with. (All create/delete/fetch operations
84will be performed against this mogile domain). See the mogadm shell command and its 'domain' category of operations for
85information on manipulating the list of possible domains on a MogileFS system.
86
87=back
88
89=cut
90
91sub new {
92    my MogileFS::Client $self = shift;
93    $self = fields::new($self) unless ref $self;
94
95    return $self->_init(@_);
96}
97
98=head2 reload
99
100  $mogc->reload( %OPTIONS )
101
102Re-init the object, like you'd just reconstructed it with 'new', but change it in-place instead.  Useful if you have a system which reloads a config file, and you want to update a singleton $mogc handle's config value.
103
104=cut
105
106sub reload {
107    my MogileFS::Client $self = shift;
108    return undef unless $self;
109
110    return $self->_init(@_);
111}
112
113sub _init {
114    my MogileFS::Client $self = shift;
115
116    my %args = @_;
117
118    # FIXME: add actual validation
119    {
120        # by default, set readonly off
121        $self->{readonly} = $args{readonly} ? 1 : 0;
122
123        # get domain (required)
124        $self->{domain} = $args{domain} or
125            _fail("constructor requires parameter 'domain'");
126
127        # create a new backend object if there's not one already,
128        # otherwise call a reload on the existing one
129        if ($self->{backend}) {
130            $self->{backend}->reload( hosts => $args{hosts} );
131        } else {
132            $self->{backend} = MogileFS::Backend->new( hosts => $args{hosts},
133                                                       timeout => $args{timeout},
134                                                       );
135        }
136        _fail("cannot instantiate MogileFS::Backend") unless $self->{backend};
137    }
138
139    _debug("MogileFS object: [$self]", $self);
140
141    return $self;
142}
143
144=head2 last_tracker
145
146Returns a scalar of form "ip:port", representing the last mogilefsd
147'tracker' server which was talked to.
148
149=cut
150
151sub last_tracker {
152    my MogileFS::Client $self = shift;
153    return $self->{backend}->last_tracker;
154}
155
156=head2 errstr
157
158Returns string representation of the last error that occurred.  It
159includes the error code (same as method 'errcode') and a space before
160the optional English error message.
161
162This isn't necessarily guaranteed to reset after a successful
163operation.  Only call it after another operation returns an error.
164
165
166=cut
167
168sub errstr {
169    my MogileFS::Client $self = shift;
170    return $self->{backend}->errstr;
171}
172
173=head2 errcode
174
175Returns an error code.  Not a number, but a string identifier
176(e.g. "no_domain") which is stable for use in error handling logic.
177
178This isn't necessarily guaranteed to reset after a successful
179operation.  Only call it after another operation returns an error.
180
181=cut
182
183sub errcode {
184    my MogileFS::Client $self = shift;
185    return $self->{backend}->errcode;
186}
187
188=head2 force_disconnect
189
190Forces the client to disconnect from the tracker, causing it to reconnect
191when the next request is made.  It will reconnect to a different tracker if
192possible.  A paranoid application may wish to do to this before retrying a
193failed command, on the off chance that another tracker may be working better.
194
195=cut
196
197sub force_disconnect {
198    my MogileFS::Client $self = shift;
199    return $self->{backend}->force_disconnect();
200}
201
202=head2 readonly
203
204  $is_readonly = $mogc->readonly
205  $mogc->readonly(1)
206
207Getter/setter to mark this client object as read-only.  Purely a local
208operation/restriction, doesn't do a network operation to the mogilefsd
209server.
210
211=cut
212
213sub readonly {
214    my MogileFS::Client $self = shift;
215    return $self->{readonly} = $_[0] ? 1 : 0 if @_;
216    return $self->{readonly};
217}
218
219=head2 new_file
220
221  $mogc->new_file($key)
222  $mogc->new_file($key, $class)
223  $mogc->new_file($key, $class, $content_length)
224  $mogc->new_file($key, $class, $content_length , $opts_hashref)
225
226Start creating a new filehandle with the given key, and option given
227class and options.
228
229Returns a filehandle you should then print to, and later close to
230complete the operation.  B<NOTE:> check the return value from close!
231If your close didn't succeed, the file didn't get saved!
232
233$opts_hashref can contain keys:
234
235=over
236
237=item fid
238
239Explicitly specify the fid number to use, rather than it being automatically allocated.
240
241=item create_open_args
242
243Hashref of extra key/value pairs to send to mogilefsd in create_open phase.
244
245=item create_close_args
246
247Hashref of extra key/value pairs to send to mogilefsd in create_close phase.
248
249=item largefile
250
251Use MogileFS::ClientHTTPFile which will not load the entire file into memory
252like the default MogileFS::NewHTTPFile but requires that the storage node
253HTTP servers support the Content-Range header in PUT requests and is a little
254slower.
255
256=back
257
258=cut
259
260# returns MogileFS::NewHTTPFile object, or undef if no device
261# available for writing
262# ARGS: ( key, class, bytes?, opts? )
263# where bytes is optional and the length of the file and opts is also optional
264# and is a hashref of options.  supported options: fid = unique file id to use
265# instead of just picking one in the database.
266sub new_file {
267    my MogileFS::Client $self = shift;
268    return undef if $self->{readonly};
269
270    my ($key, $class, $bytes, $opts) = @_;
271    $bytes += 0;
272    $opts ||= {};
273
274    # Extra args to be passed along with the create_open and create_close commands.
275    # Any internally generated args of the same name will overwrite supplied ones in
276    # these hashes.
277    my $create_open_args =  $opts->{create_open_args} || {};
278    my $create_close_args = $opts->{create_close_args} || {};
279
280    $self->run_hook('new_file_start', $self, $key, $class, $opts);
281
282    my $res = $self->{backend}->do_request
283        ("create_open", {
284            %$create_open_args,
285            domain => $self->{domain},
286            class  => $class,
287            key    => $key,
288            fid    => $opts->{fid} || 0, # fid should be specified, or pass 0 meaning to auto-generate one
289            multi_dest => 1,
290        }) or return undef;
291
292    my $dests = [];  # [ [devid,path], [devid,path], ... ]
293
294    # determine old vs. new format to populate destinations
295    unless (exists $res->{dev_count}) {
296        push @$dests, [ $res->{devid}, $res->{path} ];
297    } else {
298        for my $i (1..$res->{dev_count}) {
299            push @$dests, [ $res->{"devid_$i"}, $res->{"path_$i"} ];
300        }
301    }
302
303    my $main_dest = shift @$dests;
304    my ($main_devid, $main_path) = ($main_dest->[0], $main_dest->[1]);
305
306    # create a MogileFS::NewHTTPFile object, based off of IO::File
307    unless ($main_path =~ m!^http://!) {
308        Carp::croak("This version of MogileFS::Client no longer supports non-http storage URLs.\n");
309    }
310
311    $self->run_hook('new_file_end', $self, $key, $class, $opts);
312
313    return IO::WrapTie::wraptie( ( $opts->{largefile}
314            ? 'MogileFS::ClientHTTPFile'
315            : 'MogileFS::NewHTTPFile' ),
316                                mg    => $self,
317                                fid   => $res->{fid},
318                                path  => $main_path,
319                                devid => $main_devid,
320                                backup_dests => $dests,
321                                class => $class,
322                                key   => $key,
323                                content_length => $bytes+0,
324                                create_close_args => $create_close_args,
325                                overwrite => 1,
326                            );
327}
328
329=head2 edit_file
330
331  $mogc->edit_file($key, $opts_hashref)
332
333Edit the file with the the given key.
334
335
336B<NOTE:> edit_file is currently EXPERIMENTAL and not recommended for
337production use. MogileFS is primarily designed for storing files
338for later retrieval, rather than editing.  Use of this function may lead to
339poor performance and, until it has been proven mature, should be
340considered to also potentially cause data loss.
341
342B<NOTE:> use of this function requires support for the DAV 'MOVE'
343verb and partial PUT (i.e. Content-Range in PUT) on the back-end
344storage servers (e.g. apache with mod_dav).
345
346Returns a seekable filehandle you can read/write to. Calling this
347function may invalidate some or all URLs you currently have for this
348key, so you should call ->get_paths again afterwards if you need
349them.
350
351On close of the filehandle, the new file contents will replace the
352previous contents (and again invalidate any existing URLs).
353
354By default, the file contents are preserved on open, but you may
355specify the overwrite option to zero the file first. The seek position
356is at the beginning of the file, but you may seek to the end to append.
357
358$opts_hashref can contain keys:
359
360=over
361
362=item overwrite
363
364The edit will overwrite the file, equivalent to opening with '>'.
365Default: false.
366
367=back
368
369=cut
370
371sub edit_file {
372    my MogileFS::Client $self = shift;
373    return undef if $self->{readonly};
374
375    my($key, $opts) = @_;
376
377    my $res = $self->{backend}->do_request
378        ("edit_file", {
379            domain => $self->{domain},
380            key    => $key,
381        }) or return undef;
382
383    my $moveReq = HTTP::Request->new('MOVE', $res->{oldpath});
384    $moveReq->header(Destination => $res->{newpath});
385    my $ua = LWP::UserAgent->new;
386    my $resp = $ua->request($moveReq);
387    unless ($resp->is_success) {
388        warn "Failed to MOVE $res->{oldpath} to $res->{newpath}";
389        return undef;
390    }
391
392    return IO::WrapTie::wraptie('MogileFS::ClientHTTPFile',
393                                mg        => $self,
394                                fid       => $res->{fid},
395                                path      => $res->{newpath},
396                                devid     => $res->{devid},
397                                class     => $res->{class},
398                                key       => $key,
399                                overwrite => $opts->{overwrite},
400                            );
401}
402
403=head2 read_file
404
405  $mogc->read_file($key)
406
407Read the file with the the given key.
408
409Returns a seekable filehandle you can read() from. Note that you cannot
410read line by line using <$fh> notation.
411
412Takes the same options as get_paths (which is called internally to get
413the URIs to read from).
414
415=cut
416
417sub read_file {
418    my MogileFS::Client $self = shift;
419
420    my @paths = $self->get_paths(@_);
421
422    my $path = shift @paths;
423
424    return if !$path;
425
426    my @backup_dests = map { [ undef, $_ ] } @paths;
427
428    return IO::WrapTie::wraptie('MogileFS::ClientHTTPFile',
429                                path         => $path,
430                                backup_dests => \@backup_dests,
431                                readonly     => 1,
432                                );
433}
434
435=head2 store_file
436
437  $mogc->store_file($key, $class, $fh_or_filename[, $opts_hashref])
438
439Wrapper around new_file, print, and close.
440
441Given a key, class, and a filehandle or filename, stores the file
442contents in MogileFS.  Returns the number of bytes stored on success,
443undef on failure.
444
445$opts_hashref can contain keys for new_file, and also the following:
446
447=over
448
449=item chunk_size
450
451Number of bytes to read and write and write at once out of the larger file.
452Defaults to 8192 bytes. Increasing this can increase performance at the cost
453of more memory used while uploading the file.
454Note that this mostly helps when using largefile => 1
455
456=back
457
458=cut
459
460sub store_file {
461    my MogileFS::Client $self = shift;
462    return undef if $self->{readonly};
463
464    my($key, $class, $file, $opts) = @_;
465    $self->run_hook('store_file_start', $self, $key, $class, $opts);
466
467    my $chunk_size = $opts->{chunk_size} || 8192;
468    my $fh = $self->new_file($key, $class, undef, $opts) or return;
469    my $fh_from;
470    if (ref($file)) {
471        $fh_from = $file;
472    } else {
473        open $fh_from, $file or return;
474    }
475    my $bytes;
476    while (my $len = read $fh_from, my($chunk), $chunk_size) {
477        $fh->print($chunk);
478        $bytes += $len;
479    }
480
481    $self->run_hook('store_file_end', $self, $key, $class, $opts);
482
483    close $fh_from unless ref $file;
484    $fh->close or return;
485    $bytes;
486}
487
488=head2 store_content
489
490    $mogc->store_content($key, $class, $content[, $opts]);
491
492Wrapper around new_file, print, and close.  Given a key, class, and
493file contents (scalar or scalarref), stores the file contents in
494MogileFS. Returns the number of bytes stored on success, undef on
495failure.
496
497=cut
498
499sub store_content {
500    my MogileFS::Client $self = shift;
501    return undef if $self->{readonly};
502
503    my($key, $class, $content, $opts) = @_;
504
505    $self->run_hook('store_content_start', $self, $key, $class, $opts);
506
507    my $fh = $self->new_file($key, $class, undef, $opts) or return;
508    $content = ref($content) eq 'SCALAR' ? $$content : $content;
509    $fh->print($content);
510
511    $self->run_hook('store_content_end', $self, $key, $class, $opts);
512
513    $fh->close or return;
514    length($content);
515}
516
517=head2 get_paths
518
519  @paths = $mogc->get_paths($key)
520  @paths = $mogc->get_paths($key, $no_verify_bool); # old way
521  @paths = $mogc->get_paths($key, { noverify => $bool }); # new way
522
523Given a key, returns an array of all the locations (HTTP URLs) that
524the file has been replicated to.
525
526=over
527
528=item noverify
529
530If the "no verify" option is set, the mogilefsd tracker doesn't verify
531that the first item returned in the list is up/alive.  Skipping that
532check is faster, so use "noverify" if your application can do it
533faster/smarter.  For instance, when giving L<Perlbal> a list of URLs
534to reproxy to, Perlbal can intelligently find one that's alive, so use
535noverify and get out of mod_perl or whatever as soon as possible.
536
537=item zone
538
539If the zone option is set to 'alt', the mogilefsd tracker will use the
540alternative IP for each host if available, while constructing the paths.
541
542=item pathcount
543
544If the pathcount option is set to a positive integer greater than 2, the
545mogilefsd tracker will attempt to return that many different paths (if
546available) to the same file. If not present or out of range, this value
547defaults to 2.
548
549=back
550
551=cut
552
553# old style calling:
554#   get_paths(key, noverify)
555# new style calling:
556#   get_paths(key, { noverify => 0/1, zone => "alt", pathcount => 2..N });
557# but with both, second parameter is optional
558#
559# returns list of URLs that key can be found at, or the empty
560# list on either error or no paths
561sub get_paths {
562    my MogileFS::Client $self = shift;
563    my ($key, $opts) = @_;
564
565    # handle parameters, if any
566    my ($noverify, $zone);
567    unless (ref $opts) {
568        $opts = { noverify => $opts };
569    }
570    my %extra_args;
571
572    $noverify = 1 if $opts->{noverify};
573    $zone = $opts->{zone};
574
575    if (my $pathcount = delete $opts->{pathcount}) {
576        $extra_args{pathcount} = $pathcount;
577    }
578
579    $self->run_hook('get_paths_start', $self, $key, $opts);
580
581    my $res = $self->{backend}->do_request
582        ("get_paths", {
583            domain => $self->{domain},
584            key    => $key,
585            noverify => $noverify ? 1 : 0,
586            zone   => $zone,
587	    %extra_args,
588        }) or return ();
589
590    my @paths = map { $res->{"path$_"} } (1..$res->{paths});
591
592    $self->run_hook('get_paths_end', $self, $key, $opts);
593
594    return @paths;
595}
596
597=head2 get_file_data
598
599  $dataref = $mogc->get_file_data($key)
600
601Wrapper around get_paths & LWP, which returns scalarref of file
602contents in a scalarref.
603
604Don't use for large data, as it all comes back to you in one string.
605
606=cut
607
608# given a key, returns a scalar reference pointing at a string containing
609# the contents of the file. takes one parameter; a scalar key to get the
610# data for the file.
611sub get_file_data {
612    # given a key, load some paths and get data
613    my MogileFS::Client $self = $_[0];
614    my ($key, $timeout) = ($_[1], $_[2]);
615
616    my @paths = $self->get_paths($key, 1);
617    return undef unless @paths;
618
619    # iterate over each
620    foreach my $path (@paths) {
621        next unless defined $path;
622        if ($path =~ m!^http://!) {
623            # try via HTTP
624            my $ua = new LWP::UserAgent;
625            $ua->timeout($timeout || 10);
626
627            my $res = $ua->get($path);
628            if ($res->is_success) {
629                my $contents = $res->content;
630                return \$contents;
631            }
632
633        } else {
634            # open the file from disk and just grab it all
635            open FILE, "<$path" or next;
636            my $contents;
637            { local $/ = undef; $contents = <FILE>; }
638            close FILE;
639            return \$contents if $contents;
640        }
641    }
642    return undef;
643}
644
645=head2 delete
646
647    $mogc->delete($key);
648
649Delete a key from MogileFS.
650
651=cut
652
653# this method returns undef only on a fatal error such as inability to actually
654# delete a resource and inability to contact the server.  attempting to delete
655# something that doesn't exist counts as success, as it doesn't exist.
656sub delete {
657    my MogileFS::Client $self = shift;
658    return undef if $self->{readonly};
659
660    my $key = shift;
661
662    my $rv = $self->{backend}->do_request
663        ("delete", {
664            domain => $self->{domain},
665            key    => $key,
666        });
667
668    # if it's unknown_key, not an error
669    return undef unless defined $rv ||
670                        $self->{backend}->{lasterr} eq 'unknown_key';
671
672    return 1;
673}
674
675=head2 rename
676
677  $mogc->rename($oldkey, $newkey);
678
679Rename file (key) in MogileFS from oldkey to newkey.  Returns true on
680success, failure otherwise.
681
682=cut
683
684# this method renames a file.  it returns an undef on error (only a fatal error
685# is considered as undef; "file didn't exist" isn't an error).
686sub rename {
687    my MogileFS::Client $self = shift;
688    return undef if $self->{readonly};
689
690    my ($fkey, $tkey) = @_;
691
692    my $rv = $self->{backend}->do_request
693        ("rename", {
694            domain   => $self->{domain},
695            from_key => $fkey,
696            to_key   => $tkey,
697        });
698
699    # if it's unknown_key, not an error
700    return undef unless defined $rv ||
701                        $self->{backend}->{lasterr} eq 'unknown_key';
702
703    return 1;
704}
705
706=head2 file_debug
707
708    my $info_gob = $mogc->file_debug(fid => $fid);
709    ... or ...
710    my $info_gob = $mogc->file_debug(key => $key);
711
712Thoroughly search for any database notes about a particular fid. Searchable by
713raw fidid, or by domain and key. B<Use sparingly>. Command hits the master
714database numerous times, and if you're using it in production something is
715likely very wrong.
716
717To be used with troubleshooting broken/odd files and errors from mogilefsd.
718
719=cut
720
721sub file_debug {
722    my MogileFS::Client $self = shift;
723    my %opts = @_;
724    $opts{domain} = $self->{domain} unless exists $opts{domain};
725
726    my $res = $self->{backend}->do_request
727        ("file_debug", {
728            %opts,
729        }) or return undef;
730    return $res;
731}
732
733=head2 file_info
734
735    my $fid = $mogc->file_info($key, { devices => 0 });
736
737Used to return metadata about a file. Returns the domain, class, expected
738length, devcount, etc. Optionally device ids (not paths) can be returned as
739well.
740
741Should be used for informational purposes, and not usually for dynamically
742serving files.
743
744=cut
745
746sub file_info {
747    my MogileFS::Client $self = shift;
748    my ($key, $opts) = @_;
749
750    my %extra = ();
751    $extra{devices} = delete $opts->{devices};
752    die "Unknown arguments: " . join(', ', keys %$opts) if keys %$opts;
753
754    my $res = $self->{backend}->do_request
755        ("file_info", {
756            domain => $self->{domain},
757            key    => $key,
758            %extra,
759        }) or return undef;
760    return $res;
761}
762
763=head2 list_keys
764
765    $keys = $mogc->list_keys($prefix, $after[, $limit]);
766    ($after, $keys) = $mogc->list_keys($prefix, $after[, $limit]);
767
768Used to get a list of keys matching a certain prefix.
769
770$prefix specifies what you want to get a list of.  $after is the item
771specified as a return value from this function last time you called
772it.  $limit is optional and defaults to 1000 keys returned.
773
774In list context, returns ($after, $keys).  In scalar context, returns
775arrayref of keys.  The value $after is to be used as $after when you
776call this function again.
777
778When there are no more keys in the list, you will get back undef or
779an empty list.
780
781=cut
782
783sub list_keys {
784    my MogileFS::Client $self = shift;
785    my ($prefix, $after, $limit) = @_;
786
787    my $res = $self->{backend}->do_request
788        ("list_keys", {
789            domain => $self->{domain},
790            prefix => $prefix,
791            after => $after,
792            limit => $limit,
793        }) or return undef;
794
795    # construct our list of keys and the new after value
796    my $resafter = $res->{next_after};
797    my $reslist = [];
798    for (my $i = 1; $i <= $res->{key_count}+0; $i++) {
799        push @$reslist, $res->{"key_$i"};
800    }
801    return wantarray ? ($resafter, $reslist) : $reslist;
802}
803
804=head2 foreach_key
805
806  $mogc->foreach_key( %OPTIONS, sub { my $key = shift; ... } );
807  $mogc->foreach_key( prefix => "foo:", sub { my $key = shift; ... } );
808
809
810Functional interface/wrapper around list_keys.
811
812Given some %OPTIONS (currently only one, "prefix"), calls your callback
813for each key matching the provided prefix.
814
815=cut
816
817sub foreach_key {
818    my MogileFS::Client $self = shift;
819    my $callback = pop;
820    Carp::croak("Last parameter not a subref") unless ref $callback eq "CODE";
821    my %opts = @_;
822    my $prefix = delete $opts{prefix};
823    Carp::croak("Unknown option(s): " . join(", ", keys %opts)) if %opts;
824
825    my $last = "";
826    my $max = 1000;
827    my $count = $max;
828
829    while ($count == $max) {
830        my $res = $self->{backend}->do_request
831            ("list_keys", {
832                domain => $self->{domain},
833                prefix => $prefix,
834                after => $last,
835                limit => $max,
836            }) or return undef;
837        $count = $res->{key_count}+0;
838        for (my $i = 1; $i <= $count; $i++) {
839            $callback->($res->{"key_$i"});
840        }
841        $last = $res->{"key_$count"};
842    }
843    return 1;
844}
845
846# just makes some sleeping happen.  first and only argument is number of
847# seconds to instruct backend thread to sleep for.
848sub sleep {
849    my MogileFS::Client $self = shift;
850    my $duration = shift;
851
852    $self->{backend}->do_request("sleep", { duration => $duration + 0 })
853        or return undef;
854
855    return 1;
856}
857
858=head2 update_class
859
860    $mogc->update_class($key, $newclass);
861
862Update the replication class of a pre-existing file, causing
863the file to become more or less replicated.
864
865=cut
866
867sub update_class {
868    my MogileFS::Client $self = shift;
869    my ($key, $class) = @_;
870    my $res = $self->{backend}->do_request
871            ("updateclass", {
872                domain => $self->{domain},
873                key => $key,
874                class => $class,
875            }) or return undef;
876    return $res;
877}
878
879=head2 set_pref_ip
880
881  $mogc->set_pref_ip({ "10.0.0.2" => "10.2.0.2" });
882
883Weird option for old, weird network architecture.  Sets a mapping
884table of preferred alternate IPs, if reachable.  For instance, if
885trying to connect to 10.0.0.2 in the above example, the module would
886instead try to connect to 10.2.0.2 quickly first, then then fall back
887to 10.0.0.2 if 10.2.0.2 wasn't reachable.
888
889=cut
890
891# expects as argument a hashref of "standard-ip" => "preferred-ip"
892sub set_pref_ip {
893    my MogileFS::Client $self = shift;
894    $self->{backend}->set_pref_ip(shift)
895        if $self->{backend};
896}
897
898=head1 PLUGIN METHODS
899
900WRITEME
901
902=cut
903
904# used to support plugins that have modified the server, this builds things into
905# an argument list and passes them back to the server
906# TODO: there is no readonly protection here?  does it matter?  should we check
907# with the server to see what methods they support?  (and if they should be disallowed
908# when the client is in readonly mode?)
909sub AUTOLOAD {
910    # remove everything up to the last colon, so we only have the method name left
911    my $method = $AUTOLOAD;
912    $method =~ s/^.*://;
913
914    return if $method eq 'DESTROY';
915
916    # let this work
917    no strict 'refs';
918
919    # create a method to pass this on back
920    *{$AUTOLOAD} = sub {
921        my MogileFS::Client $self = shift;
922        # pre-assemble the arguments into a hashref
923        my $ct = 0;
924        my $args = {};
925        $args->{"arg" . ++$ct} = shift() while @_;
926        $args->{"argcount"} = $ct;
927
928        # now put in standard args
929        $args->{"domain"} = $self->{domain};
930
931        # now call and return whatever we get back from the backend
932        return $self->{backend}->do_request("plugin_$method", $args);
933    };
934
935    # now pass through
936    goto &$AUTOLOAD;
937}
938
939=head1 HOOKS
940
941=head2 add_hook
942
943WRITEME
944
945=cut
946
947sub add_hook {
948    my MogileFS::Client $self = shift;
949    my $hookname = shift || return;
950
951    if (@_) {
952        $self->{hooks}->{$hookname} = shift;
953    } else {
954        delete $self->{hooks}->{$hookname};
955    }
956}
957
958sub run_hook {
959    my MogileFS::Client $self = shift;
960    my $hookname = shift || return;
961
962    my $hook = $self->{hooks}->{$hookname};
963    return unless $hook;
964
965    eval { $hook->(@_) };
966
967    warn "MogileFS::Client hook '$hookname' threw error: $@\n" if $@;
968}
969
970=head2 add_backend_hook
971
972WRITEME
973
974=cut
975
976sub add_backend_hook {
977    my MogileFS::Client $self = shift;
978    my $backend = $self->{backend};
979
980    $backend->add_hook(@_);
981}
982
983
984################################################################################
985# MogileFS class methods
986#
987
988sub _fail {
989    croak "MogileFS: $_[0]";
990}
991
992sub _debug {
993    return 1 unless $MogileFS::DEBUG;
994
995    my $msg = shift;
996    my $ref = shift;
997    chomp $msg;
998
999    eval "use Data::Dumper;";
1000    print STDERR "$msg\n" . Dumper($ref) . "\n";
1001    return 1;
1002}
1003
1004
10051;
1006__END__
1007
1008=head1 SEE ALSO
1009
1010L<http://www.danga.com/mogilefs/>
1011
1012=head1 COPYRIGHT
1013
1014This module is Copyright 2003-2004 Brad Fitzpatrick,
1015and copyright 2005-2007 Six Apart, Ltd.
1016
1017All rights reserved.
1018
1019You may distribute under the terms of either the GNU General Public
1020License or the Artistic License, as specified in the Perl README file.
1021
1022=head1 WARRANTY
1023
1024This is free software. IT COMES WITHOUT WARRANTY OF ANY KIND.
1025
1026=head1 AUTHORS
1027
1028Brad Fitzpatrick <brad@danga.com>
1029
1030Brad Whitaker <whitaker@danga.com>
1031
1032Mark Smith <marksmith@danga.com>
1033
1034