Index: /branches/jhoblitt/Nebulous-Server/Build.PL
===================================================================
--- /branches/jhoblitt/Nebulous-Server/Build.PL	(revision 2784)
+++ /branches/jhoblitt/Nebulous-Server/Build.PL	(revision 2784)
@@ -0,0 +1,30 @@
+use Module::Build;
+# See perldoc Module::Build for details of how this works
+
+my $build_pkg   = eval { require Apache::TestMB }
+                ? 'Apache::TestMB'
+                : 'Module::Build';
+
+$build_pkg->new(
+    module_name         => 'PS::IPP::IData',
+    dist_version_from   => 'lib/PS/IPP/IData/Server.pm',
+    author              => 'Joshua Hoblitt <jhoblitt@cpan.org>',
+    license             => 'gpl',
+    requires            => {
+        'URI'                   => 1.30,
+        'SOAP::Lite'            => 0.60,
+        'Log::Log4perl'         => 0.48,
+        'DBI'                   => 1.42,
+        'DBD::mysql'            => 2.9004,
+        'Class::Accessor::Fast' => 0,
+        'Params::Validate'      => 0,
+        'Apache::Test'          => 1.12,
+        'Test::More'            => 0.49,
+    },
+    recommends          => {
+        'Apache::DBI'           => 0.94,
+    },
+    build_requires      => {
+        'File::Temp'            => 0,
+    },
+)->create_build_script;
Index: /branches/jhoblitt/Nebulous-Server/Changes
===================================================================
--- /branches/jhoblitt/Nebulous-Server/Changes	(revision 2784)
+++ /branches/jhoblitt/Nebulous-Server/Changes	(revision 2784)
@@ -0,0 +1,5 @@
+Revision history for Perl module Poi::PixelData
+
+0.01 Fri Sep  3 10:23:31 2004
+    - first working version
+	- original version; created by ExtUtils::ModuleMaker 0.32
Index: /branches/jhoblitt/Nebulous-Server/LICENSE
===================================================================
--- /branches/jhoblitt/Nebulous-Server/LICENSE	(revision 2784)
+++ /branches/jhoblitt/Nebulous-Server/LICENSE	(revision 2784)
@@ -0,0 +1,260 @@
+The General Public License (GPL)
+Version 2, June 1991
+
+Copyright (C) 1989, 1991 Free Software Foundation, Inc. 675 Mass Ave,
+Cambridge, MA 02139, USA. Everyone is permitted to copy and distribute
+verbatim copies of this license document, but changing it is not allowed.
+
+Preamble
+
+The licenses for most software are designed to take away your freedom to share
+and change it. By contrast, the GNU General Public License is intended to
+guarantee your freedom to share and change free software--to make sure the
+software is free for all its users. This General Public License applies to most of
+the Free Software Foundation's software and to any other program whose
+authors commit to using it. (Some other Free Software Foundation software is
+covered by the GNU Library General Public License instead.) You can apply it to
+your programs, too.
+
+When we speak of free software, we are referring to freedom, not price. Our
+General Public Licenses are designed to make sure that you have the freedom
+to distribute copies of free software (and charge for this service if you wish), that
+you receive source code or can get it if you want it, that you can change the
+software or use pieces of it in new free programs; and that you know you can do
+these things.
+
+To protect your rights, we need to make restrictions that forbid anyone to deny
+you these rights or to ask you to surrender the rights. These restrictions
+translate to certain responsibilities for you if you distribute copies of the
+software, or if you modify it.
+
+For example, if you distribute copies of such a program, whether gratis or for a
+fee, you must give the recipients all the rights that you have. You must make
+sure that they, too, receive or can get the source code. And you must show
+them these terms so they know their rights.
+
+We protect your rights with two steps: (1) copyright the software, and (2) offer
+you this license which gives you legal permission to copy, distribute and/or
+modify the software.
+
+Also, for each author's protection and ours, we want to make certain that
+everyone understands that there is no warranty for this free software. If the
+software is modified by someone else and passed on, we want its recipients to
+know that what they have is not the original, so that any problems introduced by
+others will not reflect on the original authors' reputations.
+
+Finally, any free program is threatened constantly by software patents. We wish
+to avoid the danger that redistributors of a free program will individually obtain
+patent licenses, in effect making the program proprietary. To prevent this, we
+have made it clear that any patent must be licensed for everyone's free use or
+not licensed at all.
+
+The precise terms and conditions for copying, distribution and modification
+follow.
+
+GNU GENERAL PUBLIC LICENSE
+TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND
+MODIFICATION
+
+0. This License applies to any program or other work which contains a notice
+placed by the copyright holder saying it may be distributed under the terms of
+this General Public License. The "Program", below, refers to any such program
+or work, and a "work based on the Program" means either the Program or any
+derivative work under copyright law: that is to say, a work containing the
+Program or a portion of it, either verbatim or with modifications and/or translated
+into another language. (Hereinafter, translation is included without limitation in
+the term "modification".) Each licensee is addressed as "you".
+
+Activities other than copying, distribution and modification are not covered by
+this License; they are outside its scope. The act of running the Program is not
+restricted, and the output from the Program is covered only if its contents
+constitute a work based on the Program (independent of having been made by
+running the Program). Whether that is true depends on what the Program does.
+
+1. You may copy and distribute verbatim copies of the Program's source code as
+you receive it, in any medium, provided that you conspicuously and appropriately
+publish on each copy an appropriate copyright notice and disclaimer of warranty;
+keep intact all the notices that refer to this License and to the absence of any
+warranty; and give any other recipients of the Program a copy of this License
+along with the Program.
+
+You may charge a fee for the physical act of transferring a copy, and you may at
+your option offer warranty protection in exchange for a fee.
+
+2. You may modify your copy or copies of the Program or any portion of it, thus
+forming a work based on the Program, and copy and distribute such
+modifications or work under the terms of Section 1 above, provided that you also
+meet all of these conditions:
+
+a) You must cause the modified files to carry prominent notices stating that you
+changed the files and the date of any change.
+
+b) You must cause any work that you distribute or publish, that in whole or in
+part contains or is derived from the Program or any part thereof, to be licensed
+as a whole at no charge to all third parties under the terms of this License.
+
+c) If the modified program normally reads commands interactively when run, you
+must cause it, when started running for such interactive use in the most ordinary
+way, to print or display an announcement including an appropriate copyright
+notice and a notice that there is no warranty (or else, saying that you provide a
+warranty) and that users may redistribute the program under these conditions,
+and telling the user how to view a copy of this License. (Exception: if the
+Program itself is interactive but does not normally print such an announcement,
+your work based on the Program is not required to print an announcement.)
+
+These requirements apply to the modified work as a whole. If identifiable
+sections of that work are not derived from the Program, and can be reasonably
+considered independent and separate works in themselves, then this License,
+and its terms, do not apply to those sections when you distribute them as
+separate works. But when you distribute the same sections as part of a whole
+which is a work based on the Program, the distribution of the whole must be on
+the terms of this License, whose permissions for other licensees extend to the
+entire whole, and thus to each and every part regardless of who wrote it.
+
+Thus, it is not the intent of this section to claim rights or contest your rights to
+work written entirely by you; rather, the intent is to exercise the right to control
+the distribution of derivative or collective works based on the Program.
+
+In addition, mere aggregation of another work not based on the Program with the
+Program (or with a work based on the Program) on a volume of a storage or
+distribution medium does not bring the other work under the scope of this
+License.
+
+3. You may copy and distribute the Program (or a work based on it, under
+Section 2) in object code or executable form under the terms of Sections 1 and 2
+above provided that you also do one of the following:
+
+a) Accompany it with the complete corresponding machine-readable source
+code, which must be distributed under the terms of Sections 1 and 2 above on a
+medium customarily used for software interchange; or,
+
+b) Accompany it with a written offer, valid for at least three years, to give any
+third party, for a charge no more than your cost of physically performing source
+distribution, a complete machine-readable copy of the corresponding source
+code, to be distributed under the terms of Sections 1 and 2 above on a medium
+customarily used for software interchange; or,
+
+c) Accompany it with the information you received as to the offer to distribute
+corresponding source code. (This alternative is allowed only for noncommercial
+distribution and only if you received the program in object code or executable
+form with such an offer, in accord with Subsection b above.)
+
+The source code for a work means the preferred form of the work for making
+modifications to it. For an executable work, complete source code means all the
+source code for all modules it contains, plus any associated interface definition
+files, plus the scripts used to control compilation and installation of the
+executable. However, as a special exception, the source code distributed need
+not include anything that is normally distributed (in either source or binary form)
+with the major components (compiler, kernel, and so on) of the operating system
+on which the executable runs, unless that component itself accompanies the
+executable.
+
+If distribution of executable or object code is made by offering access to copy
+from a designated place, then offering equivalent access to copy the source
+code from the same place counts as distribution of the source code, even though
+third parties are not compelled to copy the source along with the object code.
+
+4. You may not copy, modify, sublicense, or distribute the Program except as
+expressly provided under this License. Any attempt otherwise to copy, modify,
+sublicense or distribute the Program is void, and will automatically terminate
+your rights under this License. However, parties who have received copies, or
+rights, from you under this License will not have their licenses terminated so long
+as such parties remain in full compliance.
+
+5. You are not required to accept this License, since you have not signed it.
+However, nothing else grants you permission to modify or distribute the Program
+or its derivative works. These actions are prohibited by law if you do not accept
+this License. Therefore, by modifying or distributing the Program (or any work
+based on the Program), you indicate your acceptance of this License to do so,
+and all its terms and conditions for copying, distributing or modifying the
+Program or works based on it.
+
+6. Each time you redistribute the Program (or any work based on the Program),
+the recipient automatically receives a license from the original licensor to copy,
+distribute or modify the Program subject to these terms and conditions. You
+may not impose any further restrictions on the recipients' exercise of the rights
+granted herein. You are not responsible for enforcing compliance by third parties
+to this License.
+
+7. If, as a consequence of a court judgment or allegation of patent infringement
+or for any other reason (not limited to patent issues), conditions are imposed on
+you (whether by court order, agreement or otherwise) that contradict the
+conditions of this License, they do not excuse you from the conditions of this
+License. If you cannot distribute so as to satisfy simultaneously your obligations
+under this License and any other pertinent obligations, then as a consequence
+you may not distribute the Program at all. For example, if a patent license would
+not permit royalty-free redistribution of the Program by all those who receive
+copies directly or indirectly through you, then the only way you could satisfy
+both it and this License would be to refrain entirely from distribution of the
+Program.
+
+If any portion of this section is held invalid or unenforceable under any particular
+circumstance, the balance of the section is intended to apply and the section as
+a whole is intended to apply in other circumstances.
+
+It is not the purpose of this section to induce you to infringe any patents or other
+property right claims or to contest validity of any such claims; this section has
+the sole purpose of protecting the integrity of the free software distribution
+system, which is implemented by public license practices. Many people have
+made generous contributions to the wide range of software distributed through
+that system in reliance on consistent application of that system; it is up to the
+author/donor to decide if he or she is willing to distribute software through any
+other system and a licensee cannot impose that choice.
+
+This section is intended to make thoroughly clear what is believed to be a
+consequence of the rest of this License.
+
+8. If the distribution and/or use of the Program is restricted in certain countries
+either by patents or by copyrighted interfaces, the original copyright holder who
+places the Program under this License may add an explicit geographical
+distribution limitation excluding those countries, so that distribution is permitted
+only in or among countries not thus excluded. In such case, this License
+incorporates the limitation as if written in the body of this License.
+
+9. The Free Software Foundation may publish revised and/or new versions of the
+General Public License from time to time. Such new versions will be similar in
+spirit to the present version, but may differ in detail to address new problems or
+concerns.
+
+Each version is given a distinguishing version number. If the Program specifies a
+version number of this License which applies to it and "any later version", you
+have the option of following the terms and conditions either of that version or of
+any later version published by the Free Software Foundation. If the Program does
+not specify a version number of this License, you may choose any version ever
+published by the Free Software Foundation.
+
+10. If you wish to incorporate parts of the Program into other free programs
+whose distribution conditions are different, write to the author to ask for
+permission. For software which is copyrighted by the Free Software Foundation,
+write to the Free Software Foundation; we sometimes make exceptions for this.
+Our decision will be guided by the two goals of preserving the free status of all
+derivatives of our free software and of promoting the sharing and reuse of
+software generally.
+
+NO WARRANTY
+
+11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS
+NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
+APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE
+COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM
+"AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR
+IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
+MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE
+ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE
+PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE,
+YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR
+CORRECTION.
+
+12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED
+TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY
+WHO MAY MODIFY AND/OR REDISTRIBUTE THE PROGRAM AS
+PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
+GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES
+ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM
+(INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING
+RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
+PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY
+OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS
+BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES.
+
+END OF TERMS AND CONDITIONS
Index: /branches/jhoblitt/Nebulous-Server/MANIFEST
===================================================================
--- /branches/jhoblitt/Nebulous-Server/MANIFEST	(revision 2784)
+++ /branches/jhoblitt/Nebulous-Server/MANIFEST	(revision 2784)
@@ -0,0 +1,53 @@
+Build.PL
+Changes
+LICENSE
+MANIFEST
+META.yml
+Makefile.PL
+README
+TODO
+Todo
+bootstrap.sh
+docs/apache_setup.txt
+docs/c_api.h
+docs/calc_gid.c
+docs/database_setup.txt
+docs/design.txt
+docs/requirements.txt
+docs/setup.txt
+docs/tmp.txt
+iData.wsdl
+lib/PS/IPP/IData/Client.pm
+lib/PS/IPP/IData/Client/HTTP.pm
+lib/PS/IPP/IData/Client/Log.pm
+lib/PS/IPP/IData/Server.pm
+lib/PS/IPP/IData/Server/Apache.pm
+lib/PS/IPP/IData/Server/Log.pm
+lib/PS/IPP/IData/Server/SQL.pm
+lib/PS/IPP/IData/Util.pm
+scripts/bench_test.pl
+scripts/imageserver.pl
+scripts/ptest.pl
+scripts/stest.pl
+t/01_load.t
+t/02_server_setup.t
+t/03_server_create_object.t
+t/04_server_replicate_object.t
+t/05_server_lock_object.t
+t/06_server_unlock_object.t
+t/07_server_find_instances.t
+t/08_server_delete_instance.t
+t/09_server_stat_object.t
+t/10_client_new.t
+t/11_client_create.t
+t/12_client_replicate.t
+t/13_client_cull.t
+t/14_client_lock.t
+t/15_client_unlock.t
+t/16_client_find_instances.t
+t/TEST.PL
+t/Test/IData.pm
+t/apache_03_server_create_object.x
+t/apache_04_server_replicate_object.x
+t/apache_05_server_lock_object.x
+t/conf/extra.conf.in
Index: /branches/jhoblitt/Nebulous-Server/Makefile.PL
===================================================================
--- /branches/jhoblitt/Nebulous-Server/Makefile.PL	(revision 2784)
+++ /branches/jhoblitt/Nebulous-Server/Makefile.PL	(revision 2784)
@@ -0,0 +1,27 @@
+unless (eval "use Module::Build::Compat 0.02; 1" ) {
+  print "This module requires Module::Build to install itself.\n";
+
+  require ExtUtils::MakeMaker;
+  my $yn = ExtUtils::MakeMaker::prompt
+    ('  Install Module::Build from CPAN?', 'y');
+
+  if ($yn =~ /^y/i) {
+    require Cwd;
+    require File::Spec;
+    require CPAN;
+
+    # Save this 'cause CPAN will chdir all over the place.
+    my $cwd = Cwd::cwd();
+    my $makefile = File::Spec->rel2abs($0);
+
+    CPAN::Shell->install('Module::Build::Compat');
+
+    chdir $cwd or die "Cannot chdir() back to $cwd: $!";
+    exec $^X, $makefile, @ARGV;  # Redo now that we have Module::Build
+  } else {
+    warn " *** Cannot install without Module::Build.  Exiting ...\n";
+    exit 1;
+  }
+}
+Module::Build::Compat->run_build_pl(args => \@ARGV);
+Module::Build::Compat->write_makefile();
Index: /branches/jhoblitt/Nebulous-Server/README
===================================================================
--- /branches/jhoblitt/Nebulous-Server/README	(revision 2784)
+++ /branches/jhoblitt/Nebulous-Server/README	(revision 2784)
@@ -0,0 +1,16 @@
+pod2text Poi::PixelData.pm > README
+
+If this is still here it means the programmer was too lazy to create the readme file.
+
+You can create it now by using the command shown above from this directory.
+
+At the very least you should be able to use this set of instructions
+to install the module...
+
+perl Build.PL
+./Build
+./Build test
+./Build install
+
+
+If you are on a windows box you should use 'nmake' rather than 'make'.
Index: /branches/jhoblitt/Nebulous-Server/TODO
===================================================================
--- /branches/jhoblitt/Nebulous-Server/TODO	(revision 2784)
+++ /branches/jhoblitt/Nebulous-Server/TODO	(revision 2784)
@@ -0,0 +1,17 @@
+cache some database tables (updated at some interval) 
+reuse/cache database statement handles
+code tiding/consistency
+volume handling for instance placement
+file creation retrying
+server config file
+make Apache::DBI behave
+WSDL
+split SOAP out of Client.pm
+error string support
+
+distant future
+--
+volume size/% in-use discovery
+server side per client lock tracking
+client side lookup table of filehandles/locks
+Perl only?, scoped lock release/filehandle destruction
Index: /branches/jhoblitt/Nebulous-Server/Todo
===================================================================
--- /branches/jhoblitt/Nebulous-Server/Todo	(revision 2784)
+++ /branches/jhoblitt/Nebulous-Server/Todo	(revision 2784)
@@ -0,0 +1,5 @@
+TODO list for Perl module Poi::PixelData
+
+- Nothing yet
+
+
Index: /branches/jhoblitt/Nebulous-Server/bootstrap-moa-mp1.sh
===================================================================
--- /branches/jhoblitt/Nebulous-Server/bootstrap-moa-mp1.sh	(revision 2784)
+++ /branches/jhoblitt/Nebulous-Server/bootstrap-moa-mp1.sh	(revision 2784)
@@ -0,0 +1,1 @@
+perl Build.PL -http_conf /usr/local/apache/conf/httpd.conf -apxs /usr/local/apache/bin/apxs -httpd /usr/local/apache/bin/httpd 
Index: /branches/jhoblitt/Nebulous-Server/docs/apache_setup.txt
===================================================================
--- /branches/jhoblitt/Nebulous-Server/docs/apache_setup.txt	(revision 2784)
+++ /branches/jhoblitt/Nebulous-Server/docs/apache_setup.txt	(revision 2784)
@@ -0,0 +1,37 @@
+- add the "idata" group to /etc/group
+- configure Apache to run as the idata group. 
+
+#
+# CGI interface
+#
+
+ScriptAlias /idata /usr/local/apache/perl/imageserver.pl
+
+<Directory "/usr/local/apache/perl">
+        AllowOverride None
+        Options None
+        Order allow,deny
+        Allow from all
+</Directory>
+
+PerlModule Apache::PerlRun
+<Location /perl>
+        SetHandler perl-script
+        PerlHandler Apache::PerlRun
+        Options ExecCGI
+        allow from all
+        PerlSendHeader On
+</Location>
+
+#
+# PS::IPP::IData::Apache interface
+#
+
+PerlModule Apache::DBI
+PerlModule PS::IPP::IData::Apache
+
+<Location /idata>
+        SetHandler perl-script
+        PerlHandler PS::IPP::IData::Apache
+</Location>
+
Index: /branches/jhoblitt/Nebulous-Server/docs/c_api.h
===================================================================
--- /branches/jhoblitt/Nebulous-Server/docs/c_api.h	(revision 2784)
+++ /branches/jhoblitt/Nebulous-Server/docs/c_api.h	(revision 2784)
@@ -0,0 +1,24 @@
+/*
+ * Copyright (C) 2004 Joshua Hoblitt
+ *
+ * $Id: c_api.h,v 1.1.1.1 2004-12-22 02:16:23 jhoblitt Exp $
+ */
+
+// should there be an argument for "suggested" storage location?
+int add( const char *ext_id, const char *data, int size )
+
+int delete( const char *ext_id )
+
+// should there be an argument for "suggested" storage location?
+int update( const char *ext_id, const char *data, int size )
+
+// should there be an argument for "suggested" storage location?
+int move( const char *old_ext_id, const char *new_ext_id )
+
+// open returns a fd 
+int open( const char *ext_id )
+
+int close( const char *ext_id )
+
+// maybe open and close are a bad idea, perhaps...
+int read( const char *ext_id, char *data )
Index: /branches/jhoblitt/Nebulous-Server/docs/calc_gid.c
===================================================================
--- /branches/jhoblitt/Nebulous-Server/docs/calc_gid.c	(revision 2784)
+++ /branches/jhoblitt/Nebulous-Server/docs/calc_gid.c	(revision 2784)
@@ -0,0 +1,5 @@
+#include <stdio.h>
+
+main () {
+    printf( "%d\n", 'i'+'m'+'a'+'g'+'e'+'d'+'a'+'t'+'a' + 's'+'e'+'r'+'v'+'e'+'r' );
+}
Index: /branches/jhoblitt/Nebulous-Server/docs/database_setup.txt
===================================================================
--- /branches/jhoblitt/Nebulous-Server/docs/database_setup.txt	(revision 2784)
+++ /branches/jhoblitt/Nebulous-Server/docs/database_setup.txt	(revision 2784)
@@ -0,0 +1,147 @@
+# Copyright (C) 2004  Joshua Hoblitt
+#
+# $Id: database_setup.txt,v 1.1.1.1 2004-12-22 02:16:23 jhoblitt Exp $
+
+### Database Setup
+
+DROP DATABASE IF EXISTS pixel;
+CREATE DATABASE pixel;
+INSERT INTO db VALUES ('%','pixel','pixel','y','y','y','y','y','y','y','y','y','y','y','y');
+INSERT INTO user
+(Host,User,Password,Select_priv,Insert_priv,Update_priv,Delete_priv,Create_priv,Drop_priv,Index_priv,Alter_priv,Show_db_priv)
+VALUES ('%','pixel',password('pixel'),'y','y','y','y','y','y','y','y','y');
+INSERT INTO user
+(Host,User,Password,Select_priv,Insert_priv,Update_priv,Delete_priv,Create_priv,Drop_priv,Index_priv,Alter_priv,Show_db_priv)
+VALUES ('localhost','pixel',password('pixel'),'y','y','y','y','y','y','y','y','y');
+FLUSH PRIVILEGES;
+
+USE pixel;
+
+### MySQL 4.0
+
+DROP TABLE IF EXISTS storage_object;
+CREATE TABLE storage_object (
+    so_id BIGINT NOT NULL AUTO_INCREMENT,
+    ext_id VARCHAR(255) BINARY NOT NULL UNIQUE,
+    class_id TINYINT NOT NULL,
+    comment VARCHAR(255),
+    read_lock TINYINT DEFAULT 0 NOT NULL,
+    write_lock ENUM( 'write' ),
+    epoch TIMESTAMP NOT NULL,
+    PRIMARY KEY(so_id),
+    KEY(ext_id(64)),
+    KEY(class_id)
+) ENGINE=innodb;
+
+DROP TABLE IF EXISTS instance;
+CREATE TABLE instance ( 
+    ins_id BIGINT NOT NULL AUTO_INCREMENT,
+    so_id BIGINT NOT NULL,
+    uri VARCHAR(255) BINARY NOT NULL UNIQUE,
+    sha1sum CHAR(40),
+    assigned_location BOOL,
+    epoch TIMESTAMP NOT NULL,
+    atime TIMESTAMP NOT NULL,
+    PRIMARY KEY(ins_id),
+    KEY(so_id),
+    KEY(uri(64))
+) ENGINE=innodb;
+
+DROP TABLE IF EXISTS lock_record;
+CREATE TABLE lock_record (
+        so_id BIGINT NOT NULL,
+        type ENUM( 'read', 'write' ) NOT NULL,
+        epoch TIMESTAMP,
+        KEY(so_ID)
+    ) ENGINE=innodb;
+
+DROP TABLE IF EXISTS volume;
+CREATE TABLE volume (
+        vol_id INT NOT NULL AUTO_INCREMENT,
+        name VARCHAR(255) UNIQUE BINARY NOT NULL,
+        uri VARCHAR(255) BINARY NOT NULL,
+        key(vol_id)
+    ) ENGINE=innodb;
+
+DROP TABLE IF EXISTS class;
+CREATE TABLE class (
+        class_id TINYINT NOT NULL AUTO_INCREMENT,
+        instance TINYINT NOT NULL,
+        priority TINYINT,
+        comment VARCHAR(255),
+        key(class_id)
+    ) ENGINE=innodb;
+
+### MySQL 4.1
+
+DROP TABLE IF EXISTS storage_object;
+CREATE TABLE storage_object (
+        so_id BIGINT NOT NULL AUTO_INCREMENT,
+        ext_id VARCHAR(255) UNIQUE BINARY NOT NULL,
+        comment VARCHAR(255),
+        epoch TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
+        PRIMARY KEY(so_id),
+        KEY(ext_id(20))
+    ) ENGINE=innodb;
+
+
+DROP TABLE IF EXISTS instance;
+CREATE TABLE instance ( 
+        ins_id BIGINT NOT NULL AUTO_INCREMENT,
+        so_id BIGINT NOT NULL,
+        uri VARCHAR(255) BINARY NOT NULL,
+        sha1sum CHAR(40) ASCII,
+        read_lock TINYINT NOT NULL,
+        write_lock ENUM( 'write', 'wait' ),
+        atime TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+        PRIMARY KEY(ins_id),
+        KEY(so_id)
+    ) ENGINE=innodb;
+
+DROP TABLE IF EXISTS lock_record;
+CREATE TABLE lock_record (
+        ins_id BIGINT NOT NULL,
+        type ENUM( 'read', 'write', 'wait' ) NOT NULL,
+        epoch TIMESTAMP DEFAULT CURRENT_TIMESTAMP
+    ) ENGINE=innodb;
+
+        epoch TIMESTAMP,
+
+### Initialization
+
+# NFS
+
+INSERT INTO volume VALUES (1,'po01','file:/tmp/po01');
+INSERT INTO volume VALUES (2,'po02','file:/tmp/po02');
+INSERT INTO volume VALUES (3,'po03','file:/tmp/po03');
+INSERT INTO volume VALUES (4,'po04','file:/tmp/po04');
+INSERT INTO volume VALUES (5,'po05','file:/tmp/po05');
+INSERT INTO volume VALUES (6,'po06','file:/tmp/po06');
+INSERT INTO volume VALUES (7,'po07','file:/tmp/po07');
+INSERT INTO volume VALUES (8,'po08','file:/tmp/po08');
+
+INSERT INTO class VALUES (1,3,0,'raw image data');
+
+### Example queries
+
+LOAD DATA INFILE '/home/moa/jhoblitt/tmp/storage_objects.txt' INTO TABLE storage_object; 
+LOAD DATA INFILE '/home/moa/jhoblitt/tmp/instances.txt' INTO TABLE instance; 
+
+SELECT * FROM storage_object LEFT JOIN instance ON storage_object.so_id=instance.so_id WHERE storage_object.ext_id = "foobarbaz1.fits";
+SELECT * FROM storage_object LEFT JOIN instance USING (so_id) WHERE storage_object.ext_id = "foobarbaz1.fits";
+
+SET AUTOCOMMIT = 0;
+BEGIN;
+UPDATE instance SET read_lock=read_lock+1 WHERE ins_id=1;
+INSERT INTO lock_record values (1, 'read', NULL);
+COMMIT;
+
+SET AUTOCOMMIT = 0;
+BEGIN;
+INSERT INTO storage_object values (NULL, 'myfooedbar.fits', 'mr. wizard made this', NULL);
+INSERT INTO instance values (NULL, LAST_INSERT_ID(), 'file://tmp/bat/man/12345a', 'da39a3ee5e6b4b0d3255bfef95601890afd80709',0, NULL, NULL);
+COMMIT;
+
+IF select write_lock from instance WHERE id=1 THEN
+ELSE
+ENDIF
Index: /branches/jhoblitt/Nebulous-Server/docs/design.txt
===================================================================
--- /branches/jhoblitt/Nebulous-Server/docs/design.txt	(revision 2784)
+++ /branches/jhoblitt/Nebulous-Server/docs/design.txt	(revision 2784)
@@ -0,0 +1,163 @@
+# Copryight (C) 2004  Joshua Hoblitt
+#
+# $Id: design.txt,v 1.1.1.1 2004-12-22 02:16:23 jhoblitt Exp $
+
+Terminology
+--
+    - Storage Object
+There is one Storage Object per resource (a file) that is being managed by the
+IPP Pixel Data Server.  A Storage Object maps an External Identifier (eg.
+file name) to a Storage ID that is internal to the IPP Pixel Data Server.  In
+addition, creation time and comments may be stored in a Storage Object.
+Storage Objects /do not/ contain the storage location of a resource as we need
+to allow for N copies of a resource to be stored.  The actual storage location
+of a resource is held in a Storage Object Instance, which maps to a Storage ID
+in a many-to-one relationship.
+
+    - [Storage Object] Instance
+A Storage Object Instance, or just Instance, represents a single copy of a
+resource being managed by the IPP Pixel Data Server. An Instance contains an
+Instance ID, the location of the resource as a URI, a checksum of the
+resource, a Storage ID, a read lock semaphore (unsigned integer), a write lock
+mutex (enum( 'write', 'wait', 'free' ) - is this better then using an int and
+constants?), and a UTC timestamp of the last modification date & time.
+
+    - Lock Record
+A Lock Record is used to double check the indicated lock state of an Instance
+and can be used to identify system failures.  It must contain the Instance ID
+of of the locked instance and the type of lock, eg, enum( 'read', 'write').
+
+Locking semantics
+--
+A lock is required to either read from or write to a Storage Object Instance
+being managed by the IPP Pixel Data Server.  Stated another way, locking
+granularity is managed on the level of Storage Object Instances instead of on
+the more abstract Storage Objects.  Although, from the client perspective,
+operations are carried out on a Storage Object.   The locking state of a
+Storage Object Instance is maintained by in the Storage Object Instance and a
+Lock Record.
+
+Two types of locks are used on an Instance, a semaphore is used to track read
+locks and an exclusive mutex is for write locking.
+
+    - Read lock
+In order to read from a Storage Object Instance the Read Lock semaphore must
+first be incremented by one and a Lock Record created.  After the read is
+completed the Lock Record must be deleted and the Read Lock semaphore
+decremented by one.  The incrementation of the Read Lock and Lock Record
+creation or the decrementation of the Read Lock and the Lock Record deletion
+must happen atomically.  A Read lock can not be aquired unless the Write mutex
+is in the state of 'free'.  If a Read lock can not be aquired, a client must
+not "spin lock" on the Instanace ID as it may be in the process of being
+removed.  Instead, the "spin lock" should be re-resolving the external
+identifier to a Storage ID, and the Storage ID to Storage Instances.
+
+    - Write lock
+In order to obtain a Write lock on a Storage Object Instance the Write Lock
+mutex must be in the state of 'free' and the Read lock semaphore must have a
+value of zero.  A client should either "spin lock" or fail if all the Lock
+mutex is set or the Read Semaphore has a non-zero value.  A Lock Record must
+be created atomically with the setting of the Write mutex.  The setting of the
+Write mutex and Lock Record insertion or the clearing of the Write mutex and
+the Lock Record deletion must happen atomically.  If the Write lock has a
+state of 'free' and the Read lock has a non-zero value, the Write lock may be
+set to the 'wait' state to prevent new Read locks from being aquired.
+
+Storage Object Operations
+--
+    - Add
+The resource must already be on disk (having been named through some sort of
+mechanism to generate new filenames).  A new Storage Object must be created with a new/unqiue  Storage ID and has
+an associated with an External Identifier.  At least one Instance must be
+created atomicaly at the same time.  Each new Instance must be initially created with a Write lock.
+The resource may then be written to it's storage location.  When the writing
+of the resource is completed the Write lock must be cleared as early as is
+feasible.  The association of an External Identifier must wait until Instance
+creation is completed
+
+    - Delete
+In order to remove a Storage Object, a Write lock must be obtained on
+/all/ associated Instances.  Then the resources are removed.  As each resource
+is deleted the corresponding Instance must be deleted.  After all Instances are
+removed the Storage Object must then be deleted.
+
+    - Update
+Storage Object Instances are not truly written to.  Instead, a new set of
+Storage Object Instances and a new Storage Object are created in parallel.
+
+Write locks must be aquired on all the orignal Intances of a Storage Object.
+Then an Add operation is performed to create a new Storage Object and
+associated Instances.  Then the External Identifier must be atomically moved
+from the original Storage Object to the new Storage Object.  After the
+External Identifier migration, the original Storage Object and it's Instances
+should be removed with a special case of the Delete operation (one that will
+operate on already held locks).  If the old Storage Object is merely
+abandoned, ie. it still exists but doesn't have an External Identifier, it and
+the associated Instances will be removed during the next Consistency Sweep.
+If a Delete is not peformed, the Write locks on the orignal Instances must be
+released. [use a dead Storage Object table or flag, similar to mark and sweep
+garbage collection?]
+
+    - Move
+This is the same operation as an Update except the new Instances are identical
+to the old.
+
+    - Open
+Sets a Read lock on an Instances of a Storage Object and determines it's
+storage location.  A Read lock must only be placed on one Instance of a
+Storage Object.  All Instances of a Storage Object should be iterated through
+in a "spin lock" until a Read lock is aquired.
+
+    - Close
+Releases a Read lock aquired with open.
+
+The lock must be released as soon as is feasible.
+
+Lock sweeping
+--
+In the event that a Storage Object operation fails to complete successfully
+stale locks will have to be identified and removed from the IPP Pixel Data
+Server Database.  This should be done periodically by comparing the entries
+in the Lock table to the list of active nodes maintained by the IPP
+Controller.  It should also happen as soon as possible after a node goes
+offline (triggered by the IPP Controller marking a node as offline?).  A sweep
+must be /completed/ before an offline node can be marked on-line.
+
+Once a node is determined to be offline all entries in the Lock table set by
+that node should be identified.   The locks on the Storage Object
+Instances pointed to by those entries should then be rolled back and Lock
+Record entries themselves must be removed from the lock table.
+
+Consistency sweeping
+--
+Periodically the IPP Pixel Data Server meta-data and Storage Object will need
+to be checked for sanity.  This would be similar to running fsck on a modern
+filesystem.  Consistency sweeping should include Lock sweeping and should be
+considered a superset.
+
+All Storage Objects must have at least once associated Instance.
+
+All Storage Object Instances must be associated with a Storage Object.
+
+All Storage Object Instances with a lock set must have the correct number of
+corresponding Lock Records.
+
+All Lock Records must be plausible, ie. the Instance pointed at must be in a
+corresponding state.
+
+Storage ID Allocation
+--
+Storage IDs are a signed 64-bit integer.  Negative values are considered
+reserved for future use.  Allocation should be incremental and, as we are
+unlikely to exceed 2^63 Storage Objects in the life time of the project, never
+reused.
+
+Allocated via [SQL auto-increment/stored procedure/app server?]
+
+Instance ID Allocation
+--
+Indentical to a Storage ID but exists in a unique namespace.
+
+Storage Location determination
+--
+???
Index: /branches/jhoblitt/Nebulous-Server/docs/requirements.txt
===================================================================
--- /branches/jhoblitt/Nebulous-Server/docs/requirements.txt	(revision 2784)
+++ /branches/jhoblitt/Nebulous-Server/docs/requirements.txt	(revision 2784)
@@ -0,0 +1,23 @@
+# Copyright (C) 2004  Joshua Hoblitt
+#
+# $Id: requirements.txt,v 1.1.1.1 2004-12-22 02:16:23 jhoblitt Exp $
+
+- store all image data aquired during the first two years of PS1's
+  operations.  Approximately 10^8 files (OTA level).
+
+- injest new data from the summit at the same rate it is produced when
+  operating at a cadence of 40s between integrations.  Approximately 400Mb/s.
+
+- support phase 1 & 2 processing. x files per t_min
+
+- support phase 3 & 4 processing. x files per t_min
+
+- support accessing small blocks of a file (ie., seek)
+
+what are the science drivers for these 3?
+--
+- data corruption is guarenteed to be detectable.
+
+- relibably store xx.x% of files 
+
+- xx.x% of stored images must be retreivable in xs.
Index: /branches/jhoblitt/Nebulous-Server/docs/setup.txt
===================================================================
--- /branches/jhoblitt/Nebulous-Server/docs/setup.txt	(revision 2784)
+++ /branches/jhoblitt/Nebulous-Server/docs/setup.txt	(revision 2784)
@@ -0,0 +1,33 @@
+
+- building and install the Perl modules (see: README)
+
+- create and initialize the database (see: docs/database_setup.txt)
+
+- setup the server
+
+As a CGI, work with either Apache 1.3.x or 2.0.x
+
+cp scripts/imagesever.pl /var/www/localhost/cgi-bin
+chmod 755 /var/www/localhost/cgi-bin/imageserver.pl
+touch /tmp/imageserver.log
+chmod 1777 /tmp/imageserver.log
+
+- test the imageserver.pl is working
+
+telnet localhost 80
+Trying 127.0.0.1...
+Connected to localhost.
+Escape character is '^]'.
+GET /cgi-bin/imageserver.pl HTTP/1.0
+
+HTTP/1.1 411 Length Required
+Date: Tue, 28 Sep 2004 21:15:58 GMT
+Server: Apache/2.0.49 (Gentoo/Linux)
+Content-Length: 0
+Connection: close
+Content-Type: text/plain; charset=ISO-8859-1
+
+Connection closed by foreign host.
+
+- if you get an HTTP 5xx code check your apache error_log
+
Index: /branches/jhoblitt/Nebulous-Server/docs/tmp.txt
===================================================================
--- /branches/jhoblitt/Nebulous-Server/docs/tmp.txt	(revision 2784)
+++ /branches/jhoblitt/Nebulous-Server/docs/tmp.txt	(revision 2784)
@@ -0,0 +1,44 @@
+client
+--
+new
+
+new_file
+                new_file
+                open_file
+
+find_file
+    find_instance
+
+open_file
+    find_file
+
+delete_file
+                delete_file
+    nuke
+
+copy_file
+    open_file
+    new_file
+
+move_file
+    copy_file
+    delete_file
+
+replicate_file
+    open_file
+                replicate_file
+
+cull_file
+    find_instance
+    delete_instance
+
+delete_instance
+    nuke
+                delete_instance
+
+find_instance
+                find_instance
+
+# missing
+file_stat
+lock ?
Index: /branches/jhoblitt/Nebulous-Server/lib/Nebulous/Server.pm
===================================================================
--- /branches/jhoblitt/Nebulous-Server/lib/Nebulous/Server.pm	(revision 2784)
+++ /branches/jhoblitt/Nebulous-Server/lib/Nebulous/Server.pm	(revision 2784)
@@ -0,0 +1,660 @@
+# Copyright (c) 2004  Joshua Hoblitt
+#
+# $Id: Server.pm,v 1.1.1.1 2004-12-22 02:16:24 jhoblitt Exp $
+
+package PS::IPP::IData::Server;
+
+use strict;
+use warnings;
+no warnings qw( uninitialized );
+
+our $VERSION = '0.01';
+
+use DBI;
+use PS::IPP::IData::Util qw( :standard );
+use PS::IPP::IData::Server::Log;
+use PS::IPP::IData::Server::SQL;
+use Log::Log4perl;
+use Params::Validate qw( validate_pos SCALAR );
+
+my %class = (
+    raw => 1,
+);
+
+my $log;
+my $sql;
+my $db;
+
+sub setup {
+    my $self = shift;
+
+    my ( $dsn, $user, $passwd ) = validate_pos( @_,
+        {
+            type        => SCALAR,
+        },
+        {
+            type        => SCALAR,
+        },
+        {
+            type        => SCALAR,
+        },
+    );
+
+    $log = Log::Log4perl::get_logger( "PS::IPP::IData::Server" );
+    $sql = PS::IPP::IData::Server::SQL->new;
+
+    $log->debug( "entered - @_" );
+
+    eval {
+        $db = DBI->connect(
+            $dsn,
+            $user,
+            $passwd,
+            {
+                RaiseError => 1,
+                PrintError => 0,
+                AutoCommit => 0,
+            },
+        );
+
+        $db->do( $sql->set_transaction_model );
+
+        $db->commit;
+    };
+    if ( $@ ) {
+        $db->rollback if $db;
+        $log->logdie( "database error: $@" );
+    }
+
+    $log->debug( "connected to database: ", sub { $db->data_sources; } );
+
+    $log->debug( "leaving" );
+
+    return 1;
+}
+
+sub create_object {
+    my $self = shift;
+
+    $log->debug( "entered - @_" );
+
+    my ( $key, $class, $volume, $comment ) = validate_pos( @_,
+        {
+            type        => SCALAR,
+        },
+        {
+            type        => SCALAR,
+            callbacks   => {
+                'is + integer' => sub { $_[0] =~ /^\d+$/ },
+            },
+            default     => 0,
+        },
+        {
+            type        => SCALAR,
+            optional    => 1,
+        },
+        {
+            type        => SCALAR,
+            optional    => 1,
+        },
+    );
+
+    # TODO do some intelligent volume allocated 
+    # add hooks to make this decision based on avaiable space
+    $volume = _get_storage_volume( $volume );
+
+    # check that the comment isn't too long
+    # check that the class requested is valid
+    # check that the volume requested is valid
+
+    my $uri;
+    my $ins_id;
+
+    eval {
+        {
+            # create storage_object
+            my $query = $db->prepare( $sql->new_object ); 
+            $query->execute( $key, $class, $comment );
+        }
+
+        {
+            # create instance with no URI
+            my $query = $db->prepare( $sql->new_object_instance );
+            $query->execute;
+        }
+
+        {
+            # get instance ID
+            my $query = $db->prepare( $sql->last_insert_id );
+            $query->execute;
+            ( $ins_id ) = $query->fetchrow_array;
+        }
+    };
+    if ( $@ ) {
+        $db->rollback;
+        $log->logdie( "database error: $@" );
+    }
+
+    $uri = "$volume/$key.$ins_id";
+
+    $log->debug( "generated uri $uri");
+
+    my $path = _get_file_path( $uri );
+
+    # TODO add some stuff here to retry if unsucessful
+    eval {
+        _create_empty_file( $path );
+    };
+    if ( $@ ) {
+        $db->rollback;
+        $log->logdie( $@ );
+    }
+
+    eval {
+        {
+            my $query = $db->prepare( $sql->update_instance_uri );
+            $query->execute( $uri, $ins_id );
+        }
+
+        $db->commit;
+    };
+    $log->logdie( "database error: $@" ) if $@;
+
+    $log->debug( "leaving" );
+
+    return $uri;
+}
+
+sub replicate_object {
+    my $self = shift;
+
+    $log->debug( "entered - @_" );
+
+    my ( $key, $volume ) = validate_pos( @_,
+        {
+            type        => SCALAR,
+        },
+        {
+            type        => SCALAR,
+            optional    => 1,
+        },
+    );
+
+    # TODO
+    $volume = _get_storage_volume();
+
+    my $path;
+    my $uri;
+
+    my $so_id;
+    my $ins_id;
+    eval {
+        {
+            my $query = $db->prepare( $sql->get_object_instances );
+            my $rows = $query->execute( $key );
+
+            unless ( $rows > 0 ) {
+                $db->rollback;
+                $log->logdie( "storage object does not exist" );
+            }
+
+            $so_id = $query->fetchrow_hashref->{ 'so_id' };
+        }
+
+        {
+            my $query = $db->prepare( $sql->new_instance );
+            $query->execute( $so_id );
+        }
+
+        {
+            my $query = $db->prepare( $sql->last_insert_id );
+            $query->execute();
+
+            ( $ins_id ) = $query->fetchrow_array;
+        }
+
+        {
+            $uri = "$volume/$key.$ins_id";
+            $log->debug( "generated uri $uri");
+
+            my $query = $db->prepare( $sql->update_instance_uri );
+            $query->execute( $uri, $ins_id );
+        }
+    };
+    if ( $@ ) {
+        $db->rollback;
+        $log->logdie( "database error: $@" );
+    }
+
+
+    eval {
+        $path = _get_file_path( $uri );
+        $path = _create_empty_file( $path );
+    };
+    if ( $@ ) {
+        $db->rollback;
+        $log->logdie( $@ );
+    }
+
+    $db->commit;
+
+    $log->debug( "leaving" );
+
+    return $uri;
+}
+
+sub lock_object {
+    my $self = shift;
+
+    $log->debug( "entered - @_" );
+
+    my ( $key, $type ) = validate_pos( @_,
+        {
+            type        => SCALAR,
+        },
+        {
+            type        => SCALAR,
+            callbacks   => {
+                'is read or write' => sub { $_[0] =~ /^(?:read|write)$/ },
+            },
+        },
+    );
+
+    my $so_id;
+    my $read_lock;
+    my $write_lock;
+
+    eval {
+        # this will set update locks
+        my $query = $db->prepare( $sql->get_object_locks );
+        my $rows = $query->execute( $key );
+        unless ( $rows == 1 ) {
+            $log->logdie( "storage object does not exist" );
+        }
+
+        my $row = $query->fetchrow_hashref;
+
+        $so_id      = $row->{ 'so_id' };
+        $read_lock  = $row->{ 'read_lock' };
+        $write_lock = $row->{ 'write_lock' };
+    };
+    if ( $@ ) {
+        $db->rollback;
+        $log->logdie( "database error: $@" );
+    }
+
+    if ( $type eq 'write' ) {
+        # can't set a write lock twice and
+        # can't set a write lock if there are read locks
+        if ( $write_lock ) {
+            eval { $db->rollback; };
+            $log->logdie( "database error: $@" ) if $@;
+
+            $log->logdie( "can not write lock twice" );
+        }
+            
+        if ( $read_lock > 0 ) {
+            eval { $db->rollback; };
+            $log->logdie( "database error: $@" ) if $@;
+
+            $log->logdie( "can not write lock after read lock" );
+        }
+
+        eval {
+            my $query = $db->prepare( $sql->set_write_lock );
+            my $rows = $query->execute( $key );
+        
+            # if we affected more then one row something very bad has happened.
+            unless ( $rows == 1 ) {
+                $log->logdie( "affected row count is $rows instead of 1" );
+            }
+
+            $db->commit;
+        };
+        if ( $@ ) {
+            $db->rollback;
+            $log->logdie( "database error: $@" );
+        }
+
+        $log->debug( "leaving" );
+
+        return 1;
+    }
+
+    if ( $type eq 'read' ) {
+        # can't set a read lock if there's a write lock
+        if ( $write_lock ) {
+            eval { $db->rollback; };
+            $log->logdie( "database error: $@" ) if $@;
+
+            $log->logdie( "can not read lock after write lock" );
+        }
+
+        eval {
+            my $query = $db->prepare( $sql->increment_read_lock );
+            my $rows = $query->execute( $key );
+        
+            # if we affected more then one row something very bad has happened.
+            unless ( $rows == 1 ) {
+                $log->logdie( "affected row count is $rows instead of 1" );
+            }
+
+            $db->commit;
+        };
+        if ( $@ ) {
+            $db->rollback;
+            $log->logdie( "database error: $@" );
+        }
+
+        $log->debug( "leaving" );
+
+        return 1;
+    }
+
+    # never reached
+    $log->logdie( "this should never happen" );
+}
+
+sub unlock_object {
+    my $self = shift;
+
+    $log->debug( "entered - @_" );
+
+    my ( $key, $type ) = validate_pos( @_,
+        {
+            type        => SCALAR,
+        },
+        {
+            type        => SCALAR,
+            callbacks   => {
+                'is read or write' => sub { $_[0] =~ /^(?:read|write)$/ },
+            },
+        },
+    );
+
+    my $so_id;
+    my $read_lock;
+    my $write_lock;
+
+    eval {
+        # this will set update locks
+        my $query = $db->prepare( $sql->get_object_locks );
+        my $rows = $query->execute( $key );
+        unless ( $rows == 1 ) {
+            $log->logdie( "storage object does not exist" );
+        }
+
+        my $row = $query->fetchrow_hashref;
+
+        $so_id      = $row->{ 'so_id' };
+        $read_lock  = $row->{ 'read_lock' };
+        $write_lock = $row->{ 'write_lock' };
+    };
+    if ( $@ ) {
+        $db->rollback;
+        $log->logdie( "database error: $@" );
+    }
+
+    if ( $type eq 'write' ) {
+        # can't remove a write lock if it doesn't exist
+        if ( $read_lock ) {
+            eval { $db->rollback; };
+            $log->logdie( "database error: $@" ) if $@;
+           
+            $log->logdie( "can not have a write lock under a read lock" );
+        }
+        unless ( $write_lock ) {
+            eval { $db->rollback; };
+            $log->logdie( "database error: $@" ) if $@;
+
+            $log->logdie( "can not remove non-existant write lock" );
+        }
+
+        eval {
+            my $query = $db->prepare( $sql->delete_write_lock );
+            my $rows = $query->execute( $key );
+        
+            # if we affected more then one row something very bad has happened.
+            unless ( $rows == 1 ) {
+                $log->logdie( "affected row count is $rows instead of 1" );
+            }
+
+            $db->commit;
+        };
+        if ( $@ ) {
+            $db->rollback;
+            $log->logdie( "database error: $@" );
+        }
+
+        $log->debug( "leaving" );
+
+        return 1;
+    }
+
+    if ( $type eq 'read' ) {
+        # can't remove a read lock if there's a write lock and
+        # can't remove a read lock if there aren't any
+        if ( $write_lock ) {
+            eval { $db->rollback; };
+            $log->logdie( "database error: $@" ) if $@;
+           
+            $log->logdie( "can not have a read lock under a write lock" );
+        }
+           
+        if ( $read_lock == 0 ) {
+            eval { $db->rollback; };
+            $log->logdie( "database error: $@" ) if $@;
+           
+            $log->logdie( "can not remove non-existant read lock" );
+        }
+
+        eval {
+            my $query = $db->prepare( $sql->decrement_read_lock );
+            my $rows = $query->execute( $key );
+        
+            # if we affected more then one row something very bad has happened.
+            unless ( $rows == 1 ) {
+
+                $log->logdie( "affected row count is $rows instead of 1" );
+            }
+
+            $db->commit;
+        };
+        if ( $@ ) {
+            $db->rollback;
+            $log->logdie( "database error: $@" );
+        }
+
+        $log->debug( "leaving" );
+
+        return 1;
+    }
+
+    # never reached
+    $log->logdie( "this should never happen" );
+}
+
+
+sub find_instances {
+    my $self = shift;
+
+    $log->debug( "entered - @_" );
+
+    my ( $key, $volume ) = validate_pos( @_,
+        {
+            type        => SCALAR,
+        },
+        {
+            type        => SCALAR,
+            optional    => 1,
+        },
+    );
+
+    # TODO restrict search to a volume
+
+    my $query;
+
+    eval {
+        $query = $db->prepare( $sql->get_object_instances );
+        $query->execute( $key );
+    };
+    $log->logdie( "database error: $@" ) if $@;
+
+    my @locations;
+
+    while ( my $row = $query->fetchrow_hashref ) {
+        my $instance = $row->{ 'uri' };
+        push @locations, $instance if $instance;
+    }
+
+    $log->logdie( "no instances found" ) unless ( scalar @locations );
+
+    $log->debug( "leaving" );
+
+    return \@locations;
+}
+
+sub delete_instance {
+    my $self = shift;
+
+    $log->debug( "entered - @_" );
+
+    my ( $uri ) = validate_pos( @_,
+        {
+            type => SCALAR,
+        },
+    );
+
+    eval {
+        my $so_id;
+        my $instances;
+        # get so_id
+        {
+            my $query = $db->prepare( $sql->get_object_from_uri );
+            my $rows = $query->execute( $uri );
+
+            unless ( $rows > 0 ) {
+                $log->logdie( "no instance is associated with uri" );
+            }
+
+            $so_id = $query->fetchrow_hashref->{ 'so_id' };
+        }
+
+        {
+            my $query = $db->prepare( $sql->get_instance_count );
+            $query->execute( $so_id );
+
+            $instances = $query->fetchrow_hashref->{ 'count(ins_id)' };
+        }
+
+        # remove instance
+        {
+            my $query = $db->prepare( $sql->delete_instance );
+            my $rows = $query->execute( $uri );
+            
+            # if we affected something other then one row something very bad has happened
+            unless ( $rows == 1 ) {
+                $log->logdie( "affected row count is $rows instead of 1" );
+            }
+        }
+
+        # if we just deleted the last instance associated with a storage object
+        # remove it too
+        if ( $instances == 1 ) {
+            # we just removed the last instance
+            my $query = $db->prepare( $sql->delete_object );
+            my $rows = $query->execute( $so_id );
+
+            # if we affected something other then one row something very bad has happened
+            unless ( $rows == 1 ) {
+                $log->logdie( "affected row count is $rows instead of 1" );
+            }
+        }
+
+        $db->commit;
+    };
+    if ( $@ ) {
+        $db->rollback;
+        $log->logdie( "database error: $@" );
+    }
+
+    $log->debug( "leaving" );
+
+    return 1;
+}
+
+sub stat_object {
+    my $self = shift;
+
+    $log->debug( "entered - @_" );
+
+    my ( $key ) = validate_pos( @_,
+        {
+            type => SCALAR,
+        },
+    );
+
+    my $stat;
+    eval {
+        my $query = $db->prepare( $sql->get_object );
+        my $rows = $query->execute( $key );
+
+        unless ( $rows == 1 ) {
+            $log->logdie( "no storage object found" );
+        }
+
+        $stat = $query->fetchrow_arrayref;
+
+        $db->commit;
+    };
+    if ( $@ ) {
+        $db->rollback;
+        $log->logdie( "database error: $@" );
+    }
+
+    $log->debug( "leaving" );
+
+    return $stat;
+}
+
+sub _get_storage_volume {
+    $log->debug( "entered - @_" );
+
+    my $name = shift;
+
+    my $volume;
+    {
+        my $query;
+        eval {
+            # TODO cache this?
+            my $rows;
+            if ( $name ) {
+                $query = $db->prepare( qq{ SELECT uri FROM volume WHERE name = ? ORDER BY rand() LIMIT 1 } );
+                $rows = $query->execute( $name );
+            } else {
+                $query = $db->prepare( qq{ SELECT uri FROM volume ORDER BY rand() LIMIT 1 } );
+                $rows = $query->execute;
+            }
+
+            # there has to be atleast one storage volume
+            unless ( $rows > 0 ) {
+                $db->rollback;
+
+                $log->logdie( "affected row count is $rows instead of 1" );
+            } else {
+                $db->commit;
+            }
+        };
+        $log->logdie( "database error: $@" ) if $@;
+
+        $volume = $query->fetchrow_hashref->{ 'uri' };
+    }
+
+    $log->debug( "leaving" );
+
+    return $volume;
+}
+
+1;
+
+__END__
Index: /branches/jhoblitt/Nebulous-Server/lib/Nebulous/Server/Apache.pm
===================================================================
--- /branches/jhoblitt/Nebulous-Server/lib/Nebulous/Server/Apache.pm	(revision 2784)
+++ /branches/jhoblitt/Nebulous-Server/lib/Nebulous/Server/Apache.pm	(revision 2784)
@@ -0,0 +1,21 @@
+# Copyright (C) 2004  Joshua Hoblitt
+#
+# $Id
+
+package PS::IPP::IData::Server::Apache;
+
+use strict;
+
+our $VERSION = 0.01;
+
+use SOAP::Transport::HTTP;
+
+my $server = SOAP::Transport::HTTP::Apache
+    ->dispatch_to( 'PS::IPP::IData::Server' )
+    ->options({ compress_threshold => 10 * 1024 });
+
+sub handler { $server->handler(@_); }
+
+1;
+
+__END__
Index: /branches/jhoblitt/Nebulous-Server/lib/Nebulous/Server/Log.pm
===================================================================
--- /branches/jhoblitt/Nebulous-Server/lib/Nebulous/Server/Log.pm	(revision 2784)
+++ /branches/jhoblitt/Nebulous-Server/lib/Nebulous/Server/Log.pm	(revision 2784)
@@ -0,0 +1,27 @@
+# Copyright (c) 2004  Joshua Hoblitt
+#
+# $Id: Log.pm,v 1.1.1.1 2004-12-22 02:16:24 jhoblitt Exp $
+
+package PS::IPP::IData::Server::Log;
+
+use strict;
+
+our $VERSION = '0.01';
+
+use Log::Log4perl;
+                                                                                                                                          
+my $conf = q(
+    log4perl.category.PS.IPP.IData.Server = DEBUG, SERVERLOGFILE
+
+    log4perl.appender.SERVERLOGFILE           = Log::Log4perl::Appender::File
+    log4perl.appender.SERVERLOGFILE.filename  = /tmp/idata_server.log
+    log4perl.appender.SERVERLOGFILE.mode      = append
+    log4perl.appender.SERVERLOGFILE.layout    = Log::Log4perl::Layout::PatternLayout
+    log4perl.appender.SERVERLOGFILE.layout.ConversionPattern = %d{yyyy-MM-dd HH:mm:ss} | %H | %p | %M - %m%n
+);
+                                                                                                                          
+Log::Log4perl::init( \$conf );
+
+1;
+
+__END__
Index: /branches/jhoblitt/Nebulous-Server/lib/Nebulous/Server/SQL.pm
===================================================================
--- /branches/jhoblitt/Nebulous-Server/lib/Nebulous/Server/SQL.pm	(revision 2784)
+++ /branches/jhoblitt/Nebulous-Server/lib/Nebulous/Server/SQL.pm	(revision 2784)
@@ -0,0 +1,107 @@
+# Copyright (c) 2004  Joshua Hoblitt
+#
+# $Id: SQL.pm,v 1.1.1.1 2004-12-22 02:16:24 jhoblitt Exp $
+
+package PS::IPP::IData::Server::SQL;
+
+use strict;
+
+our $VERSION = '0.01';
+
+use base qw( Class::Accessor::Fast );
+
+my %sql = (
+    set_transaction_model   => qq{
+        SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED
+    },
+    update_instance_uri => qq{
+        UPDATE instance SET uri = ? WHERE ins_id = ?
+    },
+    last_insert_id      => qq{
+        SELECT LAST_INSERT_ID()
+    },
+    new_object          => qq{
+        INSERT INTO storage_object
+        VALUES (NULL, ?, ?, ?, 0, NULL, NULL)
+    },
+    delete_object       => qq{
+        DELETE FROM storage_object
+        WHERE so_id = ?
+    },
+    new_object_instance  => qq{
+        INSERT INTO instance 
+        VALUES (NULL, LAST_INSERT_ID(), 'error', NULL, NULL, NULL, NULL)
+    },
+    new_instance        => qq{
+        INSERT INTO instance
+        VALUES (NULL, ?, 'error', NULL,NULL, NULL, NULL)
+    },
+    get_object          => qq{
+        SELECT *
+        FROM storage_object
+        WHERE ext_id = ?
+    },
+    get_object_locks    => qq{
+        SELECT so_id, read_lock, write_lock 
+        FROM storage_object
+        WHERE ext_id = ?
+        FOR UPDATE
+    },
+    set_write_lock      => qq{
+        UPDATE storage_object
+        SET write_lock = 'write'
+        WHERE ext_id = ?
+    },
+    delete_write_lock   => qq{
+        UPDATE storage_object
+        SET write_lock = NULL
+        WHERE ext_id = ?
+    },
+    increment_read_lock => qq{
+        UPDATE storage_object
+        SET read_lock = read_lock+1
+        WHERE ext_id = ?
+    },
+    decrement_read_lock => qq{
+        UPDATE storage_object
+        SET read_lock = read_lock-1
+        WHERE ext_id = ?
+    },
+    delete_instance     => qq{
+        DELETE FROM instance
+        WHERE uri = ?
+    },
+    get_object_from_uri   => qq{
+        SELECT so_id
+        FROM instance
+        WHERE uri = ?
+    },
+    get_instance_count   => qq{
+        SELECT count(ins_id)
+        FROM instance
+        WHERE so_id = ?
+    },
+    get_object_instances    => qq{
+        SELECT storage_object.so_id, uri, assigned_location
+        FROM storage_object
+        LEFT JOIN instance
+        USING (so_id)
+        WHERE storage_object.ext_id = ?
+    },
+);
+
+__PACKAGE__->mk_ro_accessors( keys %sql );
+
+sub new {
+    my $class = shift;
+
+    my $self = \%sql;
+
+    bless $self, $class || ref $class;
+
+    return $self;
+}
+
+1;
+
+__END__
Index: /branches/jhoblitt/Nebulous-Server/nebulous.wsdl
===================================================================
--- /branches/jhoblitt/Nebulous-Server/nebulous.wsdl	(revision 2784)
+++ /branches/jhoblitt/Nebulous-Server/nebulous.wsdl	(revision 2784)
@@ -0,0 +1,96 @@
+<?xml version="1.0"?>
+
+<definitions name="idata" targetNamespace="http://pan-starrs.ifa.hawaii.edu/PS/IPP/IData/Server">
+
+    <types>
+        <schema targetNamespace="http://pan-starrs.ifa.hawaii.edu/PS/IPP/IData/Server"
+            xmlns="http://www.w3.org/1999/XMLSchema">
+
+            <element name="create_object">
+                <complexType>
+                    <sequence>
+                        <element name="key"     type="string"/>
+                        <element name="class"   type="int"/>
+                        <element name="volume"  type="string"/>
+                        <element name="comment" type="string"/>
+                    </sequence>
+                </complexType>
+            </element>
+
+            <element name="create_objectResponse">
+                <complexType>
+                    <all>
+                        <element name="uri" type="string"/>
+                    </all>
+                </complexType>
+            </element>
+
+            <element name="create_objectFault">
+                <complexType>
+                    <all>
+                        <element name="errorMessage" type="string"/>
+                    </all>
+                </complexType>
+            </element>
+
+        </schema>
+    </types>
+
+    <message name="create_objectRequest">
+        <part name="body" element="esxsd:create_object"/>
+    </message>
+
+    <message name="create_objectResponse">
+        <part name="body" element="esxsd:create_objectResponse"/>
+    </message>
+
+    <portType name="create_objectPortType">
+        <operation name="create_object">
+            <input message ="es:create_objectRequest"/>
+            <output message="es:create_objectResponse"/>
+            <fault message ="es:create_objectFault"/>
+        </operation>
+    </portType>
+
+    <binding name="idataSoapBinding"
+        type="es:GetidatarPortType">
+
+        <soap:binding style="document"
+            transport="http://schemas.xmlsoap.org/soap/http"/>
+
+        <operation name="Getidata">
+            <soap:operation
+                soapAction="http://www.snowboard-info.com/EndorsementSearch"/>
+            <input>
+                <soap:body use="literal"
+                    namespace="http://schemas.snowboard-info.com/EndorsementSearch.xsd"/>
+            </input>
+
+            <output>
+                <soap:body use="literal"
+                    namespace="http://schemas.snowboard-info.com/EndorsementSearch.xsd"/>
+            </output>
+
+            <fault>
+                <soap:body use="literal"
+                    namespace="http://schemas.snowboard-info.com/EndorsementSearch.xsd"/>
+            </fault>
+        </operation>
+    </binding>
+
+
+  <service name="EndorsementSearchService">
+
+    <documentation>snowboarding-info.com Endorsement Service</documentation> 
+
+    <port name="GetEndorsingBoarderPort"
+          binding="es:EndorsementSearchSoapBinding">
+
+      <soap:address
+location="http://www.snowboard-info.com/EndorsementSearch"/>
+
+    </port>
+
+  </service>
+
+</definitions>
Index: /branches/jhoblitt/Nebulous-Server/scripts/bench_test.pl
===================================================================
--- /branches/jhoblitt/Nebulous-Server/scripts/bench_test.pl	(revision 2784)
+++ /branches/jhoblitt/Nebulous-Server/scripts/bench_test.pl	(revision 2784)
@@ -0,0 +1,28 @@
+#!/usr/bin/perl
+
+use strict;
+use warnings;
+
+use lib "./lib";
+use Benchmark qw( cmpthese );
+
+use PS::IPP::IData::Client;
+
+my $idata = PS::IPP::IData::Client->new(
+    proxy   => 'http://localhost:80/idata'
+);
+
+my $key = "foobarbazbongbungbutufu";
+cmpthese( 10, {
+        'Image Server' => sub {
+            {
+                my $fh = $idata->create( $key );
+                close $fh;
+                $idata->unlock( $key, 'write' );
+            }
+
+            $idata->delete( $key );
+        },
+    },
+    'all',
+);
Index: /branches/jhoblitt/Nebulous-Server/scripts/imageserver.pl
===================================================================
--- /branches/jhoblitt/Nebulous-Server/scripts/imageserver.pl	(revision 2784)
+++ /branches/jhoblitt/Nebulous-Server/scripts/imageserver.pl	(revision 2784)
@@ -0,0 +1,14 @@
+#!/usr/bin/perl -w
+
+# Copyright (C) 2004  Joshua Hoblitt
+#
+# $Id
+ 
+use strict;
+
+use SOAP::Transport::HTTP;
+use PS::IPP::IData::Server;
+   
+SOAP::Transport::HTTP::CGI
+    -> dispatch_to( 'PS::IPP::IData::Server' )
+    -> handle;
Index: /branches/jhoblitt/Nebulous-Server/scripts/ptest.pl
===================================================================
--- /branches/jhoblitt/Nebulous-Server/scripts/ptest.pl	(revision 2784)
+++ /branches/jhoblitt/Nebulous-Server/scripts/ptest.pl	(revision 2784)
@@ -0,0 +1,51 @@
+#!/usr/bin/perl
+
+use strict;
+use warnings;
+
+use lib "./lib";
+
+use PS::IPP::IData::Client;
+
+my $idata = PS::IPP::IData::Client->new(
+    uri     => 'http://localhost:80/PS/IPP/IData/Server',
+    proxy   => 'http://localhost:80/idata'
+);
+
+my $key = shift;
+
+my $kids = shift || 1;
+
+foreach my $id ( 1..$kids ) {
+    my $pid = fork;
+
+    unless ( $pid )  {
+        my $fname = "${key}_$id";
+        my $fh = $idata->create( $fname );
+        die "can't create file $fname" unless $fh;
+
+        print $fh "fooby\n";
+
+        close $fh;
+        $idata->unlock( $fname, 'write' );
+
+        $fh = $idata->open( $fname, 'read' ) or die "can't open file";
+        close $fh;
+        $idata->unlock( $fname, 'read' );
+
+        $idata->replicate( $fname );
+        $idata->cull( $fname );
+        $idata->find( $fname );
+        $idata->copy( $fname, $fname . "_copy" );
+        $idata->move( $fname, $fname . "_move" );
+        $idata->delete( $fname . "_copy" );
+        $idata->delete( $fname . "_move" );
+
+        exit 0;
+    }
+}
+
+while ( $kids ) {
+    wait();
+    $kids --;
+}
Index: /branches/jhoblitt/Nebulous-Server/scripts/stest.pl
===================================================================
--- /branches/jhoblitt/Nebulous-Server/scripts/stest.pl	(revision 2784)
+++ /branches/jhoblitt/Nebulous-Server/scripts/stest.pl	(revision 2784)
@@ -0,0 +1,33 @@
+#!/usr/bin/perl
+
+use strict;
+use warnings;
+
+use lib "./lib";
+
+use PS::IPP::IData::Client; # trace => 'debug';
+
+my $idata = PS::IPP::IData::Client->new(
+    proxy   => 'http://localhost:80/idata'
+);
+
+my $fname = shift;
+
+#my $fh = $idata->create( $fname );
+#$idata->unlock( $fname, 'write' );
+
+$idata->create( $fname, 1 );
+$idata->unlock( $fname, 'write' );
+$idata->delete( $fname );
+
+__END__
+
+{
+    my $fh = $pd->create( $key );
+    die "can't create file $fname" unless $fh;
+    print $fh "fooby\n";
+    close $fh;
+    #$idata->unlock( $fname, 'write' );
+    my $filesize =  (stat( $key ))[7];
+    print "created file $key, ${filesize}bytes\n";
+}
Index: /branches/jhoblitt/Nebulous-Server/t/01_load.t
===================================================================
--- /branches/jhoblitt/Nebulous-Server/t/01_load.t	(revision 2784)
+++ /branches/jhoblitt/Nebulous-Server/t/01_load.t	(revision 2784)
@@ -0,0 +1,19 @@
+# -*- perl -*-
+
+# t/001_load.t - check module loading and create testing directory
+
+use strict;
+use warnings;
+
+use lib qw( ./t ./lib );
+
+use Test::More tests => 8;
+
+BEGIN { use_ok( 'PS::IPP::IData::Client' ); }
+BEGIN { use_ok( 'PS::IPP::IData::Client::Log' ); }
+BEGIN { use_ok( 'PS::IPP::IData::Client::HTTP' ); }
+BEGIN { use_ok( 'PS::IPP::IData::Util' ); }
+BEGIN { use_ok( 'PS::IPP::IData::Server' ); }
+BEGIN { use_ok( 'PS::IPP::IData::Server::Apache' ); }
+BEGIN { use_ok( 'PS::IPP::IData::Server::SQL' ); }
+BEGIN { use_ok( 'PS::IPP::IData::Server::Log' ); }
Index: /branches/jhoblitt/Nebulous-Server/t/02_server_setup.t
===================================================================
--- /branches/jhoblitt/Nebulous-Server/t/02_server_setup.t	(revision 2784)
+++ /branches/jhoblitt/Nebulous-Server/t/02_server_setup.t	(revision 2784)
@@ -0,0 +1,35 @@
+#!/usr/bin/perl
+
+use strict;
+use warnings;
+
+use Test::More tests => 2;
+
+use lib qw( ./t ./lib );
+
+use PS::IPP::IData::Server;
+use Test::IData;
+
+Test::IData->setup;
+
+ok(
+    PS::IPP::IData::Server->setup(
+        "DBI:mysql:database=test:host=localhost",
+        "test",
+        "",
+    ),
+    "connect",
+);
+
+Test::IData->setup;
+
+eval {
+    PS::IPP::IData::Server->setup(
+        "DBI:mysql:database=foobar:host=localhost",
+        "baz",
+        "boo",
+    );
+};
+like( $@, qr/DBI connect.*? failed/, "bad dsn/user/pass" );
+
+Test::IData->cleanup;
Index: /branches/jhoblitt/Nebulous-Server/t/03_server_create_object.t
===================================================================
--- /branches/jhoblitt/Nebulous-Server/t/03_server_create_object.t	(revision 2784)
+++ /branches/jhoblitt/Nebulous-Server/t/03_server_create_object.t	(revision 2784)
@@ -0,0 +1,84 @@
+#!/usr/bin/perl
+
+use strict;
+use warnings;
+
+use Test::More tests => 11;
+
+use lib qw( ./t ./lib );
+
+use PS::IPP::IData::Server;
+use PS::IPP::IData::Util qw( :standard );
+use Test::IData;
+
+my $idata = "PS::IPP::IData::Server";
+
+$idata->setup(
+    "DBI:mysql:database=test:host=localhost",
+    "test",
+    "",
+);
+
+Test::IData->setup;
+
+{
+    # key
+    my $uri = $idata->create_object( "foo" );
+
+    ok( -e _get_file_path( $uri ), "file exists" );
+    like( $uri, qr/^file:\//, "valid URI" );
+}
+
+Test::IData->setup;
+
+{
+    # key, class
+    my $uri = $idata->create_object( "foo", 0 );
+
+    ok( -e _get_file_path( $uri ), "file exists" );
+    like( $uri, qr/^file:\//, "valid URI" );
+}
+
+Test::IData->setup;
+
+{
+    # key, class, volume
+    my $uri = $idata->create_object( "foo", 0, "node01" );
+
+    ok( -e _get_file_path( $uri ), "file exists" );
+    like( $uri, qr/^file:\//, "valid URI" );
+}
+
+Test::IData->setup;
+
+{
+    # key, class, volume, comment
+    my $uri = $idata->create_object( "foo", 0, "node01", "this is foo" );
+
+    ok( -e _get_file_path( $uri ), "file exists" );
+    like( $uri, qr/^file:\//, "valid URI" );
+}
+
+Test::IData->setup;
+
+eval {
+    $idata->create_object( "foo" );
+    $idata->create_object( "foo" );
+};
+like( $@, qr/Duplicate entry/, "object already exists" );
+
+Test::IData->setup;
+
+eval {
+    $idata->create_object();
+};
+like( $@, qr/2 - 4 were expected/, "no params" );
+
+Test::IData->setup;
+
+eval {
+    $idata->create_object( 1, 2, 3, 4, 5 );
+};
+like( $@, qr/2 - 4 were expected/, "too many params" );
+
+Test::IData->cleanup;
Index: /branches/jhoblitt/Nebulous-Server/t/04_server_replicate_object.t
===================================================================
--- /branches/jhoblitt/Nebulous-Server/t/04_server_replicate_object.t	(revision 2784)
+++ /branches/jhoblitt/Nebulous-Server/t/04_server_replicate_object.t	(revision 2784)
@@ -0,0 +1,79 @@
+#!/usr/bin/perl
+
+use strict;
+use warnings;
+
+use Test::More tests => 11;
+
+use lib qw( ./t ./lib );
+
+use PS::IPP::IData::Server;
+use PS::IPP::IData::Util qw( :standard );
+use Test::IData;
+
+my $idata = "PS::IPP::IData::Server";
+
+$idata->setup(
+    "DBI:mysql:database=test:host=localhost",
+    "test",
+    "",
+);
+
+Test::IData->setup;
+
+{
+    # key
+    $idata->create_object( "foo" );
+    my $uri = $idata->replicate_object( "foo" );
+
+    ok( -e _get_file_path( $uri ), "file exists" );
+    like( $uri, qr/^file:\//, "valid URI" );
+}
+
+Test::IData->setup;
+
+{
+    # key, $volume
+    $idata->create_object( "foo" );
+    my $uri = $idata->replicate_object( "foo", "node01" );
+
+    ok( -e _get_file_path( $uri ), "file exists" );
+    like( $uri, qr/^file:\//, "valid URI" );
+}
+
+Test::IData->setup;
+
+{
+    # key, $volume
+    $idata->create_object( "foo" );
+    my $uri1 = $idata->replicate_object( "foo", "node01" );
+    my $uri2 = $idata->replicate_object( "foo", "node02" );
+
+    ok( -e _get_file_path( $uri1 ), "file exists" );
+    ok( -e _get_file_path( $uri2 ), "file exists" );
+    like( $uri1, qr/^file:\//, "valid URI" );
+    like( $uri2, qr/^file:\//, "valid URI" );
+}
+
+Test::IData->setup;
+
+eval {
+    $idata->replicate_object( "foo" );
+};
+like( $@, qr/storage object does not exist/, "storage object does not exist" );
+
+Test::IData->setup;
+
+eval {
+    $idata->replicate_object();
+};
+like( $@, qr/1 - 2 were expected/, "no params" );
+
+Test::IData->setup;
+
+eval {
+    $idata->replicate_object( 1, 2, 3 );
+};
+like( $@, qr/1 - 2 were expected/, "too many params" );
+
+Test::IData->cleanup;
Index: /branches/jhoblitt/Nebulous-Server/t/05_server_lock_object.t
===================================================================
--- /branches/jhoblitt/Nebulous-Server/t/05_server_lock_object.t	(revision 2784)
+++ /branches/jhoblitt/Nebulous-Server/t/05_server_lock_object.t	(revision 2784)
@@ -0,0 +1,119 @@
+#!/usr/bin/perl
+
+use strict;
+use warnings;
+
+use Test::More tests => 13;
+
+use lib qw( ./t ./lib );
+
+use PS::IPP::IData::Server;
+use PS::IPP::IData::Util qw( :standard );
+use Test::IData;
+
+my $idata = "PS::IPP::IData::Server";
+
+$idata->setup(
+    "DBI:mysql:database=test:host=localhost",
+    "test",
+    "",
+);
+
+Test::IData->setup;
+
+{
+    $idata->create_object( "foo" );
+
+    ok( $idata->lock_object( "foo", "read" ), "read lock" );
+}
+
+Test::IData->setup;
+
+{
+    $idata->create_object( "foo" );
+
+    ok( $idata->lock_object( "foo", "read" ), "read lock" );
+    ok( $idata->lock_object( "foo", "read" ), "read lock" );
+}
+
+Test::IData->setup;
+
+{
+    $idata->create_object( "foo" );
+
+    ok( $idata->lock_object( "foo", "write" ), "write lock" );
+}
+
+Test::IData->setup;
+
+eval {
+    $idata->lock_object( "foo", "read" );
+};
+like( $@, qr/storage object does not exist/, "storage object does not exist" );
+
+Test::IData->setup;
+
+eval {
+    $idata->lock_object( "foo", "write" );
+};
+like( $@, qr/storage object does not exist/, "storage object does not exist" );
+
+Test::IData->setup;
+
+eval {
+    $idata->create_object( "foo" );
+
+    $idata->lock_object( "foo", "write" );
+    $idata->lock_object( "foo", "write" );
+};
+like( $@, qr/can not write lock twice/, "can not write lock twice" );
+
+Test::IData->setup;
+
+eval {
+    $idata->create_object( "foo" );
+
+    $idata->lock_object( "foo", "read" );
+    $idata->lock_object( "foo", "write" );
+};
+like( $@, qr/can not write lock after read lock/, "can not write lock after read lock" );
+
+Test::IData->setup;
+
+eval {
+    $idata->create_object( "foo" );
+
+    $idata->lock_object( "foo", "write" );
+    $idata->lock_object( "foo", "read" );
+};
+like( $@, qr/can not read lock after write lock/, "can not read lock after write lock" );
+
+Test::IData->setup;
+
+eval {
+    $idata->lock_object();
+};
+like( $@, qr/2 were expected/, "no params" );
+
+Test::IData->setup;
+
+eval {
+    $idata->lock_object( "foo" );
+};
+like( $@, qr/2 were expected/, "not enough params" );
+
+Test::IData->setup;
+
+eval {
+    $idata->lock_object( "foo", "both" );
+};
+like( $@, qr/is read or write/, "not read or write" );
+
+Test::IData->setup;
+
+eval {
+    $idata->lock_object( "foo", 'read', 3 );
+};
+like( $@, qr/2 were expected/, "too many params" );
+
+Test::IData->cleanup;
Index: /branches/jhoblitt/Nebulous-Server/t/06_server_unlock_object.t
===================================================================
--- /branches/jhoblitt/Nebulous-Server/t/06_server_unlock_object.t	(revision 2784)
+++ /branches/jhoblitt/Nebulous-Server/t/06_server_unlock_object.t	(revision 2784)
@@ -0,0 +1,134 @@
+#!/usr/bin/perl
+
+use strict;
+use warnings;
+
+use Test::More tests => 14;
+
+use lib qw( ./t ./lib );
+
+use PS::IPP::IData::Server;
+use PS::IPP::IData::Util qw( :standard );
+use Test::IData;
+
+my $idata = "PS::IPP::IData::Server";
+
+$idata->setup(
+    "DBI:mysql:database=test:host=localhost",
+    "test",
+    "",
+);
+
+Test::IData->setup;
+
+{
+    $idata->create_object( "foo" );
+
+    $idata->lock_object( "foo", "read" );
+
+    ok( $idata->unlock_object( "foo", "read" ), "read unlock" );
+}
+
+Test::IData->setup;
+
+{
+    $idata->create_object( "foo" );
+
+    $idata->lock_object( "foo", "read" );
+    $idata->lock_object( "foo", "read" );
+
+    ok( $idata->unlock_object( "foo", "read" ), "read unlock" );
+    ok( $idata->unlock_object( "foo", "read" ), "read unlock" );
+}
+
+Test::IData->setup;
+
+{
+    $idata->create_object( "foo" );
+
+    $idata->lock_object( "foo", "write" );
+
+    ok( $idata->unlock_object( "foo", "write" ), "write unlock" );
+}
+
+Test::IData->setup;
+
+eval {
+    $idata->unlock_object( "foo", "read" );
+};
+like( $@, qr/storage object does not exist/, "storage object does not exist" );
+
+Test::IData->setup;
+
+eval {
+    $idata->unlock_object( "foo", "write" );
+};
+like( $@, qr/storage object does not exist/, "storage object does not exist" );
+
+Test::IData->setup;
+
+eval {
+    $idata->create_object( "foo" );
+
+    $idata->unlock_object( "foo", "read" );
+};
+like( $@, qr/can not remove non-existant read lock/, "no lock set" );
+
+Test::IData->setup;
+
+eval {
+    $idata->create_object( "foo" );
+
+    $idata->unlock_object( "foo", "write" );
+};
+like( $@, qr/can not remove non-existant write lock/, "no lock set" );
+
+Test::IData->setup;
+
+eval {
+    $idata->create_object( "foo" );
+    $idata->lock_object( "foo", "write" );
+
+    $idata->unlock_object( "foo", "read" );
+};
+like( $@, qr/can not have a read lock under a write lock/, "read unlock under write lock" );
+
+Test::IData->setup;
+
+eval {
+    $idata->create_object( "foo" );
+    $idata->lock_object( "foo", "read" );
+
+    $idata->unlock_object( "foo", "write" );
+};
+like( $@, qr/can not have a write lock under a read lock/, "write unlock under read lock" );
+
+Test::IData->setup;
+
+eval {
+    $idata->unlock_object();
+};
+like( $@, qr/2 were expected/, "no params" );
+
+Test::IData->setup;
+
+eval {
+    $idata->unlock_object( "foo" );
+};
+like( $@, qr/2 were expected/, "not enough params" );
+
+Test::IData->setup;
+
+eval {
+    $idata->unlock_object( "foo", "both" );
+};
+like( $@, qr/is read or write/, "not read or write" );
+
+Test::IData->setup;
+
+eval {
+    $idata->unlock_object( "foo", 'read', 3 );
+};
+like( $@, qr/2 were expected/, "too many params" );
+
+Test::IData->cleanup;
Index: /branches/jhoblitt/Nebulous-Server/t/07_server_find_instances.t
===================================================================
--- /branches/jhoblitt/Nebulous-Server/t/07_server_find_instances.t	(revision 2784)
+++ /branches/jhoblitt/Nebulous-Server/t/07_server_find_instances.t	(revision 2784)
@@ -0,0 +1,90 @@
+#!/usr/bin/perl
+
+use strict;
+use warnings;
+
+use Test::More tests => 7;
+
+use lib qw( ./t ./lib );
+
+use PS::IPP::IData::Server;
+use PS::IPP::IData::Util qw( :standard );
+use Test::IData;
+
+my $idata = "PS::IPP::IData::Server";
+
+$idata->setup(
+    "DBI:mysql:database=test:host=localhost",
+    "test",
+    "",
+);
+
+Test::IData->setup;
+
+{
+    # key
+    my $uri = $idata->create_object( "foo" );
+
+    my $locations = $idata->find_instances( "foo" );
+
+    is( $uri, $locations->[0], "URIs match" );
+}
+
+Test::IData->setup;
+
+{
+    # key
+    my $uri1 = $idata->create_object( "foo" );
+    my $uri2 = $idata->replicate_object( "foo" );
+
+    my $locations = $idata->find_instances( "foo" );
+
+    ok( eq_set( [$uri1, $uri2], $locations ), "URIs match" );
+}
+
+Test::IData->setup;
+
+{
+    # key, volume
+    my $uri = $idata->create_object( "foo" );
+
+    my $locations = $idata->find_instances( "foo", "node01" );
+
+    is( $uri, $locations->[0], "URIs match" );
+}
+
+Test::IData->setup;
+
+{
+    # key
+    my $uri1 = $idata->create_object( "foo" );
+    my $uri2 = $idata->replicate_object( "foo" );
+
+    my $locations = $idata->find_instances( "foo", "node01" );
+
+    ok( eq_set( [$uri1, $uri2], $locations ), "URIs match" );
+}
+
+
+Test::IData->setup;
+
+eval {
+    $idata->find_instances( "foo" );
+};
+like( $@, qr/no instances found/, "storage object does not exist" );
+
+Test::IData->setup;
+
+eval {
+    $idata->find_instances();
+};
+like( $@, qr/1 - 2 were expected/, "no params" );
+
+Test::IData->setup;
+
+eval {
+    $idata->find_instances( "foo", 'read', 3 );
+};
+like( $@, qr/1 - 2 were expected/, "too many params" );
+
+Test::IData->cleanup;
Index: /branches/jhoblitt/Nebulous-Server/t/08_server_delete_instance.t
===================================================================
--- /branches/jhoblitt/Nebulous-Server/t/08_server_delete_instance.t	(revision 2784)
+++ /branches/jhoblitt/Nebulous-Server/t/08_server_delete_instance.t	(revision 2784)
@@ -0,0 +1,71 @@
+#!/usr/bin/perl
+
+use strict;
+use warnings;
+
+use Test::More tests => 8;
+
+use lib qw( ./t ./lib );
+
+use PS::IPP::IData::Server;
+use PS::IPP::IData::Util qw( :standard );
+use Test::IData;
+
+my $idata = "PS::IPP::IData::Server";
+
+$idata->setup(
+    "DBI:mysql:database=test:host=localhost",
+    "test",
+    "",
+);
+
+Test::IData->setup;
+
+{
+    my $uri = $idata->create_object( "foo" );
+
+    ok( $idata->delete_instance( $uri ), "delete instance" );
+}
+
+Test::IData->setup;
+
+{
+    my $uri1 = $idata->create_object( "foo" );
+    my $uri2 = $idata->replicate_object( "foo" );
+
+    ok( $idata->delete_instance( $uri1 ), "delete instance" );
+
+    my $locations = $idata->find_instances( "foo" );
+
+    is( $locations->[0], $uri2, "instance remains" );
+
+    ok( $idata->delete_instance( $uri2 ), "delete instance" );
+
+    eval {
+        $idata->find_instances( "foo" );
+    };
+    like( $@, qr/no instances found/, "storage object was deleted" );
+}
+
+Test::IData->setup;
+
+eval {
+    $idata->delete_instance( "file:/foo" );
+};
+like( $@, qr/no instance is associated with uri/, "uri does not exist" );
+
+Test::IData->setup;
+
+eval {
+    $idata->delete_instance();
+};
+like( $@, qr/1 was expected/, "no params" );
+
+Test::IData->setup;
+
+eval {
+    $idata->delete_instance( "foo", 2 );
+};
+like( $@, qr/1 was expected/, "too many params" );
+
+Test::IData->cleanup;
Index: /branches/jhoblitt/Nebulous-Server/t/09_server_stat_object.t
===================================================================
--- /branches/jhoblitt/Nebulous-Server/t/09_server_stat_object.t	(revision 2784)
+++ /branches/jhoblitt/Nebulous-Server/t/09_server_stat_object.t	(revision 2784)
@@ -0,0 +1,70 @@
+#!/usr/bin/perl
+
+use strict;
+use warnings;
+
+use Test::More tests => 12;
+
+use lib qw( ./t ./lib );
+
+use PS::IPP::IData::Server;
+use PS::IPP::IData::Util qw( :standard );
+use Test::IData;
+
+my $idata = "PS::IPP::IData::Server";
+
+$idata->setup(
+    "DBI:mysql:database=test:host=localhost",
+    "test",
+    "",
+);
+
+Test::IData->setup;
+
+{
+    my $uri = $idata->create_object( "foo" );
+
+    my $info = $idata->stat_object( "foo" );
+
+    is( scalar @$info, 7, "number of columns" );
+}
+
+Test::IData->setup;
+
+{
+    my $uri = $idata->create_object( "foo", 9, "node01", "foobar" );
+
+    my $info = $idata->stat_object( "foo" );
+
+    is( scalar @$info, 7,                       "number of columns" );
+    is( @$info[0], 1,                           "so_id" );
+    is( @$info[1], "foo",                       "ext_id" );
+    is( @$info[2], 9,                           "class_id" );
+    is( @$info[3], "foobar",                    "comment" );
+    is( @$info[4], 0,                           "read lock" );
+    is( @$info[5], undef,                       "write lock" );
+    like( @$info[6], qr/....-..-.. ..:..:../,   "epoch" );
+}
+
+Test::IData->setup;
+
+eval {
+    $idata->stat_object( "foo" );
+};
+like( $@, qr/no storage object found/, "object does not exist" );
+
+Test::IData->setup;
+
+eval {
+    $idata->stat_object();
+};
+like( $@, qr/1 was expected/, "no params" );
+
+Test::IData->setup;
+
+eval {
+    $idata->stat_object( "foo", 2 );
+};
+like( $@, qr/1 was expected/, "too many params" );
+
+Test::IData->cleanup;
Index: /branches/jhoblitt/Nebulous-Server/t/10_client_new.t
===================================================================
--- /branches/jhoblitt/Nebulous-Server/t/10_client_new.t	(revision 2784)
+++ /branches/jhoblitt/Nebulous-Server/t/10_client_new.t	(revision 2784)
@@ -0,0 +1,56 @@
+#!/usr/bin/perl
+
+use strict;
+use warnings;
+
+use Apache::Test qw( -withtestmore );
+
+plan tests => 4;
+
+use lib qw( ./t ./lib );
+
+use PS::IPP::IData::Client;
+use PS::IPP::IData::Util qw( :standard );
+use Test::IData;
+
+my $hostport = Apache::Test->config->{ 'hostport' };
+
+Test::IData->setup;
+
+{
+    my $idata = PS::IPP::IData::Client->new(
+        proxy => "http://$hostport/idata",
+    );
+
+    isa_ok( $idata, "PS::IPP::IData::Client" );
+}
+
+Test::IData->setup;
+
+{
+    my $idata = PS::IPP::IData::Client->new(
+        proxy => "http://$hostport/idata",
+        uri   => "http://example.com/PS/IPP/IData/Client",
+    );
+
+    isa_ok( $idata, "PS::IPP::IData::Client" );
+}
+
+Test::IData->setup;
+
+eval {
+    PS::IPP::IData::Client->new;
+};
+like( $@, qr/Mandatory parameter/, "no proxy" );
+
+Test::IData->setup;
+
+eval {
+    PS::IPP::IData::Client->new(
+        proxy => "foo",
+        dog => "do"
+    );
+};
+like( $@, qr/not listed in the validation options/, "bad param" );
+
+Test::IData->cleanup;
Index: /branches/jhoblitt/Nebulous-Server/t/11_client_create.t
===================================================================
--- /branches/jhoblitt/Nebulous-Server/t/11_client_create.t	(revision 2784)
+++ /branches/jhoblitt/Nebulous-Server/t/11_client_create.t	(revision 2784)
@@ -0,0 +1,103 @@
+#!/usr/bin/perl
+
+use strict;
+use warnings;
+
+use Apache::Test qw( -withtestmore );
+
+plan tests => 7;
+
+use lib qw( ./t ./lib );
+
+use PS::IPP::IData::Client;
+use PS::IPP::IData::Util qw( :standard );
+use Test::IData;
+
+my $hostport = Apache::Test->config->{ 'hostport' };
+
+Test::IData->setup;
+
+{
+    # key
+    my $idata = PS::IPP::IData::Client->new(
+        proxy => "http://$hostport/idata",
+    );
+
+    my $fh = $idata->create( "foo" );
+
+    is( ref $fh, 'GLOB', "good filehandle" );
+}
+
+Test::IData->setup;
+
+{
+    # key, class
+    my $idata = PS::IPP::IData::Client->new(
+        proxy => "http://$hostport/idata",
+    );
+
+    my $fh = $idata->create( "foo", 0 );
+
+    is( ref $fh, 'GLOB', "good filehandle" );
+}
+
+Test::IData->setup;
+
+{
+    # key, class, volume
+    my $idata = PS::IPP::IData::Client->new(
+        proxy => "http://$hostport/idata",
+    );
+
+    my $fh = $idata->create( "foo", 0, "node01" );
+
+    is( ref $fh, 'GLOB', "good filehandle" );
+}
+
+Test::IData->setup;
+
+{
+    # key, class, volume, comment
+    my $idata = PS::IPP::IData::Client->new(
+        proxy => "http://$hostport/idata",
+    );
+
+    my $fh = $idata->create( "foo", 0, "node01", "this is foo" );
+
+    is( ref $fh, 'GLOB', "good filehandle" );
+}
+
+Test::IData->setup;
+
+{
+    my $idata = PS::IPP::IData::Client->new(
+        proxy => "http://$hostport/idata",
+    );
+
+    $idata->create( "foo" );
+    is( $idata->create( "foo" ), undef, "object already exists" );
+}
+
+Test::IData->setup;
+
+eval {
+    my $idata = PS::IPP::IData::Client->new(
+        proxy => "http://$hostport/idata",
+    );
+
+    $idata->create();
+};
+like( $@, qr/2 - 4 were expected/, "no params" );
+
+Test::IData->setup;
+
+eval {
+    my $idata = PS::IPP::IData::Client->new(
+        proxy => "http://$hostport/idata",
+    );
+
+    $idata->create( 1, 2, 3, 4, 5 );
+};
+like( $@, qr/2 - 4 were expected/, "too many params" );
+
+Test::IData->cleanup;
Index: /branches/jhoblitt/Nebulous-Server/t/12_client_replicate.t
===================================================================
--- /branches/jhoblitt/Nebulous-Server/t/12_client_replicate.t	(revision 2784)
+++ /branches/jhoblitt/Nebulous-Server/t/12_client_replicate.t	(revision 2784)
@@ -0,0 +1,97 @@
+#!/usr/bin/perl
+
+use strict;
+use warnings;
+
+use Apache::Test qw( -withtestmore );
+
+plan tests => 11;
+
+use lib qw( ./t ./lib );
+
+use PS::IPP::IData::Client;
+use PS::IPP::IData::Util qw( :standard );
+use Test::IData;
+
+my $hostport = Apache::Test->config->{ 'hostport' };
+
+Test::IData->setup;
+
+{
+    # key
+    my $idata = PS::IPP::IData::Client->new(
+        proxy => "http://$hostport/idata",
+    );
+    $idata->create( "foo" );
+
+    my $uri = $idata->replicate( "foo" );
+
+    ok( $uri, "good replication" );
+    ok( -e _get_file_path( $uri ), "file exists" );
+}
+
+Test::IData->setup;
+
+{
+    # key, $volume
+    my $idata = PS::IPP::IData::Client->new(
+        proxy => "http://$hostport/idata",
+    );
+    $idata->create( "foo" );
+
+    my $uri = $idata->replicate( "foo", "node01" );
+
+    ok( $uri, "good replication" );
+    ok( -e _get_file_path( $uri ), "file exists" );
+}
+
+Test::IData->setup;
+
+{
+    # key, $volume
+    my $idata = PS::IPP::IData::Client->new(
+        proxy => "http://$hostport/idata",
+    );
+    $idata->create( "foo" );
+
+    my $uri1 = $idata->replicate( "foo", "node01" );
+    my $uri2 = $idata->replicate( "foo", "node02" );
+
+    ok( $uri1, "good replication" );
+    ok( $uri2, "good replication" );
+    ok( -e _get_file_path( $uri1 ), "file exists" );
+    ok( -e _get_file_path( $uri2 ), "file exists" );
+}
+
+Test::IData->setup;
+
+{
+    my $idata = PS::IPP::IData::Client->new(
+        proxy => "http://$hostport/idata",
+    );
+    my $uri = $idata->replicate( "foo" );
+
+    is( $uri, undef, "storage object does not exist" );
+}
+
+Test::IData->setup;
+
+eval {
+    my $idata = PS::IPP::IData::Client->new(
+        proxy => "http://$hostport/idata",
+    );
+    $idata->replicate();
+};
+like( $@, qr/1 - 2 were expected/, "no params" );
+
+Test::IData->setup;
+
+eval {
+    my $idata = PS::IPP::IData::Client->new(
+        proxy => "http://$hostport/idata",
+    );
+    $idata->replicate( 1, 2, 3 );
+};
+like( $@, qr/1 - 2 were expected/, "too many params" );
+
+Test::IData->cleanup;
Index: /branches/jhoblitt/Nebulous-Server/t/13_client_cull.t
===================================================================
--- /branches/jhoblitt/Nebulous-Server/t/13_client_cull.t	(revision 2784)
+++ /branches/jhoblitt/Nebulous-Server/t/13_client_cull.t	(revision 2784)
@@ -0,0 +1,127 @@
+#!/usr/bin/perl
+
+use strict;
+use warnings;
+
+use Apache::Test qw( -withtestmore );
+
+plan tests => 15;
+
+use lib qw( ./t ./lib );
+
+use PS::IPP::IData::Client;
+use PS::IPP::IData::Util qw( :standard );
+use Test::IData;
+
+my $hostport = Apache::Test->config->{ 'hostport' };
+
+Test::IData->setup;
+
+{
+    # key
+    my $idata = PS::IPP::IData::Client->new(
+        proxy => "http://$hostport/idata",
+    );
+    $idata->create( "foo" );
+    $idata->replicate( "foo" );
+
+    my $uri = $idata->cull( "foo" );
+
+    ok( $uri, "good cull" );
+    ok( ! -e _get_file_path( $uri ), "file doesn't exist" );
+}
+
+Test::IData->setup;
+
+SKIP: {
+    skip "unsure if this should be part of the API", 2;
+
+    # key, $volume
+    my $idata = PS::IPP::IData::Client->new(
+        proxy => "http://$hostport/idata",
+    );
+    $idata->create( "foo" );
+    $idata->replicate( "foo", "node01" );
+
+    my $uri = $idata->cull( "foo", "node01" );
+
+    ok( $uri, "good cull" );
+    ok( ! -e _get_file_path( $uri ), "file exists" );
+}
+
+Test::IData->setup;
+
+{
+    # key
+    my $idata = PS::IPP::IData::Client->new(
+        proxy => "http://$hostport/idata",
+    );
+    $idata->create( "foo" );
+
+    $idata->replicate( "foo", "node01" );
+    $idata->replicate( "foo", "node02" );
+
+    my $uri1 = $idata->cull( "foo" );
+    my $uri2 = $idata->cull( "foo" );
+
+    ok( $uri1, "good cull" );
+    ok( $uri2, "good cull" );
+    ok( ! -e _get_file_path( $uri1 ), "file exists" );
+    ok( ! -e _get_file_path( $uri2 ), "file exists" );
+}
+
+Test::IData->setup;
+
+SKIP: {
+    skip "unsure if this should be part of the API", 4;
+
+    # key, $volume
+    my $idata = PS::IPP::IData::Client->new(
+        proxy => "http://$hostport/idata",
+    );
+    $idata->create( "foo" );
+
+    $idata->replicate( "foo", "node01" );
+    $idata->replicate( "foo", "node02" );
+
+    my $uri1 = $idata->cull( "foo", "node01" );
+    my $uri2 = $idata->cull( "foo", "node02" );
+
+    ok( $uri1, "good cull" );
+    ok( $uri2, "good cull" );
+    ok( ! -e _get_file_path( $uri1 ), "file exists" );
+    ok( ! -e _get_file_path( $uri2 ), "file exists" );
+}
+
+Test::IData->setup;
+
+{
+    my $idata = PS::IPP::IData::Client->new(
+        proxy => "http://$hostport/idata",
+    );
+    my $uri = $idata->cull( "foo" );
+
+    is( $uri, undef, "storage object does not exist" );
+}
+
+Test::IData->setup;
+
+eval {
+    my $idata = PS::IPP::IData::Client->new(
+        proxy => "http://$hostport/idata",
+    );
+    $idata->cull();
+};
+like( $@, qr/1 was expected/, "no params" );
+
+Test::IData->setup;
+
+eval {
+    my $idata = PS::IPP::IData::Client->new(
+        proxy => "http://$hostport/idata",
+    );
+    $idata->cull( 1, 2 );
+};
+like( $@, qr/1 was expected/, "too many params" );
+
+Test::IData->cleanup;
Index: /branches/jhoblitt/Nebulous-Server/t/14_client_lock.t
===================================================================
--- /branches/jhoblitt/Nebulous-Server/t/14_client_lock.t	(revision 2784)
+++ /branches/jhoblitt/Nebulous-Server/t/14_client_lock.t	(revision 2784)
@@ -0,0 +1,269 @@
+#!/usr/bin/perl
+
+use strict;
+use warnings;
+
+use Apache::Test qw( -withtestmore );
+
+plan tests => 19;
+
+use lib qw( ./t ./lib );
+
+use PS::IPP::IData::Client;
+use PS::IPP::IData::Util qw( :standard );
+use Test::IData;
+
+my $hostport = Apache::Test->config->{ 'hostport' };
+
+Test::IData->setup;
+
+{
+    # key, type
+    my $idata = PS::IPP::IData::Client->new(
+        proxy => "http://$hostport/idata",
+    );
+    $idata->create( "foo" );
+
+    ok( $idata->lock( "foo", "read" ), "read lock" );
+}
+
+Test::IData->setup;
+
+{
+    # key, type, timeout
+    my $idata = PS::IPP::IData::Client->new(
+        proxy => "http://$hostport/idata",
+    );
+    $idata->create( "foo" );
+
+    ok( $idata->lock( "foo", "read", 10 ), "read lock" );
+}
+
+Test::IData->setup;
+
+{
+    my $idata = PS::IPP::IData::Client->new(
+        proxy => "http://$hostport/idata",
+    );
+    $idata->create( "foo" );
+
+    ok( $idata->lock( "foo", "read" ), "read lock" );
+    ok( $idata->lock( "foo", "read" ), "read lock" );
+}
+
+Test::IData->setup;
+
+{
+    # key, type
+    my $idata = PS::IPP::IData::Client->new(
+        proxy => "http://$hostport/idata",
+    );
+    $idata->create( "foo" );
+
+    ok( $idata->lock( "foo", "write" ), "write lock" );
+}
+
+Test::IData->setup;
+
+{
+    # key, type, timeout
+    my $idata = PS::IPP::IData::Client->new(
+        proxy => "http://$hostport/idata",
+    );
+    $idata->create( "foo" );
+
+    ok( $idata->lock( "foo", "write", 10 ), "write lock" );
+}
+
+Test::IData->setup;
+
+{
+    my $idata = PS::IPP::IData::Client->new(
+        proxy => "http://$hostport/idata",
+    );
+
+    is( $idata->lock( "foo", "read" ), undef, "storage object does not exist" );
+}
+
+Test::IData->setup;
+
+{
+    my $idata = PS::IPP::IData::Client->new(
+        proxy => "http://$hostport/idata",
+    );
+
+    is( $idata->lock( "foo", "write" ), undef, "storage object does not exist" );
+}
+
+Test::IData->setup;
+
+diag "testing a spinlock, this will be a litle slow...";
+
+eval {
+    local $SIG{ALRM} = sub { alarm 0; die "timeout"; };
+    alarm 8;
+
+    # key, type, timeout
+    my $idata = PS::IPP::IData::Client->new(
+        proxy => "http://$hostport/idata",
+    );
+    $idata->create( "foo" );
+
+    $idata->lock( "foo", "read" );
+    $idata->lock( "foo", "write" );
+
+    alarm 0;
+};
+like( $@, qr/timeout/, "default spinlock" );
+
+Test::IData->setup;
+
+diag "testing a spinlock, this will be a litle slow...";
+
+eval {
+    local $SIG{ALRM} = sub { alarm 0; die "timeout"; };
+    alarm 12;
+
+    # key, type, timeout
+    my $idata = PS::IPP::IData::Client->new(
+        proxy => "http://$hostport/idata",
+    );
+    $idata->create( "foo" );
+
+    $idata->lock( "foo", "read" );
+    unless ( $idata->lock( "foo", "write" ) ) {
+        alarm 0;
+        die "gaveup";
+    }
+
+    alarm 0;
+};
+like( $@, qr/gaveup/, "default spinlock" );
+
+Test::IData->setup;
+
+diag "testing a spinlock, this will be a litle slow...";
+
+eval {
+    local $SIG{ALRM} = sub { alarm 0; die "timeout"; };
+    alarm 3;
+
+    # key, type, timeout
+    my $idata = PS::IPP::IData::Client->new(
+        proxy => "http://$hostport/idata",
+    );
+    $idata->create( "foo" );
+
+    $idata->lock( "foo", "read" );
+    $idata->lock( "foo", "write", 5 );
+
+    alarm 0;
+};
+like( $@, qr/timeout/, "specified spinlock" );
+
+Test::IData->setup;
+
+diag "testing a spinlock, this will be a litle slow...";
+
+eval {
+    local $SIG{ALRM} = sub { alarm 0; die "timeout"; };
+    alarm 7;
+
+    # key, type, timeout
+    my $idata = PS::IPP::IData::Client->new(
+        proxy => "http://$hostport/idata",
+    );
+    $idata->create( "foo" );
+
+    $idata->lock( "foo", "read" );
+    unless ( $idata->lock( "foo", "write", 5 ) ) {
+        alarm 0;
+        die "gaveup";
+    }
+
+    alarm 0;
+};
+
+like( $@, qr/gaveup/, "specified spinlock" );
+
+Test::IData->setup;
+
+{
+    my $idata = PS::IPP::IData::Client->new(
+        proxy => "http://$hostport/idata",
+    );
+    $idata->create( "foo" );
+
+    $idata->lock( "foo", "write" );
+    is( $idata->lock( "foo", "write", 1 ), undef, "can not write lock twice" );
+}
+
+Test::IData->setup;
+
+{
+    my $idata = PS::IPP::IData::Client->new(
+        proxy => "http://$hostport/idata",
+    );
+    $idata->create( "foo" );
+
+    $idata->lock( "foo", "read" );
+    is( $idata->lock( "foo", "write", 1 ), undef, "can not write lock after read lock" );
+}
+
+Test::IData->setup;
+
+{
+    my $idata = PS::IPP::IData::Client->new(
+        proxy => "http://$hostport/idata",
+    );
+    $idata->create( "foo" );
+
+    $idata->lock( "foo", "write" );
+    is( $idata->lock( "foo", "read", 1 ), undef, "can not read lock after write lock" );
+}
+
+Test::IData->setup;
+
+eval {
+    my $idata = PS::IPP::IData::Client->new(
+        proxy => "http://$hostport/idata",
+    );
+
+    $idata->lock();
+};
+like( $@, qr/2 - 3 were expected/, "no params" );
+
+Test::IData->setup;
+
+eval {
+    my $idata = PS::IPP::IData::Client->new(
+        proxy => "http://$hostport/idata",
+    );
+
+    $idata->lock( "foo" );
+};
+like( $@, qr/2 - 3 were expected/, "not enough params" );
+
+Test::IData->setup;
+
+eval {
+    my $idata = PS::IPP::IData::Client->new(
+        proxy => "http://$hostport/idata",
+    );
+
+    $idata->lock( "foo", "both" );
+};
+like( $@, qr/is read or write/, "not read or write" );
+
+Test::IData->setup;
+
+eval {
+    my $idata = PS::IPP::IData::Client->new(
+        proxy => "http://$hostport/idata",
+    );
+
+    $idata->lock( "foo", 'read', 3, "bar" );
+};
+like( $@, qr/2 - 3 were expected/, "too many params" );
+
+Test::IData->cleanup;
Index: /branches/jhoblitt/Nebulous-Server/t/15_client_unlock.t
===================================================================
--- /branches/jhoblitt/Nebulous-Server/t/15_client_unlock.t	(revision 2784)
+++ /branches/jhoblitt/Nebulous-Server/t/15_client_unlock.t	(revision 2784)
@@ -0,0 +1,166 @@
+#!/usr/bin/perl
+
+use strict;
+use warnings;
+
+use Apache::Test qw( -withtestmore );
+
+plan tests => 14;
+
+use lib qw( ./t ./lib );
+
+use PS::IPP::IData::Client;
+use PS::IPP::IData::Util qw( :standard );
+use Test::IData;
+
+my $hostport = Apache::Test->config->{ 'hostport' };
+
+Test::IData->setup;
+
+{
+    my $idata = PS::IPP::IData::Client->new(
+        proxy => "http://$hostport/idata",
+    );
+    $idata->create( "foo" );
+    $idata->lock( "foo", "read" );
+
+    ok( $idata->unlock( "foo", "read" ), "read unlock" );
+}
+
+Test::IData->setup;
+
+{
+    my $idata = PS::IPP::IData::Client->new(
+        proxy => "http://$hostport/idata",
+    );
+    $idata->create( "foo" );
+    $idata->lock( "foo", "read" );
+    $idata->lock( "foo", "read" );
+
+    ok( $idata->unlock( "foo", "read" ), "read unlock" );
+    ok( $idata->unlock( "foo", "read" ), "read unlock" );
+}
+
+Test::IData->setup;
+
+{
+    my $idata = PS::IPP::IData::Client->new(
+        proxy => "http://$hostport/idata",
+    );
+    $idata->create( "foo" );
+    $idata->lock( "foo", "write" );
+
+    ok( $idata->unlock( "foo", "write" ), "write unlock" );
+}
+
+Test::IData->setup;
+
+{
+    my $idata = PS::IPP::IData::Client->new(
+        proxy => "http://$hostport/idata",
+    );
+
+    is( $idata->unlock( "foo", "read" ), undef, "storage object does not exist" );
+}
+
+Test::IData->setup;
+
+{
+    my $idata = PS::IPP::IData::Client->new(
+        proxy => "http://$hostport/idata",
+    );
+
+    is( $idata->unlock( "foo", "write" ), undef, "storage object does not exist" );
+}
+
+Test::IData->setup;
+
+{
+    my $idata = PS::IPP::IData::Client->new(
+        proxy => "http://$hostport/idata",
+    );
+    $idata->create( "foo" );
+
+    is( $idata->unlock( "foo", "read" ), undef, "no lock set" );
+}
+
+Test::IData->setup;
+
+{
+    my $idata = PS::IPP::IData::Client->new(
+        proxy => "http://$hostport/idata",
+    );
+    $idata->create( "foo" );
+
+    is( $idata->unlock( "foo", "write" ), undef, "no lock set" );
+}
+
+Test::IData->setup;
+
+{
+    my $idata = PS::IPP::IData::Client->new(
+        proxy => "http://$hostport/idata",
+    );
+    $idata->create( "foo" );
+    $idata->lock( "foo", "write" );
+
+    is( $idata->unlock( "foo", "read" ), undef, "read unlock under write lock" );
+}
+
+Test::IData->setup;
+
+{
+    my $idata = PS::IPP::IData::Client->new(
+        proxy => "http://$hostport/idata",
+    );
+    $idata->create( "foo" );
+    $idata->lock( "foo", "read" );
+
+    is( $idata->unlock( "foo", "write" ), undef, "write unlock under read lock" );
+}
+
+Test::IData->setup;
+
+eval {
+    my $idata = PS::IPP::IData::Client->new(
+        proxy => "http://$hostport/idata",
+    );
+
+    $idata->unlock();
+};
+like( $@, qr/2 were expected/, "no params" );
+
+Test::IData->setup;
+
+eval {
+    my $idata = PS::IPP::IData::Client->new(
+        proxy => "http://$hostport/idata",
+    );
+
+    $idata->unlock( "foo" );
+};
+like( $@, qr/2 were expected/, "not enough params" );
+
+Test::IData->setup;
+
+eval {
+    my $idata = PS::IPP::IData::Client->new(
+        proxy => "http://$hostport/idata",
+    );
+
+    $idata->unlock( "foo", "both" );
+};
+like( $@, qr/is read or write/, "not read or write" );
+
+Test::IData->setup;
+
+eval {
+    my $idata = PS::IPP::IData::Client->new(
+        proxy => "http://$hostport/idata",
+    );
+
+    $idata->unlock( "foo", 'read', 3 );
+};
+like( $@, qr/2 were expected/, "too many params" );
+
+Test::IData->cleanup;
Index: /branches/jhoblitt/Nebulous-Server/t/16_client_find_instances.t
===================================================================
--- /branches/jhoblitt/Nebulous-Server/t/16_client_find_instances.t	(revision 2784)
+++ /branches/jhoblitt/Nebulous-Server/t/16_client_find_instances.t	(revision 2784)
@@ -0,0 +1,114 @@
+#!/usr/bin/perl
+
+use strict;
+use warnings;
+
+use Apache::Test qw( -withtestmore );
+
+plan tests => 7;
+
+use lib qw( ./t ./lib );
+
+use PS::IPP::IData::Client;
+use PS::IPP::IData::Util qw( :standard );
+use Test::IData;
+
+my $hostport = Apache::Test->config->{ 'hostport' };
+
+Test::IData->setup;
+
+SKIP: {
+    skip "need to write a module to validate URIs", 7;
+
+{
+    # key
+    my $idata = PS::IPP::IData::Client->new(
+        proxy => "http://$hostport/idata",
+    );
+    my $uri = $idata->create( "foo" );
+
+    my $locations = $idata->find_instances( "foo" );
+
+    is( $uri, @$locations[0], "URIs match" );
+}
+
+Test::IData->setup;
+
+{
+    # key
+    my $idata = PS::IPP::IData::Client->new(
+        proxy => "http://$hostport/idata",
+    );
+    my $uri1 = $idata->create( "foo" );
+    my $uri2 = $idata->replicate( "foo" );
+
+    my $locations = $idata->find_instances( "foo" );
+
+    ok( eq_set( [$uri1, $uri2], $locations ), "URIs match" );
+}
+
+Test::IData->setup;
+
+{
+    # key, volume
+    my $idata = PS::IPP::IData::Client->new(
+        proxy => "http://$hostport/idata",
+    );
+    my $uri = $idata->create( "foo" );
+
+    my $locations = $idata->find_instances( "foo", "node01" );
+
+    is( $uri, $locations->[0], "URIs match" );
+}
+
+Test::IData->setup;
+
+{
+    # key
+    my $idata = PS::IPP::IData::Client->new(
+        proxy => "http://$hostport/idata",
+    );
+    my $uri1 = $idata->create( "foo" );
+    my $uri2 = $idata->replicate( "foo" );
+
+    my $locations = $idata->find_instances( "foo", "node01" );
+
+    ok( eq_set( [$uri1, $uri2], $locations ), "URIs match" );
+}
+
+
+Test::IData->setup;
+
+{
+    my $idata = PS::IPP::IData::Client->new(
+        proxy => "http://$hostport/idata",
+    );
+
+    is( $idata->find_instances( "foo" ), undef, "storage object does not exist" );
+}
+
+Test::IData->setup;
+
+eval {
+    my $idata = PS::IPP::IData::Client->new(
+        proxy => "http://$hostport/idata",
+    );
+
+    $idata->find_instances();
+};
+like( $@, qr/1 - 2 were expected/, "no params" );
+
+Test::IData->setup;
+
+eval {
+    my $idata = PS::IPP::IData::Client->new(
+        proxy => "http://$hostport/idata",
+    );
+
+    $idata->find_instances( "foo", 'read', 3 );
+};
+like( $@, qr/1 - 2 were expected/, "too many params" );
+
+Test::IData->cleanup;
+
+}
Index: /branches/jhoblitt/Nebulous-Server/t/apache_03_server_create_object.x
===================================================================
--- /branches/jhoblitt/Nebulous-Server/t/apache_03_server_create_object.x	(revision 2784)
+++ /branches/jhoblitt/Nebulous-Server/t/apache_03_server_create_object.x	(revision 2784)
@@ -0,0 +1,95 @@
+#!/usr/bin/perl
+
+use strict;
+use warnings;
+
+use Apache::Test qw(-withtestmore);
+
+plan tests => 11;
+
+use lib qw( ./t ./lib );
+
+use Apache::TestUtil;
+use PS::IPP::IData::Util qw( :standard );
+use SOAP::Lite;
+use Test::IData;
+
+my $hostport = Apache::Test::config()->{ 'hostport' };
+my $idata = SOAP::Lite->new(
+    proxy   => "http://$hostport/idata",
+    uri     => "http://pan-starrs.ifa.hawaii.edu/PS/IPP/IData/Server",
+);
+
+Test::IData->setup;
+
+{
+    # key
+    my $uri = $idata->create_object( "foo" )->result;
+
+    ok( -e _get_file_path( $uri ), "file exists" );
+
+    like( $uri, qr/^file:\//, "valid URI" );
+}
+
+Test::IData->setup;
+
+{
+    # key, class
+    my $uri = $idata->create_object( "foo", 0 )->result;
+    $idata->delete_instance( $uri );
+
+    ok( -e _get_file_path( $uri ), "file exists" );
+
+    like( $uri, qr/^file:\//, "valid URI" );
+}
+
+Test::IData->setup;
+
+{
+    # key, class, volume
+    my $uri = $idata->create_object( "foo", 0, "node01" )->result;
+    $idata->delete_instance( $uri );
+
+    ok( -e _get_file_path( $uri ), "file exists" );
+
+    like( $uri, qr/^file:\//, "valid URI" );
+}
+
+Test::IData->setup;
+
+{
+    # key, class, volume, comment
+    my $uri = $idata->create_object( "foo", 0, "node01", "this is foo" )->result;
+    $idata->delete_instance( $uri );
+
+    ok( -e _get_file_path( $uri ), "file exists" );
+
+    like( $uri, qr/^file:\//, "valid URI" );
+}
+
+Test::IData->setup;
+
+{
+    $idata->create_object( "foo" );
+    my $response = $idata->create_object( "foo" );
+
+    like( $response->faultstring, qr/Duplicate entry/, "object already exists" );
+}
+
+Test::IData->setup;
+
+{
+    my $response = $idata->create_object();
+
+    like( $response->faultstring, qr/2 - 4 were expected/, "no params" );
+}
+
+Test::IData->setup;
+
+{
+    my $response = $idata->create_object( 1, 2, 3, 4, 5 );
+
+    like( $response->faultstring, qr/2 - 4 were expected/, "too many params" );
+}
+
+Test::IData->cleanup;
Index: /branches/jhoblitt/Nebulous-Server/t/apache_04_server_replicate_object.x
===================================================================
--- /branches/jhoblitt/Nebulous-Server/t/apache_04_server_replicate_object.x	(revision 2784)
+++ /branches/jhoblitt/Nebulous-Server/t/apache_04_server_replicate_object.x	(revision 2784)
@@ -0,0 +1,86 @@
+#!/usr/bin/perl
+
+use strict;
+use warnings;
+
+use Apache::Test qw(-withtestmore);
+
+plan tests => 11;
+
+use lib qw( ./t ./lib );
+
+use Apache::TestUtil;
+use PS::IPP::IData::Util qw( :standard );
+use SOAP::Lite;
+use Test::IData;
+
+my $hostport = Apache::Test::config()->{ 'hostport' };
+my $idata = SOAP::Lite->new(
+    proxy   => "http://$hostport/idata",
+    uri     => "http://pan-starrs.ifa.hawaii.edu/PS/IPP/IData/Server",
+);
+
+Test::IData->setup;
+
+{
+    # key
+    $idata->create_object( "foo" );
+    my $uri = $idata->replicate_object( "foo" )->result;
+
+    ok( -e _get_file_path( $uri ), "file exists" );
+
+    like( $uri, qr/^file:\//, "valid URI" );
+}
+
+Test::IData->setup;
+
+{
+    # key, $volume
+    $idata->create_object( "foo" );
+    my $uri = $idata->replicate_object( "foo", "node01" )->result;
+
+    ok( -e _get_file_path( $uri ), "file exists" );
+
+    like( $uri, qr/^file:\//, "valid URI" );
+}
+
+Test::IData->setup;
+
+{
+    # key, $volume
+    $idata->create_object( "foo" );
+    my $uri1 = $idata->replicate_object( "foo", "node01" )->result;
+    my $uri2 = $idata->replicate_object( "foo", "node02" )->result;
+
+    ok( -e _get_file_path( $uri1 ), "file exists" );
+    ok( -e _get_file_path( $uri2 ), "file exists" );
+
+    like( $uri1, qr/^file:\//, "valid URI" );
+    like( $uri2, qr/^file:\//, "valid URI" );
+}
+
+Test::IData->setup;
+
+{
+    my $response = $idata->replicate_object();
+
+    like( $response->faultstring, qr/1 - 2 were expected/, "no params" );
+}
+
+Test::IData->setup;
+
+{
+    my $response = $idata->replicate_object( 1, 2, 3 );
+
+    like( $response->faultstring, qr/1 - 2 were expected/, "too many params" );
+}
+
+Test::IData->setup;
+
+{
+    my $response = $idata->replicate_object( "foo" );
+
+    like( $response->faultstring, qr/storage object does not exist/, "storage object does not exist" );
+}
+
+Test::IData->cleanup;
Index: /branches/jhoblitt/Nebulous-Server/t/apache_05_server_lock_object.x
===================================================================
--- /branches/jhoblitt/Nebulous-Server/t/apache_05_server_lock_object.x	(revision 2784)
+++ /branches/jhoblitt/Nebulous-Server/t/apache_05_server_lock_object.x	(revision 2784)
@@ -0,0 +1,129 @@
+#!/usr/bin/perl
+
+use strict;
+use warnings;
+
+use Apache::Test qw(-withtestmore);
+
+plan tests => 13;
+
+use lib qw( ./t ./lib );
+
+use Apache::TestUtil;
+use PS::IPP::IData::Util qw( :standard );
+use SOAP::Lite;
+use Test::IData;
+
+my $hostport = Apache::Test::config()->{ 'hostport' };
+my $idata = SOAP::Lite->new(
+    proxy   => "http://$hostport/idata",
+    uri     => "http://pan-starrs.ifa.hawaii.edu/PS/IPP/IData/Server",
+);
+
+Test::IData->setup;
+
+{
+    $idata->create_object( "foo" );
+
+    ok( $idata->lock_object( "foo", "read" )->result, "read lock" );
+}
+
+Test::IData->setup;
+
+{
+    $idata->create_object( "foo" );
+
+    ok( $idata->lock_object( "foo", "read" )->result, "read lock" );
+    ok( $idata->lock_object( "foo", "read" )->result, "read lock" );
+}
+
+Test::IData->setup;
+
+{
+    $idata->create_object( "foo" );
+
+    ok( $idata->lock_object( "foo", "write" )->result, "write lock" );
+}
+
+Test::IData->setup;
+
+{
+    my $result = $idata->lock_object( "foo", "read" );
+
+    like( $result->faultstring, qr/storage object does not exist/, "storage object does not exist" );
+}
+
+Test::IData->setup;
+
+{
+    my $result = $idata->lock_object( "foo", "write" );
+
+    like( $result->faultstring, qr/storage object does not exist/, "storage object does not exist" );
+}
+
+Test::IData->setup;
+
+{
+    $idata->create_object( "foo" );
+
+    $idata->lock_object( "foo", "write" );
+    my $result = $idata->lock_object( "foo", "write" );
+
+    like( $result->faultstring, qr/can not write lock twice/, "can not write lock twice" );
+}
+
+Test::IData->setup;
+
+{
+    $idata->create_object( "foo" );
+
+    $idata->lock_object( "foo", "read" );
+    my $result = $idata->lock_object( "foo", "write" );
+
+    like( $result->faultstring, qr/can not write lock after read lock/, "can not write lock after read lock" );
+}
+
+Test::IData->setup;
+
+{
+    $idata->create_object( "foo" );
+
+    $idata->lock_object( "foo", "write" );
+    my $result = $idata->lock_object( "foo", "read" );
+
+    like( $result->faultstring, qr/can not read lock after write lock/, "can not read lock after write lock" );
+}
+
+Test::IData->setup;
+
+{
+    my $result = $idata->lock_object();
+
+    like( $result->faultstring, qr/2 were expected/, "no params" );
+}
+
+Test::IData->setup;
+
+{
+    my $result = $idata->lock_object( "foo" );
+
+    like( $result->faultstring, qr/2 were expected/, "not enough params" );
+}
+
+Test::IData->setup;
+
+{
+    my $result = $idata->lock_object( "foo", "both" );
+
+    like( $result->faultstring, qr/is read or write/, "not read or write" );
+}
+
+Test::IData->setup;
+
+{
+    my $result = $idata->lock_object( "foo", 'read', 3 );
+
+    like( $result->faultstring, qr/2 were expected/, "too many params" );
+}
+
+Test::IData->cleanup;
Index: /branches/jhoblitt/Nebulous/Build.PL
===================================================================
--- /branches/jhoblitt/Nebulous/Build.PL	(revision 2784)
+++ /branches/jhoblitt/Nebulous/Build.PL	(revision 2784)
@@ -0,0 +1,30 @@
+use Module::Build;
+# See perldoc Module::Build for details of how this works
+
+my $build_pkg   = eval { require Apache::TestMB }
+                ? 'Apache::TestMB'
+                : 'Module::Build';
+
+$build_pkg->new(
+    module_name         => 'PS::IPP::IData',
+    dist_version_from   => 'lib/PS/IPP/IData/Server.pm',
+    author              => 'Joshua Hoblitt <jhoblitt@cpan.org>',
+    license             => 'gpl',
+    requires            => {
+        'URI'                   => 1.30,
+        'SOAP::Lite'            => 0.60,
+        'Log::Log4perl'         => 0.48,
+        'DBI'                   => 1.42,
+        'DBD::mysql'            => 2.9004,
+        'Class::Accessor::Fast' => 0,
+        'Params::Validate'      => 0,
+        'Apache::Test'          => 1.12,
+        'Test::More'            => 0.49,
+    },
+    recommends          => {
+        'Apache::DBI'           => 0.94,
+    },
+    build_requires      => {
+        'File::Temp'            => 0,
+    },
+)->create_build_script;
Index: /branches/jhoblitt/Nebulous/Changes
===================================================================
--- /branches/jhoblitt/Nebulous/Changes	(revision 2784)
+++ /branches/jhoblitt/Nebulous/Changes	(revision 2784)
@@ -0,0 +1,5 @@
+Revision history for Perl module Poi::PixelData
+
+0.01 Fri Sep  3 10:23:31 2004
+    - first working version
+	- original version; created by ExtUtils::ModuleMaker 0.32
Index: /branches/jhoblitt/Nebulous/LICENSE
===================================================================
--- /branches/jhoblitt/Nebulous/LICENSE	(revision 2784)
+++ /branches/jhoblitt/Nebulous/LICENSE	(revision 2784)
@@ -0,0 +1,260 @@
+The General Public License (GPL)
+Version 2, June 1991
+
+Copyright (C) 1989, 1991 Free Software Foundation, Inc. 675 Mass Ave,
+Cambridge, MA 02139, USA. Everyone is permitted to copy and distribute
+verbatim copies of this license document, but changing it is not allowed.
+
+Preamble
+
+The licenses for most software are designed to take away your freedom to share
+and change it. By contrast, the GNU General Public License is intended to
+guarantee your freedom to share and change free software--to make sure the
+software is free for all its users. This General Public License applies to most of
+the Free Software Foundation's software and to any other program whose
+authors commit to using it. (Some other Free Software Foundation software is
+covered by the GNU Library General Public License instead.) You can apply it to
+your programs, too.
+
+When we speak of free software, we are referring to freedom, not price. Our
+General Public Licenses are designed to make sure that you have the freedom
+to distribute copies of free software (and charge for this service if you wish), that
+you receive source code or can get it if you want it, that you can change the
+software or use pieces of it in new free programs; and that you know you can do
+these things.
+
+To protect your rights, we need to make restrictions that forbid anyone to deny
+you these rights or to ask you to surrender the rights. These restrictions
+translate to certain responsibilities for you if you distribute copies of the
+software, or if you modify it.
+
+For example, if you distribute copies of such a program, whether gratis or for a
+fee, you must give the recipients all the rights that you have. You must make
+sure that they, too, receive or can get the source code. And you must show
+them these terms so they know their rights.
+
+We protect your rights with two steps: (1) copyright the software, and (2) offer
+you this license which gives you legal permission to copy, distribute and/or
+modify the software.
+
+Also, for each author's protection and ours, we want to make certain that
+everyone understands that there is no warranty for this free software. If the
+software is modified by someone else and passed on, we want its recipients to
+know that what they have is not the original, so that any problems introduced by
+others will not reflect on the original authors' reputations.
+
+Finally, any free program is threatened constantly by software patents. We wish
+to avoid the danger that redistributors of a free program will individually obtain
+patent licenses, in effect making the program proprietary. To prevent this, we
+have made it clear that any patent must be licensed for everyone's free use or
+not licensed at all.
+
+The precise terms and conditions for copying, distribution and modification
+follow.
+
+GNU GENERAL PUBLIC LICENSE
+TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND
+MODIFICATION
+
+0. This License applies to any program or other work which contains a notice
+placed by the copyright holder saying it may be distributed under the terms of
+this General Public License. The "Program", below, refers to any such program
+or work, and a "work based on the Program" means either the Program or any
+derivative work under copyright law: that is to say, a work containing the
+Program or a portion of it, either verbatim or with modifications and/or translated
+into another language. (Hereinafter, translation is included without limitation in
+the term "modification".) Each licensee is addressed as "you".
+
+Activities other than copying, distribution and modification are not covered by
+this License; they are outside its scope. The act of running the Program is not
+restricted, and the output from the Program is covered only if its contents
+constitute a work based on the Program (independent of having been made by
+running the Program). Whether that is true depends on what the Program does.
+
+1. You may copy and distribute verbatim copies of the Program's source code as
+you receive it, in any medium, provided that you conspicuously and appropriately
+publish on each copy an appropriate copyright notice and disclaimer of warranty;
+keep intact all the notices that refer to this License and to the absence of any
+warranty; and give any other recipients of the Program a copy of this License
+along with the Program.
+
+You may charge a fee for the physical act of transferring a copy, and you may at
+your option offer warranty protection in exchange for a fee.
+
+2. You may modify your copy or copies of the Program or any portion of it, thus
+forming a work based on the Program, and copy and distribute such
+modifications or work under the terms of Section 1 above, provided that you also
+meet all of these conditions:
+
+a) You must cause the modified files to carry prominent notices stating that you
+changed the files and the date of any change.
+
+b) You must cause any work that you distribute or publish, that in whole or in
+part contains or is derived from the Program or any part thereof, to be licensed
+as a whole at no charge to all third parties under the terms of this License.
+
+c) If the modified program normally reads commands interactively when run, you
+must cause it, when started running for such interactive use in the most ordinary
+way, to print or display an announcement including an appropriate copyright
+notice and a notice that there is no warranty (or else, saying that you provide a
+warranty) and that users may redistribute the program under these conditions,
+and telling the user how to view a copy of this License. (Exception: if the
+Program itself is interactive but does not normally print such an announcement,
+your work based on the Program is not required to print an announcement.)
+
+These requirements apply to the modified work as a whole. If identifiable
+sections of that work are not derived from the Program, and can be reasonably
+considered independent and separate works in themselves, then this License,
+and its terms, do not apply to those sections when you distribute them as
+separate works. But when you distribute the same sections as part of a whole
+which is a work based on the Program, the distribution of the whole must be on
+the terms of this License, whose permissions for other licensees extend to the
+entire whole, and thus to each and every part regardless of who wrote it.
+
+Thus, it is not the intent of this section to claim rights or contest your rights to
+work written entirely by you; rather, the intent is to exercise the right to control
+the distribution of derivative or collective works based on the Program.
+
+In addition, mere aggregation of another work not based on the Program with the
+Program (or with a work based on the Program) on a volume of a storage or
+distribution medium does not bring the other work under the scope of this
+License.
+
+3. You may copy and distribute the Program (or a work based on it, under
+Section 2) in object code or executable form under the terms of Sections 1 and 2
+above provided that you also do one of the following:
+
+a) Accompany it with the complete corresponding machine-readable source
+code, which must be distributed under the terms of Sections 1 and 2 above on a
+medium customarily used for software interchange; or,
+
+b) Accompany it with a written offer, valid for at least three years, to give any
+third party, for a charge no more than your cost of physically performing source
+distribution, a complete machine-readable copy of the corresponding source
+code, to be distributed under the terms of Sections 1 and 2 above on a medium
+customarily used for software interchange; or,
+
+c) Accompany it with the information you received as to the offer to distribute
+corresponding source code. (This alternative is allowed only for noncommercial
+distribution and only if you received the program in object code or executable
+form with such an offer, in accord with Subsection b above.)
+
+The source code for a work means the preferred form of the work for making
+modifications to it. For an executable work, complete source code means all the
+source code for all modules it contains, plus any associated interface definition
+files, plus the scripts used to control compilation and installation of the
+executable. However, as a special exception, the source code distributed need
+not include anything that is normally distributed (in either source or binary form)
+with the major components (compiler, kernel, and so on) of the operating system
+on which the executable runs, unless that component itself accompanies the
+executable.
+
+If distribution of executable or object code is made by offering access to copy
+from a designated place, then offering equivalent access to copy the source
+code from the same place counts as distribution of the source code, even though
+third parties are not compelled to copy the source along with the object code.
+
+4. You may not copy, modify, sublicense, or distribute the Program except as
+expressly provided under this License. Any attempt otherwise to copy, modify,
+sublicense or distribute the Program is void, and will automatically terminate
+your rights under this License. However, parties who have received copies, or
+rights, from you under this License will not have their licenses terminated so long
+as such parties remain in full compliance.
+
+5. You are not required to accept this License, since you have not signed it.
+However, nothing else grants you permission to modify or distribute the Program
+or its derivative works. These actions are prohibited by law if you do not accept
+this License. Therefore, by modifying or distributing the Program (or any work
+based on the Program), you indicate your acceptance of this License to do so,
+and all its terms and conditions for copying, distributing or modifying the
+Program or works based on it.
+
+6. Each time you redistribute the Program (or any work based on the Program),
+the recipient automatically receives a license from the original licensor to copy,
+distribute or modify the Program subject to these terms and conditions. You
+may not impose any further restrictions on the recipients' exercise of the rights
+granted herein. You are not responsible for enforcing compliance by third parties
+to this License.
+
+7. If, as a consequence of a court judgment or allegation of patent infringement
+or for any other reason (not limited to patent issues), conditions are imposed on
+you (whether by court order, agreement or otherwise) that contradict the
+conditions of this License, they do not excuse you from the conditions of this
+License. If you cannot distribute so as to satisfy simultaneously your obligations
+under this License and any other pertinent obligations, then as a consequence
+you may not distribute the Program at all. For example, if a patent license would
+not permit royalty-free redistribution of the Program by all those who receive
+copies directly or indirectly through you, then the only way you could satisfy
+both it and this License would be to refrain entirely from distribution of the
+Program.
+
+If any portion of this section is held invalid or unenforceable under any particular
+circumstance, the balance of the section is intended to apply and the section as
+a whole is intended to apply in other circumstances.
+
+It is not the purpose of this section to induce you to infringe any patents or other
+property right claims or to contest validity of any such claims; this section has
+the sole purpose of protecting the integrity of the free software distribution
+system, which is implemented by public license practices. Many people have
+made generous contributions to the wide range of software distributed through
+that system in reliance on consistent application of that system; it is up to the
+author/donor to decide if he or she is willing to distribute software through any
+other system and a licensee cannot impose that choice.
+
+This section is intended to make thoroughly clear what is believed to be a
+consequence of the rest of this License.
+
+8. If the distribution and/or use of the Program is restricted in certain countries
+either by patents or by copyrighted interfaces, the original copyright holder who
+places the Program under this License may add an explicit geographical
+distribution limitation excluding those countries, so that distribution is permitted
+only in or among countries not thus excluded. In such case, this License
+incorporates the limitation as if written in the body of this License.
+
+9. The Free Software Foundation may publish revised and/or new versions of the
+General Public License from time to time. Such new versions will be similar in
+spirit to the present version, but may differ in detail to address new problems or
+concerns.
+
+Each version is given a distinguishing version number. If the Program specifies a
+version number of this License which applies to it and "any later version", you
+have the option of following the terms and conditions either of that version or of
+any later version published by the Free Software Foundation. If the Program does
+not specify a version number of this License, you may choose any version ever
+published by the Free Software Foundation.
+
+10. If you wish to incorporate parts of the Program into other free programs
+whose distribution conditions are different, write to the author to ask for
+permission. For software which is copyrighted by the Free Software Foundation,
+write to the Free Software Foundation; we sometimes make exceptions for this.
+Our decision will be guided by the two goals of preserving the free status of all
+derivatives of our free software and of promoting the sharing and reuse of
+software generally.
+
+NO WARRANTY
+
+11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS
+NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
+APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE
+COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM
+"AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR
+IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
+MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE
+ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE
+PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE,
+YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR
+CORRECTION.
+
+12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED
+TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY
+WHO MAY MODIFY AND/OR REDISTRIBUTE THE PROGRAM AS
+PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
+GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES
+ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM
+(INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING
+RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
+PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY
+OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS
+BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES.
+
+END OF TERMS AND CONDITIONS
Index: /branches/jhoblitt/Nebulous/MANIFEST
===================================================================
--- /branches/jhoblitt/Nebulous/MANIFEST	(revision 2784)
+++ /branches/jhoblitt/Nebulous/MANIFEST	(revision 2784)
@@ -0,0 +1,53 @@
+Build.PL
+Changes
+LICENSE
+MANIFEST
+META.yml
+Makefile.PL
+README
+TODO
+Todo
+bootstrap.sh
+docs/apache_setup.txt
+docs/c_api.h
+docs/calc_gid.c
+docs/database_setup.txt
+docs/design.txt
+docs/requirements.txt
+docs/setup.txt
+docs/tmp.txt
+iData.wsdl
+lib/PS/IPP/IData/Client.pm
+lib/PS/IPP/IData/Client/HTTP.pm
+lib/PS/IPP/IData/Client/Log.pm
+lib/PS/IPP/IData/Server.pm
+lib/PS/IPP/IData/Server/Apache.pm
+lib/PS/IPP/IData/Server/Log.pm
+lib/PS/IPP/IData/Server/SQL.pm
+lib/PS/IPP/IData/Util.pm
+scripts/bench_test.pl
+scripts/imageserver.pl
+scripts/ptest.pl
+scripts/stest.pl
+t/01_load.t
+t/02_server_setup.t
+t/03_server_create_object.t
+t/04_server_replicate_object.t
+t/05_server_lock_object.t
+t/06_server_unlock_object.t
+t/07_server_find_instances.t
+t/08_server_delete_instance.t
+t/09_server_stat_object.t
+t/10_client_new.t
+t/11_client_create.t
+t/12_client_replicate.t
+t/13_client_cull.t
+t/14_client_lock.t
+t/15_client_unlock.t
+t/16_client_find_instances.t
+t/TEST.PL
+t/Test/IData.pm
+t/apache_03_server_create_object.x
+t/apache_04_server_replicate_object.x
+t/apache_05_server_lock_object.x
+t/conf/extra.conf.in
Index: /branches/jhoblitt/Nebulous/Makefile.PL
===================================================================
--- /branches/jhoblitt/Nebulous/Makefile.PL	(revision 2784)
+++ /branches/jhoblitt/Nebulous/Makefile.PL	(revision 2784)
@@ -0,0 +1,27 @@
+unless (eval "use Module::Build::Compat 0.02; 1" ) {
+  print "This module requires Module::Build to install itself.\n";
+
+  require ExtUtils::MakeMaker;
+  my $yn = ExtUtils::MakeMaker::prompt
+    ('  Install Module::Build from CPAN?', 'y');
+
+  if ($yn =~ /^y/i) {
+    require Cwd;
+    require File::Spec;
+    require CPAN;
+
+    # Save this 'cause CPAN will chdir all over the place.
+    my $cwd = Cwd::cwd();
+    my $makefile = File::Spec->rel2abs($0);
+
+    CPAN::Shell->install('Module::Build::Compat');
+
+    chdir $cwd or die "Cannot chdir() back to $cwd: $!";
+    exec $^X, $makefile, @ARGV;  # Redo now that we have Module::Build
+  } else {
+    warn " *** Cannot install without Module::Build.  Exiting ...\n";
+    exit 1;
+  }
+}
+Module::Build::Compat->run_build_pl(args => \@ARGV);
+Module::Build::Compat->write_makefile();
Index: /branches/jhoblitt/Nebulous/README
===================================================================
--- /branches/jhoblitt/Nebulous/README	(revision 2784)
+++ /branches/jhoblitt/Nebulous/README	(revision 2784)
@@ -0,0 +1,16 @@
+pod2text Poi::PixelData.pm > README
+
+If this is still here it means the programmer was too lazy to create the readme file.
+
+You can create it now by using the command shown above from this directory.
+
+At the very least you should be able to use this set of instructions
+to install the module...
+
+perl Build.PL
+./Build
+./Build test
+./Build install
+
+
+If you are on a windows box you should use 'nmake' rather than 'make'.
Index: /branches/jhoblitt/Nebulous/TODO
===================================================================
--- /branches/jhoblitt/Nebulous/TODO	(revision 2784)
+++ /branches/jhoblitt/Nebulous/TODO	(revision 2784)
@@ -0,0 +1,17 @@
+cache some database tables (updated at some interval) 
+reuse/cache database statement handles
+code tiding/consistency
+volume handling for instance placement
+file creation retrying
+server config file
+make Apache::DBI behave
+WSDL
+split SOAP out of Client.pm
+error string support
+
+distant future
+--
+volume size/% in-use discovery
+server side per client lock tracking
+client side lookup table of filehandles/locks
+Perl only?, scoped lock release/filehandle destruction
Index: /branches/jhoblitt/Nebulous/Todo
===================================================================
--- /branches/jhoblitt/Nebulous/Todo	(revision 2784)
+++ /branches/jhoblitt/Nebulous/Todo	(revision 2784)
@@ -0,0 +1,5 @@
+TODO list for Perl module Poi::PixelData
+
+- Nothing yet
+
+
Index: /branches/jhoblitt/Nebulous/bootstrap-moa-mp1.sh
===================================================================
--- /branches/jhoblitt/Nebulous/bootstrap-moa-mp1.sh	(revision 2784)
+++ /branches/jhoblitt/Nebulous/bootstrap-moa-mp1.sh	(revision 2784)
@@ -0,0 +1,1 @@
+perl Build.PL -http_conf /usr/local/apache/conf/httpd.conf -apxs /usr/local/apache/bin/apxs -httpd /usr/local/apache/bin/httpd 
Index: /branches/jhoblitt/Nebulous/docs/apache_setup.txt
===================================================================
--- /branches/jhoblitt/Nebulous/docs/apache_setup.txt	(revision 2784)
+++ /branches/jhoblitt/Nebulous/docs/apache_setup.txt	(revision 2784)
@@ -0,0 +1,37 @@
+- add the "idata" group to /etc/group
+- configure Apache to run as the idata group. 
+
+#
+# CGI interface
+#
+
+ScriptAlias /idata /usr/local/apache/perl/imageserver.pl
+
+<Directory "/usr/local/apache/perl">
+        AllowOverride None
+        Options None
+        Order allow,deny
+        Allow from all
+</Directory>
+
+PerlModule Apache::PerlRun
+<Location /perl>
+        SetHandler perl-script
+        PerlHandler Apache::PerlRun
+        Options ExecCGI
+        allow from all
+        PerlSendHeader On
+</Location>
+
+#
+# PS::IPP::IData::Apache interface
+#
+
+PerlModule Apache::DBI
+PerlModule PS::IPP::IData::Apache
+
+<Location /idata>
+        SetHandler perl-script
+        PerlHandler PS::IPP::IData::Apache
+</Location>
+
Index: /branches/jhoblitt/Nebulous/docs/c_api.h
===================================================================
--- /branches/jhoblitt/Nebulous/docs/c_api.h	(revision 2784)
+++ /branches/jhoblitt/Nebulous/docs/c_api.h	(revision 2784)
@@ -0,0 +1,24 @@
+/*
+ * Copyright (C) 2004 Joshua Hoblitt
+ *
+ * $Id: c_api.h,v 1.1.1.1 2004-12-22 02:16:23 jhoblitt Exp $
+ */
+
+// should there be an argument for "suggested" storage location?
+int add( const char *ext_id, const char *data, int size )
+
+int delete( const char *ext_id )
+
+// should there be an argument for "suggested" storage location?
+int update( const char *ext_id, const char *data, int size )
+
+// should there be an argument for "suggested" storage location?
+int move( const char *old_ext_id, const char *new_ext_id )
+
+// open returns a fd 
+int open( const char *ext_id )
+
+int close( const char *ext_id )
+
+// maybe open and close are a bad idea, perhaps...
+int read( const char *ext_id, char *data )
Index: /branches/jhoblitt/Nebulous/docs/calc_gid.c
===================================================================
--- /branches/jhoblitt/Nebulous/docs/calc_gid.c	(revision 2784)
+++ /branches/jhoblitt/Nebulous/docs/calc_gid.c	(revision 2784)
@@ -0,0 +1,5 @@
+#include <stdio.h>
+
+main () {
+    printf( "%d\n", 'i'+'m'+'a'+'g'+'e'+'d'+'a'+'t'+'a' + 's'+'e'+'r'+'v'+'e'+'r' );
+}
Index: /branches/jhoblitt/Nebulous/docs/database_setup.txt
===================================================================
--- /branches/jhoblitt/Nebulous/docs/database_setup.txt	(revision 2784)
+++ /branches/jhoblitt/Nebulous/docs/database_setup.txt	(revision 2784)
@@ -0,0 +1,147 @@
+# Copyright (C) 2004  Joshua Hoblitt
+#
+# $Id: database_setup.txt,v 1.1.1.1 2004-12-22 02:16:23 jhoblitt Exp $
+
+### Database Setup
+
+DROP DATABASE IF EXISTS pixel;
+CREATE DATABASE pixel;
+INSERT INTO db VALUES ('%','pixel','pixel','y','y','y','y','y','y','y','y','y','y','y','y');
+INSERT INTO user
+(Host,User,Password,Select_priv,Insert_priv,Update_priv,Delete_priv,Create_priv,Drop_priv,Index_priv,Alter_priv,Show_db_priv)
+VALUES ('%','pixel',password('pixel'),'y','y','y','y','y','y','y','y','y');
+INSERT INTO user
+(Host,User,Password,Select_priv,Insert_priv,Update_priv,Delete_priv,Create_priv,Drop_priv,Index_priv,Alter_priv,Show_db_priv)
+VALUES ('localhost','pixel',password('pixel'),'y','y','y','y','y','y','y','y','y');
+FLUSH PRIVILEGES;
+
+USE pixel;
+
+### MySQL 4.0
+
+DROP TABLE IF EXISTS storage_object;
+CREATE TABLE storage_object (
+    so_id BIGINT NOT NULL AUTO_INCREMENT,
+    ext_id VARCHAR(255) BINARY NOT NULL UNIQUE,
+    class_id TINYINT NOT NULL,
+    comment VARCHAR(255),
+    read_lock TINYINT DEFAULT 0 NOT NULL,
+    write_lock ENUM( 'write' ),
+    epoch TIMESTAMP NOT NULL,
+    PRIMARY KEY(so_id),
+    KEY(ext_id(64)),
+    KEY(class_id)
+) ENGINE=innodb;
+
+DROP TABLE IF EXISTS instance;
+CREATE TABLE instance ( 
+    ins_id BIGINT NOT NULL AUTO_INCREMENT,
+    so_id BIGINT NOT NULL,
+    uri VARCHAR(255) BINARY NOT NULL UNIQUE,
+    sha1sum CHAR(40),
+    assigned_location BOOL,
+    epoch TIMESTAMP NOT NULL,
+    atime TIMESTAMP NOT NULL,
+    PRIMARY KEY(ins_id),
+    KEY(so_id),
+    KEY(uri(64))
+) ENGINE=innodb;
+
+DROP TABLE IF EXISTS lock_record;
+CREATE TABLE lock_record (
+        so_id BIGINT NOT NULL,
+        type ENUM( 'read', 'write' ) NOT NULL,
+        epoch TIMESTAMP,
+        KEY(so_ID)
+    ) ENGINE=innodb;
+
+DROP TABLE IF EXISTS volume;
+CREATE TABLE volume (
+        vol_id INT NOT NULL AUTO_INCREMENT,
+        name VARCHAR(255) UNIQUE BINARY NOT NULL,
+        uri VARCHAR(255) BINARY NOT NULL,
+        key(vol_id)
+    ) ENGINE=innodb;
+
+DROP TABLE IF EXISTS class;
+CREATE TABLE class (
+        class_id TINYINT NOT NULL AUTO_INCREMENT,
+        instance TINYINT NOT NULL,
+        priority TINYINT,
+        comment VARCHAR(255),
+        key(class_id)
+    ) ENGINE=innodb;
+
+### MySQL 4.1
+
+DROP TABLE IF EXISTS storage_object;
+CREATE TABLE storage_object (
+        so_id BIGINT NOT NULL AUTO_INCREMENT,
+        ext_id VARCHAR(255) UNIQUE BINARY NOT NULL,
+        comment VARCHAR(255),
+        epoch TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
+        PRIMARY KEY(so_id),
+        KEY(ext_id(20))
+    ) ENGINE=innodb;
+
+
+DROP TABLE IF EXISTS instance;
+CREATE TABLE instance ( 
+        ins_id BIGINT NOT NULL AUTO_INCREMENT,
+        so_id BIGINT NOT NULL,
+        uri VARCHAR(255) BINARY NOT NULL,
+        sha1sum CHAR(40) ASCII,
+        read_lock TINYINT NOT NULL,
+        write_lock ENUM( 'write', 'wait' ),
+        atime TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+        PRIMARY KEY(ins_id),
+        KEY(so_id)
+    ) ENGINE=innodb;
+
+DROP TABLE IF EXISTS lock_record;
+CREATE TABLE lock_record (
+        ins_id BIGINT NOT NULL,
+        type ENUM( 'read', 'write', 'wait' ) NOT NULL,
+        epoch TIMESTAMP DEFAULT CURRENT_TIMESTAMP
+    ) ENGINE=innodb;
+
+        epoch TIMESTAMP,
+
+### Initialization
+
+# NFS
+
+INSERT INTO volume VALUES (1,'po01','file:/tmp/po01');
+INSERT INTO volume VALUES (2,'po02','file:/tmp/po02');
+INSERT INTO volume VALUES (3,'po03','file:/tmp/po03');
+INSERT INTO volume VALUES (4,'po04','file:/tmp/po04');
+INSERT INTO volume VALUES (5,'po05','file:/tmp/po05');
+INSERT INTO volume VALUES (6,'po06','file:/tmp/po06');
+INSERT INTO volume VALUES (7,'po07','file:/tmp/po07');
+INSERT INTO volume VALUES (8,'po08','file:/tmp/po08');
+
+INSERT INTO class VALUES (1,3,0,'raw image data');
+
+### Example queries
+
+LOAD DATA INFILE '/home/moa/jhoblitt/tmp/storage_objects.txt' INTO TABLE storage_object; 
+LOAD DATA INFILE '/home/moa/jhoblitt/tmp/instances.txt' INTO TABLE instance; 
+
+SELECT * FROM storage_object LEFT JOIN instance ON storage_object.so_id=instance.so_id WHERE storage_object.ext_id = "foobarbaz1.fits";
+SELECT * FROM storage_object LEFT JOIN instance USING (so_id) WHERE storage_object.ext_id = "foobarbaz1.fits";
+
+SET AUTOCOMMIT = 0;
+BEGIN;
+UPDATE instance SET read_lock=read_lock+1 WHERE ins_id=1;
+INSERT INTO lock_record values (1, 'read', NULL);
+COMMIT;
+
+SET AUTOCOMMIT = 0;
+BEGIN;
+INSERT INTO storage_object values (NULL, 'myfooedbar.fits', 'mr. wizard made this', NULL);
+INSERT INTO instance values (NULL, LAST_INSERT_ID(), 'file://tmp/bat/man/12345a', 'da39a3ee5e6b4b0d3255bfef95601890afd80709',0, NULL, NULL);
+COMMIT;
+
+IF select write_lock from instance WHERE id=1 THEN
+ELSE
+ENDIF
Index: /branches/jhoblitt/Nebulous/docs/design.txt
===================================================================
--- /branches/jhoblitt/Nebulous/docs/design.txt	(revision 2784)
+++ /branches/jhoblitt/Nebulous/docs/design.txt	(revision 2784)
@@ -0,0 +1,163 @@
+# Copryight (C) 2004  Joshua Hoblitt
+#
+# $Id: design.txt,v 1.1.1.1 2004-12-22 02:16:23 jhoblitt Exp $
+
+Terminology
+--
+    - Storage Object
+There is one Storage Object per resource (a file) that is being managed by the
+IPP Pixel Data Server.  A Storage Object maps an External Identifier (eg.
+file name) to a Storage ID that is internal to the IPP Pixel Data Server.  In
+addition, creation time and comments may be stored in a Storage Object.
+Storage Objects /do not/ contain the storage location of a resource as we need
+to allow for N copies of a resource to be stored.  The actual storage location
+of a resource is held in a Storage Object Instance, which maps to a Storage ID
+in a many-to-one relationship.
+
+    - [Storage Object] Instance
+A Storage Object Instance, or just Instance, represents a single copy of a
+resource being managed by the IPP Pixel Data Server. An Instance contains an
+Instance ID, the location of the resource as a URI, a checksum of the
+resource, a Storage ID, a read lock semaphore (unsigned integer), a write lock
+mutex (enum( 'write', 'wait', 'free' ) - is this better then using an int and
+constants?), and a UTC timestamp of the last modification date & time.
+
+    - Lock Record
+A Lock Record is used to double check the indicated lock state of an Instance
+and can be used to identify system failures.  It must contain the Instance ID
+of of the locked instance and the type of lock, eg, enum( 'read', 'write').
+
+Locking semantics
+--
+A lock is required to either read from or write to a Storage Object Instance
+being managed by the IPP Pixel Data Server.  Stated another way, locking
+granularity is managed on the level of Storage Object Instances instead of on
+the more abstract Storage Objects.  Although, from the client perspective,
+operations are carried out on a Storage Object.   The locking state of a
+Storage Object Instance is maintained by in the Storage Object Instance and a
+Lock Record.
+
+Two types of locks are used on an Instance, a semaphore is used to track read
+locks and an exclusive mutex is for write locking.
+
+    - Read lock
+In order to read from a Storage Object Instance the Read Lock semaphore must
+first be incremented by one and a Lock Record created.  After the read is
+completed the Lock Record must be deleted and the Read Lock semaphore
+decremented by one.  The incrementation of the Read Lock and Lock Record
+creation or the decrementation of the Read Lock and the Lock Record deletion
+must happen atomically.  A Read lock can not be aquired unless the Write mutex
+is in the state of 'free'.  If a Read lock can not be aquired, a client must
+not "spin lock" on the Instanace ID as it may be in the process of being
+removed.  Instead, the "spin lock" should be re-resolving the external
+identifier to a Storage ID, and the Storage ID to Storage Instances.
+
+    - Write lock
+In order to obtain a Write lock on a Storage Object Instance the Write Lock
+mutex must be in the state of 'free' and the Read lock semaphore must have a
+value of zero.  A client should either "spin lock" or fail if all the Lock
+mutex is set or the Read Semaphore has a non-zero value.  A Lock Record must
+be created atomically with the setting of the Write mutex.  The setting of the
+Write mutex and Lock Record insertion or the clearing of the Write mutex and
+the Lock Record deletion must happen atomically.  If the Write lock has a
+state of 'free' and the Read lock has a non-zero value, the Write lock may be
+set to the 'wait' state to prevent new Read locks from being aquired.
+
+Storage Object Operations
+--
+    - Add
+The resource must already be on disk (having been named through some sort of
+mechanism to generate new filenames).  A new Storage Object must be created with a new/unqiue  Storage ID and has
+an associated with an External Identifier.  At least one Instance must be
+created atomicaly at the same time.  Each new Instance must be initially created with a Write lock.
+The resource may then be written to it's storage location.  When the writing
+of the resource is completed the Write lock must be cleared as early as is
+feasible.  The association of an External Identifier must wait until Instance
+creation is completed
+
+    - Delete
+In order to remove a Storage Object, a Write lock must be obtained on
+/all/ associated Instances.  Then the resources are removed.  As each resource
+is deleted the corresponding Instance must be deleted.  After all Instances are
+removed the Storage Object must then be deleted.
+
+    - Update
+Storage Object Instances are not truly written to.  Instead, a new set of
+Storage Object Instances and a new Storage Object are created in parallel.
+
+Write locks must be aquired on all the orignal Intances of a Storage Object.
+Then an Add operation is performed to create a new Storage Object and
+associated Instances.  Then the External Identifier must be atomically moved
+from the original Storage Object to the new Storage Object.  After the
+External Identifier migration, the original Storage Object and it's Instances
+should be removed with a special case of the Delete operation (one that will
+operate on already held locks).  If the old Storage Object is merely
+abandoned, ie. it still exists but doesn't have an External Identifier, it and
+the associated Instances will be removed during the next Consistency Sweep.
+If a Delete is not peformed, the Write locks on the orignal Instances must be
+released. [use a dead Storage Object table or flag, similar to mark and sweep
+garbage collection?]
+
+    - Move
+This is the same operation as an Update except the new Instances are identical
+to the old.
+
+    - Open
+Sets a Read lock on an Instances of a Storage Object and determines it's
+storage location.  A Read lock must only be placed on one Instance of a
+Storage Object.  All Instances of a Storage Object should be iterated through
+in a "spin lock" until a Read lock is aquired.
+
+    - Close
+Releases a Read lock aquired with open.
+
+The lock must be released as soon as is feasible.
+
+Lock sweeping
+--
+In the event that a Storage Object operation fails to complete successfully
+stale locks will have to be identified and removed from the IPP Pixel Data
+Server Database.  This should be done periodically by comparing the entries
+in the Lock table to the list of active nodes maintained by the IPP
+Controller.  It should also happen as soon as possible after a node goes
+offline (triggered by the IPP Controller marking a node as offline?).  A sweep
+must be /completed/ before an offline node can be marked on-line.
+
+Once a node is determined to be offline all entries in the Lock table set by
+that node should be identified.   The locks on the Storage Object
+Instances pointed to by those entries should then be rolled back and Lock
+Record entries themselves must be removed from the lock table.
+
+Consistency sweeping
+--
+Periodically the IPP Pixel Data Server meta-data and Storage Object will need
+to be checked for sanity.  This would be similar to running fsck on a modern
+filesystem.  Consistency sweeping should include Lock sweeping and should be
+considered a superset.
+
+All Storage Objects must have at least once associated Instance.
+
+All Storage Object Instances must be associated with a Storage Object.
+
+All Storage Object Instances with a lock set must have the correct number of
+corresponding Lock Records.
+
+All Lock Records must be plausible, ie. the Instance pointed at must be in a
+corresponding state.
+
+Storage ID Allocation
+--
+Storage IDs are a signed 64-bit integer.  Negative values are considered
+reserved for future use.  Allocation should be incremental and, as we are
+unlikely to exceed 2^63 Storage Objects in the life time of the project, never
+reused.
+
+Allocated via [SQL auto-increment/stored procedure/app server?]
+
+Instance ID Allocation
+--
+Indentical to a Storage ID but exists in a unique namespace.
+
+Storage Location determination
+--
+???
Index: /branches/jhoblitt/Nebulous/docs/requirements.txt
===================================================================
--- /branches/jhoblitt/Nebulous/docs/requirements.txt	(revision 2784)
+++ /branches/jhoblitt/Nebulous/docs/requirements.txt	(revision 2784)
@@ -0,0 +1,23 @@
+# Copyright (C) 2004  Joshua Hoblitt
+#
+# $Id: requirements.txt,v 1.1.1.1 2004-12-22 02:16:23 jhoblitt Exp $
+
+- store all image data aquired during the first two years of PS1's
+  operations.  Approximately 10^8 files (OTA level).
+
+- injest new data from the summit at the same rate it is produced when
+  operating at a cadence of 40s between integrations.  Approximately 400Mb/s.
+
+- support phase 1 & 2 processing. x files per t_min
+
+- support phase 3 & 4 processing. x files per t_min
+
+- support accessing small blocks of a file (ie., seek)
+
+what are the science drivers for these 3?
+--
+- data corruption is guarenteed to be detectable.
+
+- relibably store xx.x% of files 
+
+- xx.x% of stored images must be retreivable in xs.
Index: /branches/jhoblitt/Nebulous/docs/setup.txt
===================================================================
--- /branches/jhoblitt/Nebulous/docs/setup.txt	(revision 2784)
+++ /branches/jhoblitt/Nebulous/docs/setup.txt	(revision 2784)
@@ -0,0 +1,33 @@
+
+- building and install the Perl modules (see: README)
+
+- create and initialize the database (see: docs/database_setup.txt)
+
+- setup the server
+
+As a CGI, work with either Apache 1.3.x or 2.0.x
+
+cp scripts/imagesever.pl /var/www/localhost/cgi-bin
+chmod 755 /var/www/localhost/cgi-bin/imageserver.pl
+touch /tmp/imageserver.log
+chmod 1777 /tmp/imageserver.log
+
+- test the imageserver.pl is working
+
+telnet localhost 80
+Trying 127.0.0.1...
+Connected to localhost.
+Escape character is '^]'.
+GET /cgi-bin/imageserver.pl HTTP/1.0
+
+HTTP/1.1 411 Length Required
+Date: Tue, 28 Sep 2004 21:15:58 GMT
+Server: Apache/2.0.49 (Gentoo/Linux)
+Content-Length: 0
+Connection: close
+Content-Type: text/plain; charset=ISO-8859-1
+
+Connection closed by foreign host.
+
+- if you get an HTTP 5xx code check your apache error_log
+
Index: /branches/jhoblitt/Nebulous/docs/tmp.txt
===================================================================
--- /branches/jhoblitt/Nebulous/docs/tmp.txt	(revision 2784)
+++ /branches/jhoblitt/Nebulous/docs/tmp.txt	(revision 2784)
@@ -0,0 +1,44 @@
+client
+--
+new
+
+new_file
+                new_file
+                open_file
+
+find_file
+    find_instance
+
+open_file
+    find_file
+
+delete_file
+                delete_file
+    nuke
+
+copy_file
+    open_file
+    new_file
+
+move_file
+    copy_file
+    delete_file
+
+replicate_file
+    open_file
+                replicate_file
+
+cull_file
+    find_instance
+    delete_instance
+
+delete_instance
+    nuke
+                delete_instance
+
+find_instance
+                find_instance
+
+# missing
+file_stat
+lock ?
Index: /branches/jhoblitt/Nebulous/lib/Nebulous/Client.pm
===================================================================
--- /branches/jhoblitt/Nebulous/lib/Nebulous/Client.pm	(revision 2784)
+++ /branches/jhoblitt/Nebulous/lib/Nebulous/Client.pm	(revision 2784)
@@ -0,0 +1,631 @@
+# Copyright (c) 2004  Joshua Hoblitt
+#
+# $Id: Client.pm,v 1.1.1.1 2004-12-22 02:16:23 jhoblitt Exp $
+
+package PS::IPP::IData::Client;
+
+use strict;
+use warnings;
+
+our $VERSION = '0.01';
+
+use File::Copy qw();
+use Log::Log4perl qw(get_logger :levels);
+use PS::IPP::IData::Client::Log;
+use PS::IPP::IData::Util qw( :standard );
+use Params::Validate qw( validate validate_pos SCALAR );
+use SOAP::Lite +trace => [qw( debug )];
+use Time::HiRes qw( sleep );
+use URI;
+
+use constant LOCK_INTERVAL  => 1;
+use constant LOCK_DEFAULT   => 10;    # default to a 10s lock timeout
+
+my $log = get_logger( "PS::IPP::IData::Client" );
+
+$SOAP::Constants::PATCH_HTTP_KEEPALIVE = 1;
+
+# TODO remove most of the logdies
+
+sub import {
+    my $class = shift;
+
+    $log->debug( "entered - args: @_" );
+
+	my %args = validate( @_,
+        {
+            trace => {
+                type        => SCALAR,
+                default     => $ERROR,
+                callbacks   => {
+                    'is valid level' => sub {
+                        defined $LEVELS{ lc $_[0] };
+                    },
+                },
+            },
+        },
+    );
+
+    $log->level( $LEVELS{ lc $args{ 'trace' } } );
+
+    return 1;
+}
+
+sub new {
+    my $class = shift;
+
+    my %args = validate( @_,
+        {
+            proxy   => {
+                type        => SCALAR,
+            },
+            uri     => {
+                type        => SCALAR,
+                optional    => 1,
+            },
+        },
+    );
+
+    $log->debug( "entered - args: @{[ %args ]}" );
+
+    my $lite = SOAP::Lite->new(
+            proxy => $args{ 'proxy' },
+            uri   => $args{ 'uri' } || "http://pan-starrs.ifa.hawaii.edu/PS/IPP/IData/Server",
+    );
+
+    $log->logdie( "can not create SOAP::Lite object" ) unless $lite;
+
+    my $self = bless( { server => $lite }, ref $class || $class );
+
+    $log->debug( "leaving" );
+
+    return $self;
+}
+
+sub create {
+    my $self = shift;
+
+    my ( $key, @params ) = validate_pos( @_,
+        {
+            type        => SCALAR,
+        },
+        {
+            type        => SCALAR,
+            callbacks   => {
+                'is + integer' => sub { $_[0] =~ /^\d+$/ },
+            },
+            default     => 0,
+        },
+        {
+            type        => SCALAR,
+            optional    => 1,
+        },
+        {
+            type        => SCALAR,
+            optional    => 1,
+        },
+    );
+
+    $log->debug( "entered - key: $key, params: @params" );
+
+    # how should already existing files be handled?
+
+    my $response = $self->{ 'server' }->create_object( $key, @params );
+    if ( $response->fault ) {
+        $log->error( $response->faultcode, " - ", $response->faultstring );
+        $log->debug( "leaving" );
+
+        return undef;
+    }
+
+    my $uri = $response->result;
+
+    $log->debug( "server allocated $uri" );
+
+#    unless( $self->lock( $key, "write"  )) {
+#        $log->debug( "can not lock object" );
+#        $log->debug( "leaving" );
+#
+#        return undef;
+#    }
+
+    my $fh;
+    eval {
+        $fh = _open_uri( $uri, '+<' );
+    };
+    $log->logdie( $@ ) if $@;
+
+    $log->debug( "leaving" );
+
+    return $fh;
+}
+
+sub replicate {
+    my $self = shift;
+
+    my ( $key, @params ) = validate_pos( @_,
+        {
+            type        => SCALAR,
+        },
+        {
+            # volume
+            type        => SCALAR,
+            optional    => 1,
+        },
+    );
+
+    $log->debug( "entered - key: $key, params: @params" );
+
+    my $response = $self->{ 'server' }->replicate_object( $key, @params );
+    if ( $response->fault ) {
+        $log->error( $response->faultcode, " - ", $response->faultstring );
+        $log->debug( "leaving" );
+
+        return undef;
+    }
+
+    # don't let anybody start reading the new instance before it's fully
+    # written
+    my $fh  = $self->open( $key, 'write' );
+    unless ( $fh ) {
+        $log->debug( "can't open $key" );
+        $log->debug( "leaving" );
+
+        return undef;
+    }
+
+    $log->debug( "server allocated new instance" );
+
+    my $uri = $response->result;
+
+    my $new_fh;
+    eval {
+        $new_fh = _open_uri( $uri, '>' );
+    };
+    $log->logdie( $@ ) if $@;
+
+    File::Copy::copy( $fh, $new_fh ) or $log->logdie( "can not copy instance $uri" );
+
+    $log->debug( "copied instance" );
+
+    close( $fh ) or $log->logdie( "can not close $key" );
+    close( $new_fh ) or $log->logdie( "can not close $key" );
+
+#    unless ($self->unlock( $key, "write" )) {
+#        $log->debug( "can not lock object" );
+#        $log->debug( "leaving" );
+#
+#        return undef;
+#    }
+
+    $log->debug( "leaving" );
+
+    return $uri;
+}
+
+sub cull {
+    my $self = shift;
+
+    my ( $key ) = validate_pos( @_,
+        {
+            type => SCALAR,
+        },
+    );
+
+    $log->debug( "entered - key: $key" );
+
+    # need some way to determine which is the best instance to remove
+    my $locations = $self->find_instances( $key );
+    unless ( $locations ) {
+            $log->debug( "no instances" );
+            $log->debug( "leaving" );
+
+            return undef;
+    }
+
+    if ( scalar @{ $locations } < 2 ) {
+        $log->debug( "can not cull - not enough instances" );
+        $log->debug( "leaving" );
+
+        return undef;
+    }
+
+    # make sure nobody is reading while we are deleting
+    unless ($self->lock( $key, "write" )) {
+        $log->debug( "can not lock object" );
+        $log->debug( "leaving" );
+
+        return undef;
+    }
+
+    my $status = $self->delete_instance( $locations->[0] );
+
+    unless ($self->unlock( $key, "write" )) {
+        $log->debug( "can not lock object" );
+        $log->debug( "leaving" );
+
+        return undef;
+    }
+
+    $log->debug( "leaving" );
+
+    return $status;
+}
+
+sub lock {
+    my $self = shift;
+
+    my ( $key, $type, $timeout ) = validate_pos( @_,
+        {
+            type        => SCALAR,
+        },
+        {
+            type        => SCALAR,
+            callbacks   => {
+                'is read or write' => sub { $_[0] =~ /^(?:read|write)$/ },
+            },
+        },
+        {
+            type        => SCALAR,
+            default     => LOCK_DEFAULT,
+            optional    => 1,
+            callbacks   => {
+                'is + integer' => sub { $_[0] =~ /^\d+$/ },
+            },
+        },
+    );
+
+    my $endtime = time() + $timeout;
+
+    $log->debug( "entered - key: $key, type: $type, timeout: $timeout" );
+
+    my $locked;
+
+    while ( time() < $endtime ) {
+        $log->debug( "trying to get a $type lock..." );
+
+        my $response = $self->{ 'server' }->lock_object( $key, $type );
+        if ( $response->fault ) {
+            $log->error( $response->faultcode, " - ", $response->faultstring );
+            $log->debug( "leaving" );
+            
+            # don't bother to spin lock if the object doesn't exist
+            if ( $response->faultstring =~ /storage object does not exist/ ) {
+                return undef;
+            }
+
+            next;
+        }
+
+        $locked = $response->result;
+        if ( $locked ) {
+            $log->debug( "got a $type lock" );
+            last;
+        }
+    } continue {
+        $log->debug( "sleeping..." );
+        sleep LOCK_INTERVAL;
+    }
+
+    $log->debug( "can not get a lock" ) unless $locked;
+
+    $log->debug( "leaving" );
+    
+    return $locked ? 1 : undef;
+}
+
+sub unlock {
+    my $self = shift;
+
+    my ( $key, $type ) = validate_pos( @_,
+        {
+            type        => SCALAR,
+        },
+        {
+            type        => SCALAR,
+            callbacks   => {
+                'is read or write' => sub { $_[0] =~ /^(?:read|write)$/ },
+            },
+        },
+    );
+
+    $log->debug( "entered - key: $key, type: $type" );
+
+    my $unlocked;
+
+    $log->debug( "trying to release a $type lock..." );
+
+    my $response = $self->{ 'server' }->unlock_object( $key, $type );
+    if ( $response->fault ) {
+        $log->error( $response->faultcode, " - ", $response->faultstring );
+        $log->debug( "leaving" );
+
+        return undef;
+    }
+
+    $unlocked = $response->result;
+    if ( $unlocked ) {
+        $log->debug( "released a $type lock" );
+    } else {
+        $log->debug( "can not release $type lock" );
+    }
+
+    $log->debug( "leaving" );
+    
+    return $unlocked ? 1 : undef;
+}
+
+sub find_instances {
+    my $self = shift;
+
+    my ( $key, @params ) = validate_pos( @_,
+        {
+            type        => SCALAR,
+        },
+        {
+            #volume
+            type        => SCALAR,
+            optional    => 1,
+        },
+    );
+
+    $log->debug( "entered - key: $key" );
+
+    my $response = $self->{ 'server' }->find_instances( $key, @params );
+    if ( $response->fault ) {
+        $log->error( $response->faultcode, " - ", $response->faultstring );
+        $log->debug( "leaving" );
+
+        return undef;
+    }
+
+    my $uris = $response->result;
+
+    $log->debug( "server found: @$uris" );
+
+#    foreach my $path ( @{ $uris } ) {
+#        $path = _get_file_path( $path );
+#    }
+
+    $log->debug( "leaving" );
+
+    return $uris;
+}
+
+sub find {
+    my $self = shift;
+
+    my ( $key ) = validate_pos( @_,
+        {
+            type        => SCALAR,
+        },
+    );
+
+    $log->debug( "entered - key: $key" );
+
+    my $locations = $self->find_instances( $key );
+
+    my $path;
+    eval {
+        $path = _get_file_path( $locations->[0] );
+    };
+    $log->logdie( $@ ) if $@;
+
+    $log->debug( "leaving" );
+
+    return $path;
+}
+
+sub open {
+    my $self = shift;
+
+    my ( $key, $type ) = validate_pos( @_,
+        {
+            type        => SCALAR,
+        },
+        {
+            type        => SCALAR,
+            callbacks   => {
+                'is read or write' => sub { $_[0] =~ /^(?:read|write)$/ },
+            },
+        },
+    );
+
+    $log->debug( "entered - key: $key, type: $type" );
+
+    my $fh;
+
+    # need to figure out which is the most optimal copy to open
+    my $locations = $self->find_instances( $key );
+
+    my $path;
+    eval {
+        $path = _get_file_path( $locations->[0] );
+    };
+    $log->logdie( $@ ) if $@;
+
+    unless ( $path ) {
+        $log->debug( "no instances" );
+
+        if ( $type eq 'write' ) {
+            $log->debug( "creating a new storage object" );
+
+            my $ret = $self->create( $key );
+
+            $log->debug( "leaving" );
+
+            return $ret;
+        } else {
+            $log->debug( "leaving" );
+            return undef;
+        }
+    }
+
+    if ( $type eq 'write' ) {
+#        unless ($self->lock( $key, $type )) {
+#            $log->debug( "can not lock object" );
+#            $log->debug( "leaving" );
+#
+#            return undef;
+#        }
+
+        eval {
+            $fh = _get_filehandle( $path, '+<' );
+        };
+        $log->logdie( $@ ) if $@;
+    } elsif ( $type eq 'read' ) {
+#            unless ($self->lock( $key, $type )) {
+#            $log->debug( "can not lock object" );
+#            $log->debug( "leaving" );
+#
+#            return undef;
+#        }
+
+        eval {
+            $fh = _get_filehandle( $path, '<' );
+        };
+        $log->logdie( $@ ) if $@;
+    }
+
+    $log->debug( "leaving" );
+
+    return $fh;
+}
+
+sub delete {
+    my $self = shift;
+
+    my ( $key ) = validate_pos( @_,
+        {
+            type => SCALAR,
+        },
+    );
+
+    $log->debug( "entered - key: $key" );
+
+    unless ($self->lock( $key, "write" )) {
+        $log->debug( "can not lock object" );
+        $log->debug( "leaving" );
+
+        return undef;
+    }
+
+    my $locations = $self->find_instances( $key );
+        
+    foreach my $uri ( @$locations ) {
+        $self->delete_instance( $uri );
+    }
+
+    # lock is implicitly removed when the last storage object is deleted
+
+    $log->debug( "leaving" );
+
+    return 1;
+}
+
+sub copy {
+    my $self = shift;
+
+    my ( $key, $new_key ) = validate_pos( @_,
+        {
+            type => SCALAR,
+        },
+        {
+            type => SCALAR,
+        },
+    );
+
+    $log->debug( "entered - key: $key, new_key: $new_key" );
+
+    my $fh      = $self->open( $key, 'read' );
+    my $new_fh  = $self->create( $new_key );
+
+    File::Copy::copy( $fh, $new_fh ) or $log->logdie( "can not copy object $key" );
+
+    close( $fh ) or $log->logdie->( "can not close $key" );
+    close( $new_fh ) or $log->logdie->( "can not close $new_key" );
+
+    unless ($self->unlock( $key, "read" )) {
+        $log->debug( "can not lock object" );
+        $log->debug( "leaving" );
+
+        return undef;
+    }
+
+    unless ($self->unlock( $new_key, "write" )) {
+        $log->debug( "can not lock object" );
+        $log->debug( "leaving" );
+
+        return undef;
+    }
+
+    $log->debug( "leaving" );
+
+    return 1;
+}
+
+sub move {
+    my $self = shift;
+
+    my ( $key, $new_key ) = validate_pos( @_,
+        {
+            type => SCALAR,
+        },
+        {
+            type => SCALAR,
+        },
+    );
+
+    $log->debug( "entered - key: $key, new_key: $new_key" );
+
+    $self->copy( $key, $new_key );
+    $self->delete( $key );
+
+    $log->debug( "leaving" );
+
+    return 1;
+}
+
+sub delete_instance {
+    my $self = shift;
+
+    my ( $uri ) = validate_pos( @_,
+        {
+            type => SCALAR,
+        },
+    );
+
+    $log->debug( "entered - uri: $uri" );
+
+    # it is being assumed here that it is better to have files on disk and not in
+    # the database then the inverse.
+
+    my $path;
+    eval {
+        $path = _get_file_path( $uri );
+    };
+    $log->logdie( $@ ) if $@;
+
+    my $response = $self->{ 'server' }->delete_instance( $uri );
+    if ( $response->fault ) {
+        $log->error( $response->faultcode, " - ", $response->faultstring );
+        $log->debug( "leaving" );
+
+        return undef;
+    }
+
+    $log->debug( "server deleted instance" );
+
+    eval {
+        _nuke_file( $path );
+    };
+    $log->logdie( $@ ) if $@;
+
+    $log->debug( "leaving" );
+
+    return $uri;
+}
+
+1;
+
+__END__
Index: /branches/jhoblitt/Nebulous/lib/Nebulous/Client/HTTP.pm
===================================================================
--- /branches/jhoblitt/Nebulous/lib/Nebulous/Client/HTTP.pm	(revision 2784)
+++ /branches/jhoblitt/Nebulous/lib/Nebulous/Client/HTTP.pm	(revision 2784)
@@ -0,0 +1,26 @@
+# Copyright (c) 2004  Joshua Hoblitt
+#
+# $Id: HTTP.pm,v 1.1.1.1 2004-12-22 02:16:24 jhoblitt Exp $
+
+package PS::IPP::IData::Client::HTTP;
+
+use strict;
+
+our $VERSION = '0.01';
+
+use base qw( SOAP::Lite );
+
+sub call {
+    my $result = $_[0]->SUPER::call( @_ );
+
+    if (( UNIVERSAL::isa( $result, 'SOAP::SOM' )) && $result->fault ) {
+
+        return $result->faultstring;
+    }
+
+    return $result;
+}
+
+1;
+
+__END__
Index: /branches/jhoblitt/Nebulous/lib/Nebulous/Client/Log.pm
===================================================================
--- /branches/jhoblitt/Nebulous/lib/Nebulous/Client/Log.pm	(revision 2784)
+++ /branches/jhoblitt/Nebulous/lib/Nebulous/Client/Log.pm	(revision 2784)
@@ -0,0 +1,33 @@
+# Copyright (c) 2004  Joshua Hoblitt
+#
+# $Id: Log.pm,v 1.1.1.1 2004-12-22 02:16:24 jhoblitt Exp $
+
+package PS::IPP::IData::Client::Log;
+
+use strict;
+
+our $VERSION = '0.01';
+
+use Log::Log4perl;
+
+my $conf = q(
+    log4perl.category.PS.IPP.IData.Client = DEBUG, Screen
+#log4perl.category.PS.IPP.IData.Client = DEBUG, CLIENTLOGFILE, Screen
+
+    log4perl.appender.Screen        = Log::Log4perl::Appender::Screen
+    log4perl.appender.Screen.stderr = 0
+    log4perl.appender.Screen.layout = Log::Log4perl::Layout::PatternLayout
+    log4perl.appender.Screen.layout.ConversionPattern = %d | %H | %p | %M - %m%n
+
+    log4perl.appender.CLIENTLOGFILE           = Log::Log4perl::Appender::File
+    log4perl.appender.CLIENTLOGFILE.filename  = /tmp/idata_client.log
+    log4perl.appender.CLIENTLOGFILE.mode      = append
+    log4perl.appender.CLIENTLOGFILE.layout    = Log::Log4perl::Layout::PatternLayout
+    log4perl.appender.CLIENTLOGFILE.layout.ConversionPattern = %d{yyyy-MM-dd HH:mm:ss} | %H | %p | %M - %m%n
+);
+
+Log::Log4perl::init( \$conf );
+
+1;
+
+__END__
Index: /branches/jhoblitt/Nebulous/lib/Nebulous/Server.pm
===================================================================
--- /branches/jhoblitt/Nebulous/lib/Nebulous/Server.pm	(revision 2784)
+++ /branches/jhoblitt/Nebulous/lib/Nebulous/Server.pm	(revision 2784)
@@ -0,0 +1,660 @@
+# Copyright (c) 2004  Joshua Hoblitt
+#
+# $Id: Server.pm,v 1.1.1.1 2004-12-22 02:16:24 jhoblitt Exp $
+
+package PS::IPP::IData::Server;
+
+use strict;
+use warnings;
+no warnings qw( uninitialized );
+
+our $VERSION = '0.01';
+
+use DBI;
+use PS::IPP::IData::Util qw( :standard );
+use PS::IPP::IData::Server::Log;
+use PS::IPP::IData::Server::SQL;
+use Log::Log4perl;
+use Params::Validate qw( validate_pos SCALAR );
+
+my %class = (
+    raw => 1,
+);
+
+my $log;
+my $sql;
+my $db;
+
+sub setup {
+    my $self = shift;
+
+    my ( $dsn, $user, $passwd ) = validate_pos( @_,
+        {
+            type        => SCALAR,
+        },
+        {
+            type        => SCALAR,
+        },
+        {
+            type        => SCALAR,
+        },
+    );
+
+    $log = Log::Log4perl::get_logger( "PS::IPP::IData::Server" );
+    $sql = PS::IPP::IData::Server::SQL->new;
+
+    $log->debug( "entered - @_" );
+
+    eval {
+        $db = DBI->connect(
+            $dsn,
+            $user,
+            $passwd,
+            {
+                RaiseError => 1,
+                PrintError => 0,
+                AutoCommit => 0,
+            },
+        );
+
+        $db->do( $sql->set_transaction_model );
+
+        $db->commit;
+    };
+    if ( $@ ) {
+        $db->rollback if $db;
+        $log->logdie( "database error: $@" );
+    }
+
+    $log->debug( "connected to database: ", sub { $db->data_sources; } );
+
+    $log->debug( "leaving" );
+
+    return 1;
+}
+
+sub create_object {
+    my $self = shift;
+
+    $log->debug( "entered - @_" );
+
+    my ( $key, $class, $volume, $comment ) = validate_pos( @_,
+        {
+            type        => SCALAR,
+        },
+        {
+            type        => SCALAR,
+            callbacks   => {
+                'is + integer' => sub { $_[0] =~ /^\d+$/ },
+            },
+            default     => 0,
+        },
+        {
+            type        => SCALAR,
+            optional    => 1,
+        },
+        {
+            type        => SCALAR,
+            optional    => 1,
+        },
+    );
+
+    # TODO do some intelligent volume allocated 
+    # add hooks to make this decision based on avaiable space
+    $volume = _get_storage_volume( $volume );
+
+    # check that the comment isn't too long
+    # check that the class requested is valid
+    # check that the volume requested is valid
+
+    my $uri;
+    my $ins_id;
+
+    eval {
+        {
+            # create storage_object
+            my $query = $db->prepare( $sql->new_object ); 
+            $query->execute( $key, $class, $comment );
+        }
+
+        {
+            # create instance with no URI
+            my $query = $db->prepare( $sql->new_object_instance );
+            $query->execute;
+        }
+
+        {
+            # get instance ID
+            my $query = $db->prepare( $sql->last_insert_id );
+            $query->execute;
+            ( $ins_id ) = $query->fetchrow_array;
+        }
+    };
+    if ( $@ ) {
+        $db->rollback;
+        $log->logdie( "database error: $@" );
+    }
+
+    $uri = "$volume/$key.$ins_id";
+
+    $log->debug( "generated uri $uri");
+
+    my $path = _get_file_path( $uri );
+
+    # TODO add some stuff here to retry if unsucessful
+    eval {
+        _create_empty_file( $path );
+    };
+    if ( $@ ) {
+        $db->rollback;
+        $log->logdie( $@ );
+    }
+
+    eval {
+        {
+            my $query = $db->prepare( $sql->update_instance_uri );
+            $query->execute( $uri, $ins_id );
+        }
+
+        $db->commit;
+    };
+    $log->logdie( "database error: $@" ) if $@;
+
+    $log->debug( "leaving" );
+
+    return $uri;
+}
+
+sub replicate_object {
+    my $self = shift;
+
+    $log->debug( "entered - @_" );
+
+    my ( $key, $volume ) = validate_pos( @_,
+        {
+            type        => SCALAR,
+        },
+        {
+            type        => SCALAR,
+            optional    => 1,
+        },
+    );
+
+    # TODO
+    $volume = _get_storage_volume();
+
+    my $path;
+    my $uri;
+
+    my $so_id;
+    my $ins_id;
+    eval {
+        {
+            my $query = $db->prepare( $sql->get_object_instances );
+            my $rows = $query->execute( $key );
+
+            unless ( $rows > 0 ) {
+                $db->rollback;
+                $log->logdie( "storage object does not exist" );
+            }
+
+            $so_id = $query->fetchrow_hashref->{ 'so_id' };
+        }
+
+        {
+            my $query = $db->prepare( $sql->new_instance );
+            $query->execute( $so_id );
+        }
+
+        {
+            my $query = $db->prepare( $sql->last_insert_id );
+            $query->execute();
+
+            ( $ins_id ) = $query->fetchrow_array;
+        }
+
+        {
+            $uri = "$volume/$key.$ins_id";
+            $log->debug( "generated uri $uri");
+
+            my $query = $db->prepare( $sql->update_instance_uri );
+            $query->execute( $uri, $ins_id );
+        }
+    };
+    if ( $@ ) {
+        $db->rollback;
+        $log->logdie( "database error: $@" );
+    }
+
+
+    eval {
+        $path = _get_file_path( $uri );
+        $path = _create_empty_file( $path );
+    };
+    if ( $@ ) {
+        $db->rollback;
+        $log->logdie( $@ );
+    }
+
+    $db->commit;
+
+    $log->debug( "leaving" );
+
+    return $uri;
+}
+
+sub lock_object {
+    my $self = shift;
+
+    $log->debug( "entered - @_" );
+
+    my ( $key, $type ) = validate_pos( @_,
+        {
+            type        => SCALAR,
+        },
+        {
+            type        => SCALAR,
+            callbacks   => {
+                'is read or write' => sub { $_[0] =~ /^(?:read|write)$/ },
+            },
+        },
+    );
+
+    my $so_id;
+    my $read_lock;
+    my $write_lock;
+
+    eval {
+        # this will set update locks
+        my $query = $db->prepare( $sql->get_object_locks );
+        my $rows = $query->execute( $key );
+        unless ( $rows == 1 ) {
+            $log->logdie( "storage object does not exist" );
+        }
+
+        my $row = $query->fetchrow_hashref;
+
+        $so_id      = $row->{ 'so_id' };
+        $read_lock  = $row->{ 'read_lock' };
+        $write_lock = $row->{ 'write_lock' };
+    };
+    if ( $@ ) {
+        $db->rollback;
+        $log->logdie( "database error: $@" );
+    }
+
+    if ( $type eq 'write' ) {
+        # can't set a write lock twice and
+        # can't set a write lock if there are read locks
+        if ( $write_lock ) {
+            eval { $db->rollback; };
+            $log->logdie( "database error: $@" ) if $@;
+
+            $log->logdie( "can not write lock twice" );
+        }
+            
+        if ( $read_lock > 0 ) {
+            eval { $db->rollback; };
+            $log->logdie( "database error: $@" ) if $@;
+
+            $log->logdie( "can not write lock after read lock" );
+        }
+
+        eval {
+            my $query = $db->prepare( $sql->set_write_lock );
+            my $rows = $query->execute( $key );
+        
+            # if we affected more then one row something very bad has happened.
+            unless ( $rows == 1 ) {
+                $log->logdie( "affected row count is $rows instead of 1" );
+            }
+
+            $db->commit;
+        };
+        if ( $@ ) {
+            $db->rollback;
+            $log->logdie( "database error: $@" );
+        }
+
+        $log->debug( "leaving" );
+
+        return 1;
+    }
+
+    if ( $type eq 'read' ) {
+        # can't set a read lock if there's a write lock
+        if ( $write_lock ) {
+            eval { $db->rollback; };
+            $log->logdie( "database error: $@" ) if $@;
+
+            $log->logdie( "can not read lock after write lock" );
+        }
+
+        eval {
+            my $query = $db->prepare( $sql->increment_read_lock );
+            my $rows = $query->execute( $key );
+        
+            # if we affected more then one row something very bad has happened.
+            unless ( $rows == 1 ) {
+                $log->logdie( "affected row count is $rows instead of 1" );
+            }
+
+            $db->commit;
+        };
+        if ( $@ ) {
+            $db->rollback;
+            $log->logdie( "database error: $@" );
+        }
+
+        $log->debug( "leaving" );
+
+        return 1;
+    }
+
+    # never reached
+    $log->logdie( "this should never happen" );
+}
+
+sub unlock_object {
+    my $self = shift;
+
+    $log->debug( "entered - @_" );
+
+    my ( $key, $type ) = validate_pos( @_,
+        {
+            type        => SCALAR,
+        },
+        {
+            type        => SCALAR,
+            callbacks   => {
+                'is read or write' => sub { $_[0] =~ /^(?:read|write)$/ },
+            },
+        },
+    );
+
+    my $so_id;
+    my $read_lock;
+    my $write_lock;
+
+    eval {
+        # this will set update locks
+        my $query = $db->prepare( $sql->get_object_locks );
+        my $rows = $query->execute( $key );
+        unless ( $rows == 1 ) {
+            $log->logdie( "storage object does not exist" );
+        }
+
+        my $row = $query->fetchrow_hashref;
+
+        $so_id      = $row->{ 'so_id' };
+        $read_lock  = $row->{ 'read_lock' };
+        $write_lock = $row->{ 'write_lock' };
+    };
+    if ( $@ ) {
+        $db->rollback;
+        $log->logdie( "database error: $@" );
+    }
+
+    if ( $type eq 'write' ) {
+        # can't remove a write lock if it doesn't exist
+        if ( $read_lock ) {
+            eval { $db->rollback; };
+            $log->logdie( "database error: $@" ) if $@;
+           
+            $log->logdie( "can not have a write lock under a read lock" );
+        }
+        unless ( $write_lock ) {
+            eval { $db->rollback; };
+            $log->logdie( "database error: $@" ) if $@;
+
+            $log->logdie( "can not remove non-existant write lock" );
+        }
+
+        eval {
+            my $query = $db->prepare( $sql->delete_write_lock );
+            my $rows = $query->execute( $key );
+        
+            # if we affected more then one row something very bad has happened.
+            unless ( $rows == 1 ) {
+                $log->logdie( "affected row count is $rows instead of 1" );
+            }
+
+            $db->commit;
+        };
+        if ( $@ ) {
+            $db->rollback;
+            $log->logdie( "database error: $@" );
+        }
+
+        $log->debug( "leaving" );
+
+        return 1;
+    }
+
+    if ( $type eq 'read' ) {
+        # can't remove a read lock if there's a write lock and
+        # can't remove a read lock if there aren't any
+        if ( $write_lock ) {
+            eval { $db->rollback; };
+            $log->logdie( "database error: $@" ) if $@;
+           
+            $log->logdie( "can not have a read lock under a write lock" );
+        }
+           
+        if ( $read_lock == 0 ) {
+            eval { $db->rollback; };
+            $log->logdie( "database error: $@" ) if $@;
+           
+            $log->logdie( "can not remove non-existant read lock" );
+        }
+
+        eval {
+            my $query = $db->prepare( $sql->decrement_read_lock );
+            my $rows = $query->execute( $key );
+        
+            # if we affected more then one row something very bad has happened.
+            unless ( $rows == 1 ) {
+
+                $log->logdie( "affected row count is $rows instead of 1" );
+            }
+
+            $db->commit;
+        };
+        if ( $@ ) {
+            $db->rollback;
+            $log->logdie( "database error: $@" );
+        }
+
+        $log->debug( "leaving" );
+
+        return 1;
+    }
+
+    # never reached
+    $log->logdie( "this should never happen" );
+}
+
+
+sub find_instances {
+    my $self = shift;
+
+    $log->debug( "entered - @_" );
+
+    my ( $key, $volume ) = validate_pos( @_,
+        {
+            type        => SCALAR,
+        },
+        {
+            type        => SCALAR,
+            optional    => 1,
+        },
+    );
+
+    # TODO restrict search to a volume
+
+    my $query;
+
+    eval {
+        $query = $db->prepare( $sql->get_object_instances );
+        $query->execute( $key );
+    };
+    $log->logdie( "database error: $@" ) if $@;
+
+    my @locations;
+
+    while ( my $row = $query->fetchrow_hashref ) {
+        my $instance = $row->{ 'uri' };
+        push @locations, $instance if $instance;
+    }
+
+    $log->logdie( "no instances found" ) unless ( scalar @locations );
+
+    $log->debug( "leaving" );
+
+    return \@locations;
+}
+
+sub delete_instance {
+    my $self = shift;
+
+    $log->debug( "entered - @_" );
+
+    my ( $uri ) = validate_pos( @_,
+        {
+            type => SCALAR,
+        },
+    );
+
+    eval {
+        my $so_id;
+        my $instances;
+        # get so_id
+        {
+            my $query = $db->prepare( $sql->get_object_from_uri );
+            my $rows = $query->execute( $uri );
+
+            unless ( $rows > 0 ) {
+                $log->logdie( "no instance is associated with uri" );
+            }
+
+            $so_id = $query->fetchrow_hashref->{ 'so_id' };
+        }
+
+        {
+            my $query = $db->prepare( $sql->get_instance_count );
+            $query->execute( $so_id );
+
+            $instances = $query->fetchrow_hashref->{ 'count(ins_id)' };
+        }
+
+        # remove instance
+        {
+            my $query = $db->prepare( $sql->delete_instance );
+            my $rows = $query->execute( $uri );
+            
+            # if we affected something other then one row something very bad has happened
+            unless ( $rows == 1 ) {
+                $log->logdie( "affected row count is $rows instead of 1" );
+            }
+        }
+
+        # if we just deleted the last instance associated with a storage object
+        # remove it too
+        if ( $instances == 1 ) {
+            # we just removed the last instance
+            my $query = $db->prepare( $sql->delete_object );
+            my $rows = $query->execute( $so_id );
+
+            # if we affected something other then one row something very bad has happened
+            unless ( $rows == 1 ) {
+                $log->logdie( "affected row count is $rows instead of 1" );
+            }
+        }
+
+        $db->commit;
+    };
+    if ( $@ ) {
+        $db->rollback;
+        $log->logdie( "database error: $@" );
+    }
+
+    $log->debug( "leaving" );
+
+    return 1;
+}
+
+sub stat_object {
+    my $self = shift;
+
+    $log->debug( "entered - @_" );
+
+    my ( $key ) = validate_pos( @_,
+        {
+            type => SCALAR,
+        },
+    );
+
+    my $stat;
+    eval {
+        my $query = $db->prepare( $sql->get_object );
+        my $rows = $query->execute( $key );
+
+        unless ( $rows == 1 ) {
+            $log->logdie( "no storage object found" );
+        }
+
+        $stat = $query->fetchrow_arrayref;
+
+        $db->commit;
+    };
+    if ( $@ ) {
+        $db->rollback;
+        $log->logdie( "database error: $@" );
+    }
+
+    $log->debug( "leaving" );
+
+    return $stat;
+}
+
+sub _get_storage_volume {
+    $log->debug( "entered - @_" );
+
+    my $name = shift;
+
+    my $volume;
+    {
+        my $query;
+        eval {
+            # TODO cache this?
+            my $rows;
+            if ( $name ) {
+                $query = $db->prepare( qq{ SELECT uri FROM volume WHERE name = ? ORDER BY rand() LIMIT 1 } );
+                $rows = $query->execute( $name );
+            } else {
+                $query = $db->prepare( qq{ SELECT uri FROM volume ORDER BY rand() LIMIT 1 } );
+                $rows = $query->execute;
+            }
+
+            # there has to be atleast one storage volume
+            unless ( $rows > 0 ) {
+                $db->rollback;
+
+                $log->logdie( "affected row count is $rows instead of 1" );
+            } else {
+                $db->commit;
+            }
+        };
+        $log->logdie( "database error: $@" ) if $@;
+
+        $volume = $query->fetchrow_hashref->{ 'uri' };
+    }
+
+    $log->debug( "leaving" );
+
+    return $volume;
+}
+
+1;
+
+__END__
Index: /branches/jhoblitt/Nebulous/lib/Nebulous/Server/Apache.pm
===================================================================
--- /branches/jhoblitt/Nebulous/lib/Nebulous/Server/Apache.pm	(revision 2784)
+++ /branches/jhoblitt/Nebulous/lib/Nebulous/Server/Apache.pm	(revision 2784)
@@ -0,0 +1,21 @@
+# Copyright (C) 2004  Joshua Hoblitt
+#
+# $Id
+
+package PS::IPP::IData::Server::Apache;
+
+use strict;
+
+our $VERSION = 0.01;
+
+use SOAP::Transport::HTTP;
+
+my $server = SOAP::Transport::HTTP::Apache
+    ->dispatch_to( 'PS::IPP::IData::Server' )
+    ->options({ compress_threshold => 10 * 1024 });
+
+sub handler { $server->handler(@_); }
+
+1;
+
+__END__
Index: /branches/jhoblitt/Nebulous/lib/Nebulous/Server/Log.pm
===================================================================
--- /branches/jhoblitt/Nebulous/lib/Nebulous/Server/Log.pm	(revision 2784)
+++ /branches/jhoblitt/Nebulous/lib/Nebulous/Server/Log.pm	(revision 2784)
@@ -0,0 +1,27 @@
+# Copyright (c) 2004  Joshua Hoblitt
+#
+# $Id: Log.pm,v 1.1.1.1 2004-12-22 02:16:24 jhoblitt Exp $
+
+package PS::IPP::IData::Server::Log;
+
+use strict;
+
+our $VERSION = '0.01';
+
+use Log::Log4perl;
+                                                                                                                                          
+my $conf = q(
+    log4perl.category.PS.IPP.IData.Server = DEBUG, SERVERLOGFILE
+
+    log4perl.appender.SERVERLOGFILE           = Log::Log4perl::Appender::File
+    log4perl.appender.SERVERLOGFILE.filename  = /tmp/idata_server.log
+    log4perl.appender.SERVERLOGFILE.mode      = append
+    log4perl.appender.SERVERLOGFILE.layout    = Log::Log4perl::Layout::PatternLayout
+    log4perl.appender.SERVERLOGFILE.layout.ConversionPattern = %d{yyyy-MM-dd HH:mm:ss} | %H | %p | %M - %m%n
+);
+                                                                                                                          
+Log::Log4perl::init( \$conf );
+
+1;
+
+__END__
Index: /branches/jhoblitt/Nebulous/lib/Nebulous/Server/SQL.pm
===================================================================
--- /branches/jhoblitt/Nebulous/lib/Nebulous/Server/SQL.pm	(revision 2784)
+++ /branches/jhoblitt/Nebulous/lib/Nebulous/Server/SQL.pm	(revision 2784)
@@ -0,0 +1,107 @@
+# Copyright (c) 2004  Joshua Hoblitt
+#
+# $Id: SQL.pm,v 1.1.1.1 2004-12-22 02:16:24 jhoblitt Exp $
+
+package PS::IPP::IData::Server::SQL;
+
+use strict;
+
+our $VERSION = '0.01';
+
+use base qw( Class::Accessor::Fast );
+
+my %sql = (
+    set_transaction_model   => qq{
+        SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED
+    },
+    update_instance_uri => qq{
+        UPDATE instance SET uri = ? WHERE ins_id = ?
+    },
+    last_insert_id      => qq{
+        SELECT LAST_INSERT_ID()
+    },
+    new_object          => qq{
+        INSERT INTO storage_object
+        VALUES (NULL, ?, ?, ?, 0, NULL, NULL)
+    },
+    delete_object       => qq{
+        DELETE FROM storage_object
+        WHERE so_id = ?
+    },
+    new_object_instance  => qq{
+        INSERT INTO instance 
+        VALUES (NULL, LAST_INSERT_ID(), 'error', NULL, NULL, NULL, NULL)
+    },
+    new_instance        => qq{
+        INSERT INTO instance
+        VALUES (NULL, ?, 'error', NULL,NULL, NULL, NULL)
+    },
+    get_object          => qq{
+        SELECT *
+        FROM storage_object
+        WHERE ext_id = ?
+    },
+    get_object_locks    => qq{
+        SELECT so_id, read_lock, write_lock 
+        FROM storage_object
+        WHERE ext_id = ?
+        FOR UPDATE
+    },
+    set_write_lock      => qq{
+        UPDATE storage_object
+        SET write_lock = 'write'
+        WHERE ext_id = ?
+    },
+    delete_write_lock   => qq{
+        UPDATE storage_object
+        SET write_lock = NULL
+        WHERE ext_id = ?
+    },
+    increment_read_lock => qq{
+        UPDATE storage_object
+        SET read_lock = read_lock+1
+        WHERE ext_id = ?
+    },
+    decrement_read_lock => qq{
+        UPDATE storage_object
+        SET read_lock = read_lock-1
+        WHERE ext_id = ?
+    },
+    delete_instance     => qq{
+        DELETE FROM instance
+        WHERE uri = ?
+    },
+    get_object_from_uri   => qq{
+        SELECT so_id
+        FROM instance
+        WHERE uri = ?
+    },
+    get_instance_count   => qq{
+        SELECT count(ins_id)
+        FROM instance
+        WHERE so_id = ?
+    },
+    get_object_instances    => qq{
+        SELECT storage_object.so_id, uri, assigned_location
+        FROM storage_object
+        LEFT JOIN instance
+        USING (so_id)
+        WHERE storage_object.ext_id = ?
+    },
+);
+
+__PACKAGE__->mk_ro_accessors( keys %sql );
+
+sub new {
+    my $class = shift;
+
+    my $self = \%sql;
+
+    bless $self, $class || ref $class;
+
+    return $self;
+}
+
+1;
+
+__END__
Index: /branches/jhoblitt/Nebulous/lib/Nebulous/Util.pm
===================================================================
--- /branches/jhoblitt/Nebulous/lib/Nebulous/Util.pm	(revision 2784)
+++ /branches/jhoblitt/Nebulous/lib/Nebulous/Util.pm	(revision 2784)
@@ -0,0 +1,104 @@
+# Copyright (c) 2004  Joshua Hoblitt
+#
+# $Id: Util.pm,v 1.1.1.1 2004-12-22 02:16:24 jhoblitt Exp $
+
+package PS::IPP::IData::Util;
+
+use strict;
+
+our $VERSION = '0.01';
+
+use base qw( Exporter );
+
+use Log::Log4perl qw( :levels );
+use URI;
+
+my @symbols = qw(
+    %LEVELS
+    _nuke_file
+    _get_file_path
+    _get_filehandle
+    _open_uri
+    _create_empty_file
+);
+
+our @EXPORT_OK      = @symbols;
+our %EXPORT_TAGS    = ( standard => [ @symbols ] );
+
+our %LEVELS = (
+    off     => $OFF,
+    fatal   => $FATAL,
+    error   => $ERROR,
+    warn    => $WARN,
+    info    => $INFO,
+    debug   => $DEBUG,
+    all     => $ALL,
+);
+
+sub _nuke_file {
+    my $path = shift;
+
+    die "can't unlink file $path: it's a directory" 
+        if -d $path;
+    die "can't unlink file $path: it doesn't exist"
+        unless -e $path;
+    unlink $path or die "can't unlink file $path: $!";
+
+    return 1;
+}
+
+sub _get_file_path {
+    my $uri = shift;
+
+    my $path = URI->new( $uri )->path;
+
+    return $path;
+}
+
+sub _get_filehandle {
+    my ( $path, $flags ) = @_;
+
+    die "can't open file $path: file doesn't exist" 
+        unless -e $path;
+    CORE::open( my $fh, $flags, $path )
+        or die "can't open file $path: $!";
+
+    return $fh;
+}
+
+sub _open_uri {
+    my ( $uri , $flags ) = @_;
+
+    my $path = _get_file_path( $uri );
+    my $fh = _get_filehandle( $path, $flags );
+
+    return $fh;
+}
+
+sub _create_empty_file {
+    my $path = shift;
+
+    # perl's open() can't do an O_CREAT | O_EXCL
+    if ( -e $path ) {
+        die "file $path already exists";
+    }
+
+    my $fh;
+    unless ( open( $fh, '>', $path ) ) {
+        die "can not open $path: $!";
+    } 
+
+    unless ( close( $fh ) ) {
+        die "can not close $path: $!";
+    }
+
+    unless ( chmod 0664, $path ) {
+        die "can not chmod $path: $!";
+    }
+
+    return $path;
+}
+
+1;
+
+__END__
Index: /branches/jhoblitt/Nebulous/nebulous.wsdl
===================================================================
--- /branches/jhoblitt/Nebulous/nebulous.wsdl	(revision 2784)
+++ /branches/jhoblitt/Nebulous/nebulous.wsdl	(revision 2784)
@@ -0,0 +1,96 @@
+<?xml version="1.0"?>
+
+<definitions name="idata" targetNamespace="http://pan-starrs.ifa.hawaii.edu/PS/IPP/IData/Server">
+
+    <types>
+        <schema targetNamespace="http://pan-starrs.ifa.hawaii.edu/PS/IPP/IData/Server"
+            xmlns="http://www.w3.org/1999/XMLSchema">
+
+            <element name="create_object">
+                <complexType>
+                    <sequence>
+                        <element name="key"     type="string"/>
+                        <element name="class"   type="int"/>
+                        <element name="volume"  type="string"/>
+                        <element name="comment" type="string"/>
+                    </sequence>
+                </complexType>
+            </element>
+
+            <element name="create_objectResponse">
+                <complexType>
+                    <all>
+                        <element name="uri" type="string"/>
+                    </all>
+                </complexType>
+            </element>
+
+            <element name="create_objectFault">
+                <complexType>
+                    <all>
+                        <element name="errorMessage" type="string"/>
+                    </all>
+                </complexType>
+            </element>
+
+        </schema>
+    </types>
+
+    <message name="create_objectRequest">
+        <part name="body" element="esxsd:create_object"/>
+    </message>
+
+    <message name="create_objectResponse">
+        <part name="body" element="esxsd:create_objectResponse"/>
+    </message>
+
+    <portType name="create_objectPortType">
+        <operation name="create_object">
+            <input message ="es:create_objectRequest"/>
+            <output message="es:create_objectResponse"/>
+            <fault message ="es:create_objectFault"/>
+        </operation>
+    </portType>
+
+    <binding name="idataSoapBinding"
+        type="es:GetidatarPortType">
+
+        <soap:binding style="document"
+            transport="http://schemas.xmlsoap.org/soap/http"/>
+
+        <operation name="Getidata">
+            <soap:operation
+                soapAction="http://www.snowboard-info.com/EndorsementSearch"/>
+            <input>
+                <soap:body use="literal"
+                    namespace="http://schemas.snowboard-info.com/EndorsementSearch.xsd"/>
+            </input>
+
+            <output>
+                <soap:body use="literal"
+                    namespace="http://schemas.snowboard-info.com/EndorsementSearch.xsd"/>
+            </output>
+
+            <fault>
+                <soap:body use="literal"
+                    namespace="http://schemas.snowboard-info.com/EndorsementSearch.xsd"/>
+            </fault>
+        </operation>
+    </binding>
+
+
+  <service name="EndorsementSearchService">
+
+    <documentation>snowboarding-info.com Endorsement Service</documentation> 
+
+    <port name="GetEndorsingBoarderPort"
+          binding="es:EndorsementSearchSoapBinding">
+
+      <soap:address
+location="http://www.snowboard-info.com/EndorsementSearch"/>
+
+    </port>
+
+  </service>
+
+</definitions>
Index: /branches/jhoblitt/Nebulous/scripts/bench_test.pl
===================================================================
--- /branches/jhoblitt/Nebulous/scripts/bench_test.pl	(revision 2784)
+++ /branches/jhoblitt/Nebulous/scripts/bench_test.pl	(revision 2784)
@@ -0,0 +1,28 @@
+#!/usr/bin/perl
+
+use strict;
+use warnings;
+
+use lib "./lib";
+use Benchmark qw( cmpthese );
+
+use PS::IPP::IData::Client;
+
+my $idata = PS::IPP::IData::Client->new(
+    proxy   => 'http://localhost:80/idata'
+);
+
+my $key = "foobarbazbongbungbutufu";
+cmpthese( 10, {
+        'Image Server' => sub {
+            {
+                my $fh = $idata->create( $key );
+                close $fh;
+                $idata->unlock( $key, 'write' );
+            }
+
+            $idata->delete( $key );
+        },
+    },
+    'all',
+);
Index: /branches/jhoblitt/Nebulous/scripts/imageserver.pl
===================================================================
--- /branches/jhoblitt/Nebulous/scripts/imageserver.pl	(revision 2784)
+++ /branches/jhoblitt/Nebulous/scripts/imageserver.pl	(revision 2784)
@@ -0,0 +1,14 @@
+#!/usr/bin/perl -w
+
+# Copyright (C) 2004  Joshua Hoblitt
+#
+# $Id
+ 
+use strict;
+
+use SOAP::Transport::HTTP;
+use PS::IPP::IData::Server;
+   
+SOAP::Transport::HTTP::CGI
+    -> dispatch_to( 'PS::IPP::IData::Server' )
+    -> handle;
Index: /branches/jhoblitt/Nebulous/scripts/ptest.pl
===================================================================
--- /branches/jhoblitt/Nebulous/scripts/ptest.pl	(revision 2784)
+++ /branches/jhoblitt/Nebulous/scripts/ptest.pl	(revision 2784)
@@ -0,0 +1,51 @@
+#!/usr/bin/perl
+
+use strict;
+use warnings;
+
+use lib "./lib";
+
+use PS::IPP::IData::Client;
+
+my $idata = PS::IPP::IData::Client->new(
+    uri     => 'http://localhost:80/PS/IPP/IData/Server',
+    proxy   => 'http://localhost:80/idata'
+);
+
+my $key = shift;
+
+my $kids = shift || 1;
+
+foreach my $id ( 1..$kids ) {
+    my $pid = fork;
+
+    unless ( $pid )  {
+        my $fname = "${key}_$id";
+        my $fh = $idata->create( $fname );
+        die "can't create file $fname" unless $fh;
+
+        print $fh "fooby\n";
+
+        close $fh;
+        $idata->unlock( $fname, 'write' );
+
+        $fh = $idata->open( $fname, 'read' ) or die "can't open file";
+        close $fh;
+        $idata->unlock( $fname, 'read' );
+
+        $idata->replicate( $fname );
+        $idata->cull( $fname );
+        $idata->find( $fname );
+        $idata->copy( $fname, $fname . "_copy" );
+        $idata->move( $fname, $fname . "_move" );
+        $idata->delete( $fname . "_copy" );
+        $idata->delete( $fname . "_move" );
+
+        exit 0;
+    }
+}
+
+while ( $kids ) {
+    wait();
+    $kids --;
+}
Index: /branches/jhoblitt/Nebulous/scripts/stest.pl
===================================================================
--- /branches/jhoblitt/Nebulous/scripts/stest.pl	(revision 2784)
+++ /branches/jhoblitt/Nebulous/scripts/stest.pl	(revision 2784)
@@ -0,0 +1,33 @@
+#!/usr/bin/perl
+
+use strict;
+use warnings;
+
+use lib "./lib";
+
+use PS::IPP::IData::Client; # trace => 'debug';
+
+my $idata = PS::IPP::IData::Client->new(
+    proxy   => 'http://localhost:80/idata'
+);
+
+my $fname = shift;
+
+#my $fh = $idata->create( $fname );
+#$idata->unlock( $fname, 'write' );
+
+$idata->create( $fname, 1 );
+$idata->unlock( $fname, 'write' );
+$idata->delete( $fname );
+
+__END__
+
+{
+    my $fh = $pd->create( $key );
+    die "can't create file $fname" unless $fh;
+    print $fh "fooby\n";
+    close $fh;
+    #$idata->unlock( $fname, 'write' );
+    my $filesize =  (stat( $key ))[7];
+    print "created file $key, ${filesize}bytes\n";
+}
Index: /branches/jhoblitt/Nebulous/t/01_load.t
===================================================================
--- /branches/jhoblitt/Nebulous/t/01_load.t	(revision 2784)
+++ /branches/jhoblitt/Nebulous/t/01_load.t	(revision 2784)
@@ -0,0 +1,19 @@
+# -*- perl -*-
+
+# t/001_load.t - check module loading and create testing directory
+
+use strict;
+use warnings;
+
+use lib qw( ./t ./lib );
+
+use Test::More tests => 8;
+
+BEGIN { use_ok( 'PS::IPP::IData::Client' ); }
+BEGIN { use_ok( 'PS::IPP::IData::Client::Log' ); }
+BEGIN { use_ok( 'PS::IPP::IData::Client::HTTP' ); }
+BEGIN { use_ok( 'PS::IPP::IData::Util' ); }
+BEGIN { use_ok( 'PS::IPP::IData::Server' ); }
+BEGIN { use_ok( 'PS::IPP::IData::Server::Apache' ); }
+BEGIN { use_ok( 'PS::IPP::IData::Server::SQL' ); }
+BEGIN { use_ok( 'PS::IPP::IData::Server::Log' ); }
Index: /branches/jhoblitt/Nebulous/t/02_server_setup.t
===================================================================
--- /branches/jhoblitt/Nebulous/t/02_server_setup.t	(revision 2784)
+++ /branches/jhoblitt/Nebulous/t/02_server_setup.t	(revision 2784)
@@ -0,0 +1,35 @@
+#!/usr/bin/perl
+
+use strict;
+use warnings;
+
+use Test::More tests => 2;
+
+use lib qw( ./t ./lib );
+
+use PS::IPP::IData::Server;
+use Test::IData;
+
+Test::IData->setup;
+
+ok(
+    PS::IPP::IData::Server->setup(
+        "DBI:mysql:database=test:host=localhost",
+        "test",
+        "",
+    ),
+    "connect",
+);
+
+Test::IData->setup;
+
+eval {
+    PS::IPP::IData::Server->setup(
+        "DBI:mysql:database=foobar:host=localhost",
+        "baz",
+        "boo",
+    );
+};
+like( $@, qr/DBI connect.*? failed/, "bad dsn/user/pass" );
+
+Test::IData->cleanup;
Index: /branches/jhoblitt/Nebulous/t/03_server_create_object.t
===================================================================
--- /branches/jhoblitt/Nebulous/t/03_server_create_object.t	(revision 2784)
+++ /branches/jhoblitt/Nebulous/t/03_server_create_object.t	(revision 2784)
@@ -0,0 +1,84 @@
+#!/usr/bin/perl
+
+use strict;
+use warnings;
+
+use Test::More tests => 11;
+
+use lib qw( ./t ./lib );
+
+use PS::IPP::IData::Server;
+use PS::IPP::IData::Util qw( :standard );
+use Test::IData;
+
+my $idata = "PS::IPP::IData::Server";
+
+$idata->setup(
+    "DBI:mysql:database=test:host=localhost",
+    "test",
+    "",
+);
+
+Test::IData->setup;
+
+{
+    # key
+    my $uri = $idata->create_object( "foo" );
+
+    ok( -e _get_file_path( $uri ), "file exists" );
+    like( $uri, qr/^file:\//, "valid URI" );
+}
+
+Test::IData->setup;
+
+{
+    # key, class
+    my $uri = $idata->create_object( "foo", 0 );
+
+    ok( -e _get_file_path( $uri ), "file exists" );
+    like( $uri, qr/^file:\//, "valid URI" );
+}
+
+Test::IData->setup;
+
+{
+    # key, class, volume
+    my $uri = $idata->create_object( "foo", 0, "node01" );
+
+    ok( -e _get_file_path( $uri ), "file exists" );
+    like( $uri, qr/^file:\//, "valid URI" );
+}
+
+Test::IData->setup;
+
+{
+    # key, class, volume, comment
+    my $uri = $idata->create_object( "foo", 0, "node01", "this is foo" );
+
+    ok( -e _get_file_path( $uri ), "file exists" );
+    like( $uri, qr/^file:\//, "valid URI" );
+}
+
+Test::IData->setup;
+
+eval {
+    $idata->create_object( "foo" );
+    $idata->create_object( "foo" );
+};
+like( $@, qr/Duplicate entry/, "object already exists" );
+
+Test::IData->setup;
+
+eval {
+    $idata->create_object();
+};
+like( $@, qr/2 - 4 were expected/, "no params" );
+
+Test::IData->setup;
+
+eval {
+    $idata->create_object( 1, 2, 3, 4, 5 );
+};
+like( $@, qr/2 - 4 were expected/, "too many params" );
+
+Test::IData->cleanup;
Index: /branches/jhoblitt/Nebulous/t/04_server_replicate_object.t
===================================================================
--- /branches/jhoblitt/Nebulous/t/04_server_replicate_object.t	(revision 2784)
+++ /branches/jhoblitt/Nebulous/t/04_server_replicate_object.t	(revision 2784)
@@ -0,0 +1,79 @@
+#!/usr/bin/perl
+
+use strict;
+use warnings;
+
+use Test::More tests => 11;
+
+use lib qw( ./t ./lib );
+
+use PS::IPP::IData::Server;
+use PS::IPP::IData::Util qw( :standard );
+use Test::IData;
+
+my $idata = "PS::IPP::IData::Server";
+
+$idata->setup(
+    "DBI:mysql:database=test:host=localhost",
+    "test",
+    "",
+);
+
+Test::IData->setup;
+
+{
+    # key
+    $idata->create_object( "foo" );
+    my $uri = $idata->replicate_object( "foo" );
+
+    ok( -e _get_file_path( $uri ), "file exists" );
+    like( $uri, qr/^file:\//, "valid URI" );
+}
+
+Test::IData->setup;
+
+{
+    # key, $volume
+    $idata->create_object( "foo" );
+    my $uri = $idata->replicate_object( "foo", "node01" );
+
+    ok( -e _get_file_path( $uri ), "file exists" );
+    like( $uri, qr/^file:\//, "valid URI" );
+}
+
+Test::IData->setup;
+
+{
+    # key, $volume
+    $idata->create_object( "foo" );
+    my $uri1 = $idata->replicate_object( "foo", "node01" );
+    my $uri2 = $idata->replicate_object( "foo", "node02" );
+
+    ok( -e _get_file_path( $uri1 ), "file exists" );
+    ok( -e _get_file_path( $uri2 ), "file exists" );
+    like( $uri1, qr/^file:\//, "valid URI" );
+    like( $uri2, qr/^file:\//, "valid URI" );
+}
+
+Test::IData->setup;
+
+eval {
+    $idata->replicate_object( "foo" );
+};
+like( $@, qr/storage object does not exist/, "storage object does not exist" );
+
+Test::IData->setup;
+
+eval {
+    $idata->replicate_object();
+};
+like( $@, qr/1 - 2 were expected/, "no params" );
+
+Test::IData->setup;
+
+eval {
+    $idata->replicate_object( 1, 2, 3 );
+};
+like( $@, qr/1 - 2 were expected/, "too many params" );
+
+Test::IData->cleanup;
Index: /branches/jhoblitt/Nebulous/t/05_server_lock_object.t
===================================================================
--- /branches/jhoblitt/Nebulous/t/05_server_lock_object.t	(revision 2784)
+++ /branches/jhoblitt/Nebulous/t/05_server_lock_object.t	(revision 2784)
@@ -0,0 +1,119 @@
+#!/usr/bin/perl
+
+use strict;
+use warnings;
+
+use Test::More tests => 13;
+
+use lib qw( ./t ./lib );
+
+use PS::IPP::IData::Server;
+use PS::IPP::IData::Util qw( :standard );
+use Test::IData;
+
+my $idata = "PS::IPP::IData::Server";
+
+$idata->setup(
+    "DBI:mysql:database=test:host=localhost",
+    "test",
+    "",
+);
+
+Test::IData->setup;
+
+{
+    $idata->create_object( "foo" );
+
+    ok( $idata->lock_object( "foo", "read" ), "read lock" );
+}
+
+Test::IData->setup;
+
+{
+    $idata->create_object( "foo" );
+
+    ok( $idata->lock_object( "foo", "read" ), "read lock" );
+    ok( $idata->lock_object( "foo", "read" ), "read lock" );
+}
+
+Test::IData->setup;
+
+{
+    $idata->create_object( "foo" );
+
+    ok( $idata->lock_object( "foo", "write" ), "write lock" );
+}
+
+Test::IData->setup;
+
+eval {
+    $idata->lock_object( "foo", "read" );
+};
+like( $@, qr/storage object does not exist/, "storage object does not exist" );
+
+Test::IData->setup;
+
+eval {
+    $idata->lock_object( "foo", "write" );
+};
+like( $@, qr/storage object does not exist/, "storage object does not exist" );
+
+Test::IData->setup;
+
+eval {
+    $idata->create_object( "foo" );
+
+    $idata->lock_object( "foo", "write" );
+    $idata->lock_object( "foo", "write" );
+};
+like( $@, qr/can not write lock twice/, "can not write lock twice" );
+
+Test::IData->setup;
+
+eval {
+    $idata->create_object( "foo" );
+
+    $idata->lock_object( "foo", "read" );
+    $idata->lock_object( "foo", "write" );
+};
+like( $@, qr/can not write lock after read lock/, "can not write lock after read lock" );
+
+Test::IData->setup;
+
+eval {
+    $idata->create_object( "foo" );
+
+    $idata->lock_object( "foo", "write" );
+    $idata->lock_object( "foo", "read" );
+};
+like( $@, qr/can not read lock after write lock/, "can not read lock after write lock" );
+
+Test::IData->setup;
+
+eval {
+    $idata->lock_object();
+};
+like( $@, qr/2 were expected/, "no params" );
+
+Test::IData->setup;
+
+eval {
+    $idata->lock_object( "foo" );
+};
+like( $@, qr/2 were expected/, "not enough params" );
+
+Test::IData->setup;
+
+eval {
+    $idata->lock_object( "foo", "both" );
+};
+like( $@, qr/is read or write/, "not read or write" );
+
+Test::IData->setup;
+
+eval {
+    $idata->lock_object( "foo", 'read', 3 );
+};
+like( $@, qr/2 were expected/, "too many params" );
+
+Test::IData->cleanup;
Index: /branches/jhoblitt/Nebulous/t/06_server_unlock_object.t
===================================================================
--- /branches/jhoblitt/Nebulous/t/06_server_unlock_object.t	(revision 2784)
+++ /branches/jhoblitt/Nebulous/t/06_server_unlock_object.t	(revision 2784)
@@ -0,0 +1,134 @@
+#!/usr/bin/perl
+
+use strict;
+use warnings;
+
+use Test::More tests => 14;
+
+use lib qw( ./t ./lib );
+
+use PS::IPP::IData::Server;
+use PS::IPP::IData::Util qw( :standard );
+use Test::IData;
+
+my $idata = "PS::IPP::IData::Server";
+
+$idata->setup(
+    "DBI:mysql:database=test:host=localhost",
+    "test",
+    "",
+);
+
+Test::IData->setup;
+
+{
+    $idata->create_object( "foo" );
+
+    $idata->lock_object( "foo", "read" );
+
+    ok( $idata->unlock_object( "foo", "read" ), "read unlock" );
+}
+
+Test::IData->setup;
+
+{
+    $idata->create_object( "foo" );
+
+    $idata->lock_object( "foo", "read" );
+    $idata->lock_object( "foo", "read" );
+
+    ok( $idata->unlock_object( "foo", "read" ), "read unlock" );
+    ok( $idata->unlock_object( "foo", "read" ), "read unlock" );
+}
+
+Test::IData->setup;
+
+{
+    $idata->create_object( "foo" );
+
+    $idata->lock_object( "foo", "write" );
+
+    ok( $idata->unlock_object( "foo", "write" ), "write unlock" );
+}
+
+Test::IData->setup;
+
+eval {
+    $idata->unlock_object( "foo", "read" );
+};
+like( $@, qr/storage object does not exist/, "storage object does not exist" );
+
+Test::IData->setup;
+
+eval {
+    $idata->unlock_object( "foo", "write" );
+};
+like( $@, qr/storage object does not exist/, "storage object does not exist" );
+
+Test::IData->setup;
+
+eval {
+    $idata->create_object( "foo" );
+
+    $idata->unlock_object( "foo", "read" );
+};
+like( $@, qr/can not remove non-existant read lock/, "no lock set" );
+
+Test::IData->setup;
+
+eval {
+    $idata->create_object( "foo" );
+
+    $idata->unlock_object( "foo", "write" );
+};
+like( $@, qr/can not remove non-existant write lock/, "no lock set" );
+
+Test::IData->setup;
+
+eval {
+    $idata->create_object( "foo" );
+    $idata->lock_object( "foo", "write" );
+
+    $idata->unlock_object( "foo", "read" );
+};
+like( $@, qr/can not have a read lock under a write lock/, "read unlock under write lock" );
+
+Test::IData->setup;
+
+eval {
+    $idata->create_object( "foo" );
+    $idata->lock_object( "foo", "read" );
+
+    $idata->unlock_object( "foo", "write" );
+};
+like( $@, qr/can not have a write lock under a read lock/, "write unlock under read lock" );
+
+Test::IData->setup;
+
+eval {
+    $idata->unlock_object();
+};
+like( $@, qr/2 were expected/, "no params" );
+
+Test::IData->setup;
+
+eval {
+    $idata->unlock_object( "foo" );
+};
+like( $@, qr/2 were expected/, "not enough params" );
+
+Test::IData->setup;
+
+eval {
+    $idata->unlock_object( "foo", "both" );
+};
+like( $@, qr/is read or write/, "not read or write" );
+
+Test::IData->setup;
+
+eval {
+    $idata->unlock_object( "foo", 'read', 3 );
+};
+like( $@, qr/2 were expected/, "too many params" );
+
+Test::IData->cleanup;
Index: /branches/jhoblitt/Nebulous/t/07_server_find_instances.t
===================================================================
--- /branches/jhoblitt/Nebulous/t/07_server_find_instances.t	(revision 2784)
+++ /branches/jhoblitt/Nebulous/t/07_server_find_instances.t	(revision 2784)
@@ -0,0 +1,90 @@
+#!/usr/bin/perl
+
+use strict;
+use warnings;
+
+use Test::More tests => 7;
+
+use lib qw( ./t ./lib );
+
+use PS::IPP::IData::Server;
+use PS::IPP::IData::Util qw( :standard );
+use Test::IData;
+
+my $idata = "PS::IPP::IData::Server";
+
+$idata->setup(
+    "DBI:mysql:database=test:host=localhost",
+    "test",
+    "",
+);
+
+Test::IData->setup;
+
+{
+    # key
+    my $uri = $idata->create_object( "foo" );
+
+    my $locations = $idata->find_instances( "foo" );
+
+    is( $uri, $locations->[0], "URIs match" );
+}
+
+Test::IData->setup;
+
+{
+    # key
+    my $uri1 = $idata->create_object( "foo" );
+    my $uri2 = $idata->replicate_object( "foo" );
+
+    my $locations = $idata->find_instances( "foo" );
+
+    ok( eq_set( [$uri1, $uri2], $locations ), "URIs match" );
+}
+
+Test::IData->setup;
+
+{
+    # key, volume
+    my $uri = $idata->create_object( "foo" );
+
+    my $locations = $idata->find_instances( "foo", "node01" );
+
+    is( $uri, $locations->[0], "URIs match" );
+}
+
+Test::IData->setup;
+
+{
+    # key
+    my $uri1 = $idata->create_object( "foo" );
+    my $uri2 = $idata->replicate_object( "foo" );
+
+    my $locations = $idata->find_instances( "foo", "node01" );
+
+    ok( eq_set( [$uri1, $uri2], $locations ), "URIs match" );
+}
+
+
+Test::IData->setup;
+
+eval {
+    $idata->find_instances( "foo" );
+};
+like( $@, qr/no instances found/, "storage object does not exist" );
+
+Test::IData->setup;
+
+eval {
+    $idata->find_instances();
+};
+like( $@, qr/1 - 2 were expected/, "no params" );
+
+Test::IData->setup;
+
+eval {
+    $idata->find_instances( "foo", 'read', 3 );
+};
+like( $@, qr/1 - 2 were expected/, "too many params" );
+
+Test::IData->cleanup;
Index: /branches/jhoblitt/Nebulous/t/08_server_delete_instance.t
===================================================================
--- /branches/jhoblitt/Nebulous/t/08_server_delete_instance.t	(revision 2784)
+++ /branches/jhoblitt/Nebulous/t/08_server_delete_instance.t	(revision 2784)
@@ -0,0 +1,71 @@
+#!/usr/bin/perl
+
+use strict;
+use warnings;
+
+use Test::More tests => 8;
+
+use lib qw( ./t ./lib );
+
+use PS::IPP::IData::Server;
+use PS::IPP::IData::Util qw( :standard );
+use Test::IData;
+
+my $idata = "PS::IPP::IData::Server";
+
+$idata->setup(
+    "DBI:mysql:database=test:host=localhost",
+    "test",
+    "",
+);
+
+Test::IData->setup;
+
+{
+    my $uri = $idata->create_object( "foo" );
+
+    ok( $idata->delete_instance( $uri ), "delete instance" );
+}
+
+Test::IData->setup;
+
+{
+    my $uri1 = $idata->create_object( "foo" );
+    my $uri2 = $idata->replicate_object( "foo" );
+
+    ok( $idata->delete_instance( $uri1 ), "delete instance" );
+
+    my $locations = $idata->find_instances( "foo" );
+
+    is( $locations->[0], $uri2, "instance remains" );
+
+    ok( $idata->delete_instance( $uri2 ), "delete instance" );
+
+    eval {
+        $idata->find_instances( "foo" );
+    };
+    like( $@, qr/no instances found/, "storage object was deleted" );
+}
+
+Test::IData->setup;
+
+eval {
+    $idata->delete_instance( "file:/foo" );
+};
+like( $@, qr/no instance is associated with uri/, "uri does not exist" );
+
+Test::IData->setup;
+
+eval {
+    $idata->delete_instance();
+};
+like( $@, qr/1 was expected/, "no params" );
+
+Test::IData->setup;
+
+eval {
+    $idata->delete_instance( "foo", 2 );
+};
+like( $@, qr/1 was expected/, "too many params" );
+
+Test::IData->cleanup;
Index: /branches/jhoblitt/Nebulous/t/09_server_stat_object.t
===================================================================
--- /branches/jhoblitt/Nebulous/t/09_server_stat_object.t	(revision 2784)
+++ /branches/jhoblitt/Nebulous/t/09_server_stat_object.t	(revision 2784)
@@ -0,0 +1,70 @@
+#!/usr/bin/perl
+
+use strict;
+use warnings;
+
+use Test::More tests => 12;
+
+use lib qw( ./t ./lib );
+
+use PS::IPP::IData::Server;
+use PS::IPP::IData::Util qw( :standard );
+use Test::IData;
+
+my $idata = "PS::IPP::IData::Server";
+
+$idata->setup(
+    "DBI:mysql:database=test:host=localhost",
+    "test",
+    "",
+);
+
+Test::IData->setup;
+
+{
+    my $uri = $idata->create_object( "foo" );
+
+    my $info = $idata->stat_object( "foo" );
+
+    is( scalar @$info, 7, "number of columns" );
+}
+
+Test::IData->setup;
+
+{
+    my $uri = $idata->create_object( "foo", 9, "node01", "foobar" );
+
+    my $info = $idata->stat_object( "foo" );
+
+    is( scalar @$info, 7,                       "number of columns" );
+    is( @$info[0], 1,                           "so_id" );
+    is( @$info[1], "foo",                       "ext_id" );
+    is( @$info[2], 9,                           "class_id" );
+    is( @$info[3], "foobar",                    "comment" );
+    is( @$info[4], 0,                           "read lock" );
+    is( @$info[5], undef,                       "write lock" );
+    like( @$info[6], qr/....-..-.. ..:..:../,   "epoch" );
+}
+
+Test::IData->setup;
+
+eval {
+    $idata->stat_object( "foo" );
+};
+like( $@, qr/no storage object found/, "object does not exist" );
+
+Test::IData->setup;
+
+eval {
+    $idata->stat_object();
+};
+like( $@, qr/1 was expected/, "no params" );
+
+Test::IData->setup;
+
+eval {
+    $idata->stat_object( "foo", 2 );
+};
+like( $@, qr/1 was expected/, "too many params" );
+
+Test::IData->cleanup;
Index: /branches/jhoblitt/Nebulous/t/10_client_new.t
===================================================================
--- /branches/jhoblitt/Nebulous/t/10_client_new.t	(revision 2784)
+++ /branches/jhoblitt/Nebulous/t/10_client_new.t	(revision 2784)
@@ -0,0 +1,56 @@
+#!/usr/bin/perl
+
+use strict;
+use warnings;
+
+use Apache::Test qw( -withtestmore );
+
+plan tests => 4;
+
+use lib qw( ./t ./lib );
+
+use PS::IPP::IData::Client;
+use PS::IPP::IData::Util qw( :standard );
+use Test::IData;
+
+my $hostport = Apache::Test->config->{ 'hostport' };
+
+Test::IData->setup;
+
+{
+    my $idata = PS::IPP::IData::Client->new(
+        proxy => "http://$hostport/idata",
+    );
+
+    isa_ok( $idata, "PS::IPP::IData::Client" );
+}
+
+Test::IData->setup;
+
+{
+    my $idata = PS::IPP::IData::Client->new(
+        proxy => "http://$hostport/idata",
+        uri   => "http://example.com/PS/IPP/IData/Client",
+    );
+
+    isa_ok( $idata, "PS::IPP::IData::Client" );
+}
+
+Test::IData->setup;
+
+eval {
+    PS::IPP::IData::Client->new;
+};
+like( $@, qr/Mandatory parameter/, "no proxy" );
+
+Test::IData->setup;
+
+eval {
+    PS::IPP::IData::Client->new(
+        proxy => "foo",
+        dog => "do"
+    );
+};
+like( $@, qr/not listed in the validation options/, "bad param" );
+
+Test::IData->cleanup;
Index: /branches/jhoblitt/Nebulous/t/11_client_create.t
===================================================================
--- /branches/jhoblitt/Nebulous/t/11_client_create.t	(revision 2784)
+++ /branches/jhoblitt/Nebulous/t/11_client_create.t	(revision 2784)
@@ -0,0 +1,103 @@
+#!/usr/bin/perl
+
+use strict;
+use warnings;
+
+use Apache::Test qw( -withtestmore );
+
+plan tests => 7;
+
+use lib qw( ./t ./lib );
+
+use PS::IPP::IData::Client;
+use PS::IPP::IData::Util qw( :standard );
+use Test::IData;
+
+my $hostport = Apache::Test->config->{ 'hostport' };
+
+Test::IData->setup;
+
+{
+    # key
+    my $idata = PS::IPP::IData::Client->new(
+        proxy => "http://$hostport/idata",
+    );
+
+    my $fh = $idata->create( "foo" );
+
+    is( ref $fh, 'GLOB', "good filehandle" );
+}
+
+Test::IData->setup;
+
+{
+    # key, class
+    my $idata = PS::IPP::IData::Client->new(
+        proxy => "http://$hostport/idata",
+    );
+
+    my $fh = $idata->create( "foo", 0 );
+
+    is( ref $fh, 'GLOB', "good filehandle" );
+}
+
+Test::IData->setup;
+
+{
+    # key, class, volume
+    my $idata = PS::IPP::IData::Client->new(
+        proxy => "http://$hostport/idata",
+    );
+
+    my $fh = $idata->create( "foo", 0, "node01" );
+
+    is( ref $fh, 'GLOB', "good filehandle" );
+}
+
+Test::IData->setup;
+
+{
+    # key, class, volume, comment
+    my $idata = PS::IPP::IData::Client->new(
+        proxy => "http://$hostport/idata",
+    );
+
+    my $fh = $idata->create( "foo", 0, "node01", "this is foo" );
+
+    is( ref $fh, 'GLOB', "good filehandle" );
+}
+
+Test::IData->setup;
+
+{
+    my $idata = PS::IPP::IData::Client->new(
+        proxy => "http://$hostport/idata",
+    );
+
+    $idata->create( "foo" );
+    is( $idata->create( "foo" ), undef, "object already exists" );
+}
+
+Test::IData->setup;
+
+eval {
+    my $idata = PS::IPP::IData::Client->new(
+        proxy => "http://$hostport/idata",
+    );
+
+    $idata->create();
+};
+like( $@, qr/2 - 4 were expected/, "no params" );
+
+Test::IData->setup;
+
+eval {
+    my $idata = PS::IPP::IData::Client->new(
+        proxy => "http://$hostport/idata",
+    );
+
+    $idata->create( 1, 2, 3, 4, 5 );
+};
+like( $@, qr/2 - 4 were expected/, "too many params" );
+
+Test::IData->cleanup;
Index: /branches/jhoblitt/Nebulous/t/12_client_replicate.t
===================================================================
--- /branches/jhoblitt/Nebulous/t/12_client_replicate.t	(revision 2784)
+++ /branches/jhoblitt/Nebulous/t/12_client_replicate.t	(revision 2784)
@@ -0,0 +1,97 @@
+#!/usr/bin/perl
+
+use strict;
+use warnings;
+
+use Apache::Test qw( -withtestmore );
+
+plan tests => 11;
+
+use lib qw( ./t ./lib );
+
+use PS::IPP::IData::Client;
+use PS::IPP::IData::Util qw( :standard );
+use Test::IData;
+
+my $hostport = Apache::Test->config->{ 'hostport' };
+
+Test::IData->setup;
+
+{
+    # key
+    my $idata = PS::IPP::IData::Client->new(
+        proxy => "http://$hostport/idata",
+    );
+    $idata->create( "foo" );
+
+    my $uri = $idata->replicate( "foo" );
+
+    ok( $uri, "good replication" );
+    ok( -e _get_file_path( $uri ), "file exists" );
+}
+
+Test::IData->setup;
+
+{
+    # key, $volume
+    my $idata = PS::IPP::IData::Client->new(
+        proxy => "http://$hostport/idata",
+    );
+    $idata->create( "foo" );
+
+    my $uri = $idata->replicate( "foo", "node01" );
+
+    ok( $uri, "good replication" );
+    ok( -e _get_file_path( $uri ), "file exists" );
+}
+
+Test::IData->setup;
+
+{
+    # key, $volume
+    my $idata = PS::IPP::IData::Client->new(
+        proxy => "http://$hostport/idata",
+    );
+    $idata->create( "foo" );
+
+    my $uri1 = $idata->replicate( "foo", "node01" );
+    my $uri2 = $idata->replicate( "foo", "node02" );
+
+    ok( $uri1, "good replication" );
+    ok( $uri2, "good replication" );
+    ok( -e _get_file_path( $uri1 ), "file exists" );
+    ok( -e _get_file_path( $uri2 ), "file exists" );
+}
+
+Test::IData->setup;
+
+{
+    my $idata = PS::IPP::IData::Client->new(
+        proxy => "http://$hostport/idata",
+    );
+    my $uri = $idata->replicate( "foo" );
+
+    is( $uri, undef, "storage object does not exist" );
+}
+
+Test::IData->setup;
+
+eval {
+    my $idata = PS::IPP::IData::Client->new(
+        proxy => "http://$hostport/idata",
+    );
+    $idata->replicate();
+};
+like( $@, qr/1 - 2 were expected/, "no params" );
+
+Test::IData->setup;
+
+eval {
+    my $idata = PS::IPP::IData::Client->new(
+        proxy => "http://$hostport/idata",
+    );
+    $idata->replicate( 1, 2, 3 );
+};
+like( $@, qr/1 - 2 were expected/, "too many params" );
+
+Test::IData->cleanup;
Index: /branches/jhoblitt/Nebulous/t/13_client_cull.t
===================================================================
--- /branches/jhoblitt/Nebulous/t/13_client_cull.t	(revision 2784)
+++ /branches/jhoblitt/Nebulous/t/13_client_cull.t	(revision 2784)
@@ -0,0 +1,127 @@
+#!/usr/bin/perl
+
+use strict;
+use warnings;
+
+use Apache::Test qw( -withtestmore );
+
+plan tests => 15;
+
+use lib qw( ./t ./lib );
+
+use PS::IPP::IData::Client;
+use PS::IPP::IData::Util qw( :standard );
+use Test::IData;
+
+my $hostport = Apache::Test->config->{ 'hostport' };
+
+Test::IData->setup;
+
+{
+    # key
+    my $idata = PS::IPP::IData::Client->new(
+        proxy => "http://$hostport/idata",
+    );
+    $idata->create( "foo" );
+    $idata->replicate( "foo" );
+
+    my $uri = $idata->cull( "foo" );
+
+    ok( $uri, "good cull" );
+    ok( ! -e _get_file_path( $uri ), "file doesn't exist" );
+}
+
+Test::IData->setup;
+
+SKIP: {
+    skip "unsure if this should be part of the API", 2;
+
+    # key, $volume
+    my $idata = PS::IPP::IData::Client->new(
+        proxy => "http://$hostport/idata",
+    );
+    $idata->create( "foo" );
+    $idata->replicate( "foo", "node01" );
+
+    my $uri = $idata->cull( "foo", "node01" );
+
+    ok( $uri, "good cull" );
+    ok( ! -e _get_file_path( $uri ), "file exists" );
+}
+
+Test::IData->setup;
+
+{
+    # key
+    my $idata = PS::IPP::IData::Client->new(
+        proxy => "http://$hostport/idata",
+    );
+    $idata->create( "foo" );
+
+    $idata->replicate( "foo", "node01" );
+    $idata->replicate( "foo", "node02" );
+
+    my $uri1 = $idata->cull( "foo" );
+    my $uri2 = $idata->cull( "foo" );
+
+    ok( $uri1, "good cull" );
+    ok( $uri2, "good cull" );
+    ok( ! -e _get_file_path( $uri1 ), "file exists" );
+    ok( ! -e _get_file_path( $uri2 ), "file exists" );
+}
+
+Test::IData->setup;
+
+SKIP: {
+    skip "unsure if this should be part of the API", 4;
+
+    # key, $volume
+    my $idata = PS::IPP::IData::Client->new(
+        proxy => "http://$hostport/idata",
+    );
+    $idata->create( "foo" );
+
+    $idata->replicate( "foo", "node01" );
+    $idata->replicate( "foo", "node02" );
+
+    my $uri1 = $idata->cull( "foo", "node01" );
+    my $uri2 = $idata->cull( "foo", "node02" );
+
+    ok( $uri1, "good cull" );
+    ok( $uri2, "good cull" );
+    ok( ! -e _get_file_path( $uri1 ), "file exists" );
+    ok( ! -e _get_file_path( $uri2 ), "file exists" );
+}
+
+Test::IData->setup;
+
+{
+    my $idata = PS::IPP::IData::Client->new(
+        proxy => "http://$hostport/idata",
+    );
+    my $uri = $idata->cull( "foo" );
+
+    is( $uri, undef, "storage object does not exist" );
+}
+
+Test::IData->setup;
+
+eval {
+    my $idata = PS::IPP::IData::Client->new(
+        proxy => "http://$hostport/idata",
+    );
+    $idata->cull();
+};
+like( $@, qr/1 was expected/, "no params" );
+
+Test::IData->setup;
+
+eval {
+    my $idata = PS::IPP::IData::Client->new(
+        proxy => "http://$hostport/idata",
+    );
+    $idata->cull( 1, 2 );
+};
+like( $@, qr/1 was expected/, "too many params" );
+
+Test::IData->cleanup;
Index: /branches/jhoblitt/Nebulous/t/14_client_lock.t
===================================================================
--- /branches/jhoblitt/Nebulous/t/14_client_lock.t	(revision 2784)
+++ /branches/jhoblitt/Nebulous/t/14_client_lock.t	(revision 2784)
@@ -0,0 +1,269 @@
+#!/usr/bin/perl
+
+use strict;
+use warnings;
+
+use Apache::Test qw( -withtestmore );
+
+plan tests => 19;
+
+use lib qw( ./t ./lib );
+
+use PS::IPP::IData::Client;
+use PS::IPP::IData::Util qw( :standard );
+use Test::IData;
+
+my $hostport = Apache::Test->config->{ 'hostport' };
+
+Test::IData->setup;
+
+{
+    # key, type
+    my $idata = PS::IPP::IData::Client->new(
+        proxy => "http://$hostport/idata",
+    );
+    $idata->create( "foo" );
+
+    ok( $idata->lock( "foo", "read" ), "read lock" );
+}
+
+Test::IData->setup;
+
+{
+    # key, type, timeout
+    my $idata = PS::IPP::IData::Client->new(
+        proxy => "http://$hostport/idata",
+    );
+    $idata->create( "foo" );
+
+    ok( $idata->lock( "foo", "read", 10 ), "read lock" );
+}
+
+Test::IData->setup;
+
+{
+    my $idata = PS::IPP::IData::Client->new(
+        proxy => "http://$hostport/idata",
+    );
+    $idata->create( "foo" );
+
+    ok( $idata->lock( "foo", "read" ), "read lock" );
+    ok( $idata->lock( "foo", "read" ), "read lock" );
+}
+
+Test::IData->setup;
+
+{
+    # key, type
+    my $idata = PS::IPP::IData::Client->new(
+        proxy => "http://$hostport/idata",
+    );
+    $idata->create( "foo" );
+
+    ok( $idata->lock( "foo", "write" ), "write lock" );
+}
+
+Test::IData->setup;
+
+{
+    # key, type, timeout
+    my $idata = PS::IPP::IData::Client->new(
+        proxy => "http://$hostport/idata",
+    );
+    $idata->create( "foo" );
+
+    ok( $idata->lock( "foo", "write", 10 ), "write lock" );
+}
+
+Test::IData->setup;
+
+{
+    my $idata = PS::IPP::IData::Client->new(
+        proxy => "http://$hostport/idata",
+    );
+
+    is( $idata->lock( "foo", "read" ), undef, "storage object does not exist" );
+}
+
+Test::IData->setup;
+
+{
+    my $idata = PS::IPP::IData::Client->new(
+        proxy => "http://$hostport/idata",
+    );
+
+    is( $idata->lock( "foo", "write" ), undef, "storage object does not exist" );
+}
+
+Test::IData->setup;
+
+diag "testing a spinlock, this will be a litle slow...";
+
+eval {
+    local $SIG{ALRM} = sub { alarm 0; die "timeout"; };
+    alarm 8;
+
+    # key, type, timeout
+    my $idata = PS::IPP::IData::Client->new(
+        proxy => "http://$hostport/idata",
+    );
+    $idata->create( "foo" );
+
+    $idata->lock( "foo", "read" );
+    $idata->lock( "foo", "write" );
+
+    alarm 0;
+};
+like( $@, qr/timeout/, "default spinlock" );
+
+Test::IData->setup;
+
+diag "testing a spinlock, this will be a litle slow...";
+
+eval {
+    local $SIG{ALRM} = sub { alarm 0; die "timeout"; };
+    alarm 12;
+
+    # key, type, timeout
+    my $idata = PS::IPP::IData::Client->new(
+        proxy => "http://$hostport/idata",
+    );
+    $idata->create( "foo" );
+
+    $idata->lock( "foo", "read" );
+    unless ( $idata->lock( "foo", "write" ) ) {
+        alarm 0;
+        die "gaveup";
+    }
+
+    alarm 0;
+};
+like( $@, qr/gaveup/, "default spinlock" );
+
+Test::IData->setup;
+
+diag "testing a spinlock, this will be a litle slow...";
+
+eval {
+    local $SIG{ALRM} = sub { alarm 0; die "timeout"; };
+    alarm 3;
+
+    # key, type, timeout
+    my $idata = PS::IPP::IData::Client->new(
+        proxy => "http://$hostport/idata",
+    );
+    $idata->create( "foo" );
+
+    $idata->lock( "foo", "read" );
+    $idata->lock( "foo", "write", 5 );
+
+    alarm 0;
+};
+like( $@, qr/timeout/, "specified spinlock" );
+
+Test::IData->setup;
+
+diag "testing a spinlock, this will be a litle slow...";
+
+eval {
+    local $SIG{ALRM} = sub { alarm 0; die "timeout"; };
+    alarm 7;
+
+    # key, type, timeout
+    my $idata = PS::IPP::IData::Client->new(
+        proxy => "http://$hostport/idata",
+    );
+    $idata->create( "foo" );
+
+    $idata->lock( "foo", "read" );
+    unless ( $idata->lock( "foo", "write", 5 ) ) {
+        alarm 0;
+        die "gaveup";
+    }
+
+    alarm 0;
+};
+
+like( $@, qr/gaveup/, "specified spinlock" );
+
+Test::IData->setup;
+
+{
+    my $idata = PS::IPP::IData::Client->new(
+        proxy => "http://$hostport/idata",
+    );
+    $idata->create( "foo" );
+
+    $idata->lock( "foo", "write" );
+    is( $idata->lock( "foo", "write", 1 ), undef, "can not write lock twice" );
+}
+
+Test::IData->setup;
+
+{
+    my $idata = PS::IPP::IData::Client->new(
+        proxy => "http://$hostport/idata",
+    );
+    $idata->create( "foo" );
+
+    $idata->lock( "foo", "read" );
+    is( $idata->lock( "foo", "write", 1 ), undef, "can not write lock after read lock" );
+}
+
+Test::IData->setup;
+
+{
+    my $idata = PS::IPP::IData::Client->new(
+        proxy => "http://$hostport/idata",
+    );
+    $idata->create( "foo" );
+
+    $idata->lock( "foo", "write" );
+    is( $idata->lock( "foo", "read", 1 ), undef, "can not read lock after write lock" );
+}
+
+Test::IData->setup;
+
+eval {
+    my $idata = PS::IPP::IData::Client->new(
+        proxy => "http://$hostport/idata",
+    );
+
+    $idata->lock();
+};
+like( $@, qr/2 - 3 were expected/, "no params" );
+
+Test::IData->setup;
+
+eval {
+    my $idata = PS::IPP::IData::Client->new(
+        proxy => "http://$hostport/idata",
+    );
+
+    $idata->lock( "foo" );
+};
+like( $@, qr/2 - 3 were expected/, "not enough params" );
+
+Test::IData->setup;
+
+eval {
+    my $idata = PS::IPP::IData::Client->new(
+        proxy => "http://$hostport/idata",
+    );
+
+    $idata->lock( "foo", "both" );
+};
+like( $@, qr/is read or write/, "not read or write" );
+
+Test::IData->setup;
+
+eval {
+    my $idata = PS::IPP::IData::Client->new(
+        proxy => "http://$hostport/idata",
+    );
+
+    $idata->lock( "foo", 'read', 3, "bar" );
+};
+like( $@, qr/2 - 3 were expected/, "too many params" );
+
+Test::IData->cleanup;
Index: /branches/jhoblitt/Nebulous/t/15_client_unlock.t
===================================================================
--- /branches/jhoblitt/Nebulous/t/15_client_unlock.t	(revision 2784)
+++ /branches/jhoblitt/Nebulous/t/15_client_unlock.t	(revision 2784)
@@ -0,0 +1,166 @@
+#!/usr/bin/perl
+
+use strict;
+use warnings;
+
+use Apache::Test qw( -withtestmore );
+
+plan tests => 14;
+
+use lib qw( ./t ./lib );
+
+use PS::IPP::IData::Client;
+use PS::IPP::IData::Util qw( :standard );
+use Test::IData;
+
+my $hostport = Apache::Test->config->{ 'hostport' };
+
+Test::IData->setup;
+
+{
+    my $idata = PS::IPP::IData::Client->new(
+        proxy => "http://$hostport/idata",
+    );
+    $idata->create( "foo" );
+    $idata->lock( "foo", "read" );
+
+    ok( $idata->unlock( "foo", "read" ), "read unlock" );
+}
+
+Test::IData->setup;
+
+{
+    my $idata = PS::IPP::IData::Client->new(
+        proxy => "http://$hostport/idata",
+    );
+    $idata->create( "foo" );
+    $idata->lock( "foo", "read" );
+    $idata->lock( "foo", "read" );
+
+    ok( $idata->unlock( "foo", "read" ), "read unlock" );
+    ok( $idata->unlock( "foo", "read" ), "read unlock" );
+}
+
+Test::IData->setup;
+
+{
+    my $idata = PS::IPP::IData::Client->new(
+        proxy => "http://$hostport/idata",
+    );
+    $idata->create( "foo" );
+    $idata->lock( "foo", "write" );
+
+    ok( $idata->unlock( "foo", "write" ), "write unlock" );
+}
+
+Test::IData->setup;
+
+{
+    my $idata = PS::IPP::IData::Client->new(
+        proxy => "http://$hostport/idata",
+    );
+
+    is( $idata->unlock( "foo", "read" ), undef, "storage object does not exist" );
+}
+
+Test::IData->setup;
+
+{
+    my $idata = PS::IPP::IData::Client->new(
+        proxy => "http://$hostport/idata",
+    );
+
+    is( $idata->unlock( "foo", "write" ), undef, "storage object does not exist" );
+}
+
+Test::IData->setup;
+
+{
+    my $idata = PS::IPP::IData::Client->new(
+        proxy => "http://$hostport/idata",
+    );
+    $idata->create( "foo" );
+
+    is( $idata->unlock( "foo", "read" ), undef, "no lock set" );
+}
+
+Test::IData->setup;
+
+{
+    my $idata = PS::IPP::IData::Client->new(
+        proxy => "http://$hostport/idata",
+    );
+    $idata->create( "foo" );
+
+    is( $idata->unlock( "foo", "write" ), undef, "no lock set" );
+}
+
+Test::IData->setup;
+
+{
+    my $idata = PS::IPP::IData::Client->new(
+        proxy => "http://$hostport/idata",
+    );
+    $idata->create( "foo" );
+    $idata->lock( "foo", "write" );
+
+    is( $idata->unlock( "foo", "read" ), undef, "read unlock under write lock" );
+}
+
+Test::IData->setup;
+
+{
+    my $idata = PS::IPP::IData::Client->new(
+        proxy => "http://$hostport/idata",
+    );
+    $idata->create( "foo" );
+    $idata->lock( "foo", "read" );
+
+    is( $idata->unlock( "foo", "write" ), undef, "write unlock under read lock" );
+}
+
+Test::IData->setup;
+
+eval {
+    my $idata = PS::IPP::IData::Client->new(
+        proxy => "http://$hostport/idata",
+    );
+
+    $idata->unlock();
+};
+like( $@, qr/2 were expected/, "no params" );
+
+Test::IData->setup;
+
+eval {
+    my $idata = PS::IPP::IData::Client->new(
+        proxy => "http://$hostport/idata",
+    );
+
+    $idata->unlock( "foo" );
+};
+like( $@, qr/2 were expected/, "not enough params" );
+
+Test::IData->setup;
+
+eval {
+    my $idata = PS::IPP::IData::Client->new(
+        proxy => "http://$hostport/idata",
+    );
+
+    $idata->unlock( "foo", "both" );
+};
+like( $@, qr/is read or write/, "not read or write" );
+
+Test::IData->setup;
+
+eval {
+    my $idata = PS::IPP::IData::Client->new(
+        proxy => "http://$hostport/idata",
+    );
+
+    $idata->unlock( "foo", 'read', 3 );
+};
+like( $@, qr/2 were expected/, "too many params" );
+
+Test::IData->cleanup;
Index: /branches/jhoblitt/Nebulous/t/16_client_find_instances.t
===================================================================
--- /branches/jhoblitt/Nebulous/t/16_client_find_instances.t	(revision 2784)
+++ /branches/jhoblitt/Nebulous/t/16_client_find_instances.t	(revision 2784)
@@ -0,0 +1,114 @@
+#!/usr/bin/perl
+
+use strict;
+use warnings;
+
+use Apache::Test qw( -withtestmore );
+
+plan tests => 7;
+
+use lib qw( ./t ./lib );
+
+use PS::IPP::IData::Client;
+use PS::IPP::IData::Util qw( :standard );
+use Test::IData;
+
+my $hostport = Apache::Test->config->{ 'hostport' };
+
+Test::IData->setup;
+
+SKIP: {
+    skip "need to write a module to validate URIs", 7;
+
+{
+    # key
+    my $idata = PS::IPP::IData::Client->new(
+        proxy => "http://$hostport/idata",
+    );
+    my $uri = $idata->create( "foo" );
+
+    my $locations = $idata->find_instances( "foo" );
+
+    is( $uri, @$locations[0], "URIs match" );
+}
+
+Test::IData->setup;
+
+{
+    # key
+    my $idata = PS::IPP::IData::Client->new(
+        proxy => "http://$hostport/idata",
+    );
+    my $uri1 = $idata->create( "foo" );
+    my $uri2 = $idata->replicate( "foo" );
+
+    my $locations = $idata->find_instances( "foo" );
+
+    ok( eq_set( [$uri1, $uri2], $locations ), "URIs match" );
+}
+
+Test::IData->setup;
+
+{
+    # key, volume
+    my $idata = PS::IPP::IData::Client->new(
+        proxy => "http://$hostport/idata",
+    );
+    my $uri = $idata->create( "foo" );
+
+    my $locations = $idata->find_instances( "foo", "node01" );
+
+    is( $uri, $locations->[0], "URIs match" );
+}
+
+Test::IData->setup;
+
+{
+    # key
+    my $idata = PS::IPP::IData::Client->new(
+        proxy => "http://$hostport/idata",
+    );
+    my $uri1 = $idata->create( "foo" );
+    my $uri2 = $idata->replicate( "foo" );
+
+    my $locations = $idata->find_instances( "foo", "node01" );
+
+    ok( eq_set( [$uri1, $uri2], $locations ), "URIs match" );
+}
+
+
+Test::IData->setup;
+
+{
+    my $idata = PS::IPP::IData::Client->new(
+        proxy => "http://$hostport/idata",
+    );
+
+    is( $idata->find_instances( "foo" ), undef, "storage object does not exist" );
+}
+
+Test::IData->setup;
+
+eval {
+    my $idata = PS::IPP::IData::Client->new(
+        proxy => "http://$hostport/idata",
+    );
+
+    $idata->find_instances();
+};
+like( $@, qr/1 - 2 were expected/, "no params" );
+
+Test::IData->setup;
+
+eval {
+    my $idata = PS::IPP::IData::Client->new(
+        proxy => "http://$hostport/idata",
+    );
+
+    $idata->find_instances( "foo", 'read', 3 );
+};
+like( $@, qr/1 - 2 were expected/, "too many params" );
+
+Test::IData->cleanup;
+
+}
Index: /branches/jhoblitt/Nebulous/t/TEST.PL
===================================================================
--- /branches/jhoblitt/Nebulous/t/TEST.PL	(revision 2784)
+++ /branches/jhoblitt/Nebulous/t/TEST.PL	(revision 2784)
@@ -0,0 +1,12 @@
+#file:t/TEST.PL
+#--------------
+#!perl
+
+use strict;
+use warnings FATAL => 'all';
+
+use lib qw(lib);
+
+use Apache::TestRunPerl ();
+
+Apache::TestRunPerl->new->run(@ARGV);
Index: /branches/jhoblitt/Nebulous/t/Test/Nebulous.pm
===================================================================
--- /branches/jhoblitt/Nebulous/t/Test/Nebulous.pm	(revision 2784)
+++ /branches/jhoblitt/Nebulous/t/Test/Nebulous.pm	(revision 2784)
@@ -0,0 +1,113 @@
+# Copyright (C) 2004  Joshua Hoblitt
+#
+# $Id: Nebulous.pm,v 1.1.1.1 2004-12-22 02:16:23 jhoblitt Exp $
+
+package Test::IData;
+
+use strict;
+
+use DBI;
+use File::Path qw( mkpath rmtree );
+use File::Temp qw( tempdir );
+
+my $dsn     = "DBI:mysql:database=test:host=localhost";
+my $user    = "test";
+my $passwd  = "";
+
+my $dbh = DBI->connect( $dsn, $user, $passwd );
+
+#my $dir1 = tempdir( CLEANUP => 0 );
+#my $dir2 = tempdir( CLEANUP => 0 );
+
+my $dir1 = "/tmp/foo1";
+my $dir2 = "/tmp/foo1";
+
+
+sub setup {
+    my $self = shift;
+
+    $self->cleanup;
+
+    mkpath([$dir1, $dir2]);
+
+    local $/ = ';';
+
+    my $pos = tell DATA;
+
+    foreach my $statement (<DATA>) {
+        last unless ( $statement =~ /\S+/ );
+        $dbh->do( $statement );
+    }
+
+    seek DATA, $pos, 0;
+
+    # FIXME
+    $dbh->do( qq{ INSERT INTO volume VALUES (1,'node01',?) }, undef, 'file:' . $dir1 );
+    $dbh->do( qq{ INSERT INTO volume VALUES (2,'node02',?) }, undef, 'file:' . $dir2 );
+}
+
+sub cleanup {
+    $dbh->do( "DROP TABLE IF EXISTS storage_object" );
+    $dbh->do( "DROP TABLE IF EXISTS instance" );
+    $dbh->do( "DROP TABLE IF EXISTS lock_record" );
+    $dbh->do( "DROP TABLE IF EXISTS volume" );
+    $dbh->do( "DROP TABLE IF EXISTS class" );
+
+    rmtree([$dir1, $dir2], 0, 1);
+}
+
+1;
+
+__DATA__
+DROP TABLE IF EXISTS storage_object;
+CREATE TABLE storage_object (
+    so_id BIGINT NOT NULL AUTO_INCREMENT,
+    ext_id VARCHAR(255) BINARY NOT NULL UNIQUE,
+    class_id TINYINT NOT NULL,
+    comment VARCHAR(255),
+    read_lock TINYINT DEFAULT 0 NOT NULL,
+    write_lock ENUM( 'write' ),
+    epoch TIMESTAMP NOT NULL,
+    PRIMARY KEY(so_id),
+    KEY(ext_id(64)),
+    KEY(class_id)
+) ENGINE=innodb;
+
+DROP TABLE IF EXISTS instance;
+CREATE TABLE instance (
+    ins_id BIGINT NOT NULL AUTO_INCREMENT,
+    so_id BIGINT NOT NULL,
+    uri VARCHAR(255) BINARY NOT NULL UNIQUE,
+    sha1sum CHAR(40),
+    assigned_location BOOL,
+    epoch TIMESTAMP NOT NULL,
+    atime TIMESTAMP NOT NULL,
+    PRIMARY KEY(ins_id),
+    KEY(so_id),
+    KEY(uri(64))
+) ENGINE=innodb;
+
+DROP TABLE IF EXISTS lock_record;
+CREATE TABLE lock_record (
+        so_id BIGINT NOT NULL,
+        type ENUM( 'read', 'write' ) NOT NULL,
+        epoch TIMESTAMP,
+        KEY(so_ID)
+    ) ENGINE=innodb;
+
+DROP TABLE IF EXISTS volume;
+CREATE TABLE volume (
+        vol_id INT NOT NULL AUTO_INCREMENT,
+        name VARCHAR(255) UNIQUE BINARY NOT NULL,
+        uri VARCHAR(255) BINARY NOT NULL,
+        key(vol_id)
+    ) ENGINE=innodb;
+
+DROP TABLE IF EXISTS class;
+CREATE TABLE class (
+        class_id TINYINT NOT NULL AUTO_INCREMENT,
+        instance TINYINT NOT NULL,
+        priority TINYINT,
+        comment VARCHAR(255),
+        key(class_id)
+    ) ENGINE=innodb;
Index: /branches/jhoblitt/Nebulous/t/apache_03_server_create_object.x
===================================================================
--- /branches/jhoblitt/Nebulous/t/apache_03_server_create_object.x	(revision 2784)
+++ /branches/jhoblitt/Nebulous/t/apache_03_server_create_object.x	(revision 2784)
@@ -0,0 +1,95 @@
+#!/usr/bin/perl
+
+use strict;
+use warnings;
+
+use Apache::Test qw(-withtestmore);
+
+plan tests => 11;
+
+use lib qw( ./t ./lib );
+
+use Apache::TestUtil;
+use PS::IPP::IData::Util qw( :standard );
+use SOAP::Lite;
+use Test::IData;
+
+my $hostport = Apache::Test::config()->{ 'hostport' };
+my $idata = SOAP::Lite->new(
+    proxy   => "http://$hostport/idata",
+    uri     => "http://pan-starrs.ifa.hawaii.edu/PS/IPP/IData/Server",
+);
+
+Test::IData->setup;
+
+{
+    # key
+    my $uri = $idata->create_object( "foo" )->result;
+
+    ok( -e _get_file_path( $uri ), "file exists" );
+
+    like( $uri, qr/^file:\//, "valid URI" );
+}
+
+Test::IData->setup;
+
+{
+    # key, class
+    my $uri = $idata->create_object( "foo", 0 )->result;
+    $idata->delete_instance( $uri );
+
+    ok( -e _get_file_path( $uri ), "file exists" );
+
+    like( $uri, qr/^file:\//, "valid URI" );
+}
+
+Test::IData->setup;
+
+{
+    # key, class, volume
+    my $uri = $idata->create_object( "foo", 0, "node01" )->result;
+    $idata->delete_instance( $uri );
+
+    ok( -e _get_file_path( $uri ), "file exists" );
+
+    like( $uri, qr/^file:\//, "valid URI" );
+}
+
+Test::IData->setup;
+
+{
+    # key, class, volume, comment
+    my $uri = $idata->create_object( "foo", 0, "node01", "this is foo" )->result;
+    $idata->delete_instance( $uri );
+
+    ok( -e _get_file_path( $uri ), "file exists" );
+
+    like( $uri, qr/^file:\//, "valid URI" );
+}
+
+Test::IData->setup;
+
+{
+    $idata->create_object( "foo" );
+    my $response = $idata->create_object( "foo" );
+
+    like( $response->faultstring, qr/Duplicate entry/, "object already exists" );
+}
+
+Test::IData->setup;
+
+{
+    my $response = $idata->create_object();
+
+    like( $response->faultstring, qr/2 - 4 were expected/, "no params" );
+}
+
+Test::IData->setup;
+
+{
+    my $response = $idata->create_object( 1, 2, 3, 4, 5 );
+
+    like( $response->faultstring, qr/2 - 4 were expected/, "too many params" );
+}
+
+Test::IData->cleanup;
Index: /branches/jhoblitt/Nebulous/t/apache_04_server_replicate_object.x
===================================================================
--- /branches/jhoblitt/Nebulous/t/apache_04_server_replicate_object.x	(revision 2784)
+++ /branches/jhoblitt/Nebulous/t/apache_04_server_replicate_object.x	(revision 2784)
@@ -0,0 +1,86 @@
+#!/usr/bin/perl
+
+use strict;
+use warnings;
+
+use Apache::Test qw(-withtestmore);
+
+plan tests => 11;
+
+use lib qw( ./t ./lib );
+
+use Apache::TestUtil;
+use PS::IPP::IData::Util qw( :standard );
+use SOAP::Lite;
+use Test::IData;
+
+my $hostport = Apache::Test::config()->{ 'hostport' };
+my $idata = SOAP::Lite->new(
+    proxy   => "http://$hostport/idata",
+    uri     => "http://pan-starrs.ifa.hawaii.edu/PS/IPP/IData/Server",
+);
+
+Test::IData->setup;
+
+{
+    # key
+    $idata->create_object( "foo" );
+    my $uri = $idata->replicate_object( "foo" )->result;
+
+    ok( -e _get_file_path( $uri ), "file exists" );
+
+    like( $uri, qr/^file:\//, "valid URI" );
+}
+
+Test::IData->setup;
+
+{
+    # key, $volume
+    $idata->create_object( "foo" );
+    my $uri = $idata->replicate_object( "foo", "node01" )->result;
+
+    ok( -e _get_file_path( $uri ), "file exists" );
+
+    like( $uri, qr/^file:\//, "valid URI" );
+}
+
+Test::IData->setup;
+
+{
+    # key, $volume
+    $idata->create_object( "foo" );
+    my $uri1 = $idata->replicate_object( "foo", "node01" )->result;
+    my $uri2 = $idata->replicate_object( "foo", "node02" )->result;
+
+    ok( -e _get_file_path( $uri1 ), "file exists" );
+    ok( -e _get_file_path( $uri2 ), "file exists" );
+
+    like( $uri1, qr/^file:\//, "valid URI" );
+    like( $uri2, qr/^file:\//, "valid URI" );
+}
+
+Test::IData->setup;
+
+{
+    my $response = $idata->replicate_object();
+
+    like( $response->faultstring, qr/1 - 2 were expected/, "no params" );
+}
+
+Test::IData->setup;
+
+{
+    my $response = $idata->replicate_object( 1, 2, 3 );
+
+    like( $response->faultstring, qr/1 - 2 were expected/, "too many params" );
+}
+
+Test::IData->setup;
+
+{
+    my $response = $idata->replicate_object( "foo" );
+
+    like( $response->faultstring, qr/storage object does not exist/, "storage object does not exist" );
+}
+
+Test::IData->cleanup;
Index: /branches/jhoblitt/Nebulous/t/apache_05_server_lock_object.x
===================================================================
--- /branches/jhoblitt/Nebulous/t/apache_05_server_lock_object.x	(revision 2784)
+++ /branches/jhoblitt/Nebulous/t/apache_05_server_lock_object.x	(revision 2784)
@@ -0,0 +1,129 @@
+#!/usr/bin/perl
+
+use strict;
+use warnings;
+
+use Apache::Test qw(-withtestmore);
+
+plan tests => 13;
+
+use lib qw( ./t ./lib );
+
+use Apache::TestUtil;
+use PS::IPP::IData::Util qw( :standard );
+use SOAP::Lite;
+use Test::IData;
+
+my $hostport = Apache::Test::config()->{ 'hostport' };
+my $idata = SOAP::Lite->new(
+    proxy   => "http://$hostport/idata",
+    uri     => "http://pan-starrs.ifa.hawaii.edu/PS/IPP/IData/Server",
+);
+
+Test::IData->setup;
+
+{
+    $idata->create_object( "foo" );
+
+    ok( $idata->lock_object( "foo", "read" )->result, "read lock" );
+}
+
+Test::IData->setup;
+
+{
+    $idata->create_object( "foo" );
+
+    ok( $idata->lock_object( "foo", "read" )->result, "read lock" );
+    ok( $idata->lock_object( "foo", "read" )->result, "read lock" );
+}
+
+Test::IData->setup;
+
+{
+    $idata->create_object( "foo" );
+
+    ok( $idata->lock_object( "foo", "write" )->result, "write lock" );
+}
+
+Test::IData->setup;
+
+{
+    my $result = $idata->lock_object( "foo", "read" );
+
+    like( $result->faultstring, qr/storage object does not exist/, "storage object does not exist" );
+}
+
+Test::IData->setup;
+
+{
+    my $result = $idata->lock_object( "foo", "write" );
+
+    like( $result->faultstring, qr/storage object does not exist/, "storage object does not exist" );
+}
+
+Test::IData->setup;
+
+{
+    $idata->create_object( "foo" );
+
+    $idata->lock_object( "foo", "write" );
+    my $result = $idata->lock_object( "foo", "write" );
+
+    like( $result->faultstring, qr/can not write lock twice/, "can not write lock twice" );
+}
+
+Test::IData->setup;
+
+{
+    $idata->create_object( "foo" );
+
+    $idata->lock_object( "foo", "read" );
+    my $result = $idata->lock_object( "foo", "write" );
+
+    like( $result->faultstring, qr/can not write lock after read lock/, "can not write lock after read lock" );
+}
+
+Test::IData->setup;
+
+{
+    $idata->create_object( "foo" );
+
+    $idata->lock_object( "foo", "write" );
+    my $result = $idata->lock_object( "foo", "read" );
+
+    like( $result->faultstring, qr/can not read lock after write lock/, "can not read lock after write lock" );
+}
+
+Test::IData->setup;
+
+{
+    my $result = $idata->lock_object();
+
+    like( $result->faultstring, qr/2 were expected/, "no params" );
+}
+
+Test::IData->setup;
+
+{
+    my $result = $idata->lock_object( "foo" );
+
+    like( $result->faultstring, qr/2 were expected/, "not enough params" );
+}
+
+Test::IData->setup;
+
+{
+    my $result = $idata->lock_object( "foo", "both" );
+
+    like( $result->faultstring, qr/is read or write/, "not read or write" );
+}
+
+Test::IData->setup;
+
+{
+    my $result = $idata->lock_object( "foo", 'read', 3 );
+
+    like( $result->faultstring, qr/2 were expected/, "too many params" );
+}
+
+Test::IData->cleanup;
Index: /branches/jhoblitt/Nebulous/t/conf/extra.conf.in
===================================================================
--- /branches/jhoblitt/Nebulous/t/conf/extra.conf.in	(revision 2784)
+++ /branches/jhoblitt/Nebulous/t/conf/extra.conf.in	(revision 2784)
@@ -0,0 +1,31 @@
+#file:t/conf/extra.conf.in
+#-------------------------
+# this file will be Include-d by @ServerRoot@/httpd.conf
+
+# where PS::IPP::IData::Server::Apache can be found
+#PerlSwitches -I@ServerRoot@/../lib
+<perl>
+use lib qw( @ServerRoot@/../lib );
+</perl>
+
+PerlModule Apache::DBI
+PerlModule DBI
+PerlModule PS::IPP::IData::Server::Apache
+PerlModule PS::IPP::IData::Server
+
+<perl>
+Apache->push_handlers(
+    PerlChildInitHandler => sub {
+        PS::IPP::IData::Server->setup(
+            "DBI:mysql:database=test:host=localhost",
+            "test",
+            "",
+        );
+    },
+);
+</perl>
+
+<Location /idata>
+    SetHandler perl-script
+    PerlHandler PS::IPP::IData::Server::Apache
+</Location>
