Index: /branches/ccl_branches/ipponly-20191108/DataStoreServer/Makefile.am
===================================================================
--- /branches/ccl_branches/ipponly-20191108/DataStoreServer/Makefile.am	(revision 41111)
+++ /branches/ccl_branches/ipponly-20191108/DataStoreServer/Makefile.am	(revision 41111)
@@ -0,0 +1,3 @@
+SUBDIRS = scripts
+
+CLEANFILES = *~ core core.*
Index: /branches/ccl_branches/ipponly-20191108/DataStoreServer/autogen.sh
===================================================================
--- /branches/ccl_branches/ipponly-20191108/DataStoreServer/autogen.sh	(revision 41111)
+++ /branches/ccl_branches/ipponly-20191108/DataStoreServer/autogen.sh	(revision 41111)
@@ -0,0 +1,103 @@
+#!/bin/sh
+# Run this to generate all the initial makefiles, etc.
+
+srcdir=`dirname $0`
+test -z "$srcdir" && srcdir=.
+
+ORIGDIR=`pwd`
+cd $srcdir
+
+PROJECT=DataStoreServer
+TEST_TYPE=-f
+# change this to be a unique filename in the top level dir
+FILE=autogen.sh
+
+DIE=0
+
+LIBTOOLIZE=libtoolize
+ACLOCAL="aclocal $ACLOCAL_FLAGS"
+AUTOHEADER=autoheader
+AUTOMAKE=automake
+AUTOCONF=autoconf
+
+#($LIBTOOLIZE --version) < /dev/null > /dev/null 2>&1 || {
+#        echo
+#        echo "You must have $LIBTOOLIZE installed to compile $PROJECT."
+#        echo "Download the appropriate package for your distribution,"
+#        echo "or get the source tarball at http://ftp.gnu.org/gnu/libtool/"
+#        DIE=1
+#}
+
+($ACLOCAL --version) < /dev/null > /dev/null 2>&1 || {
+        echo
+        echo "You must have $ACLOCAL installed to compile $PROJECT."
+        echo "Download the appropriate package for your distribution,"
+        echo "or get the source tarball at http://ftp.gnu.org/gnu/automake/"
+        DIE=1
+}
+
+#($AUTOHEADER --version) < /dev/null > /dev/null 2>&1 || {
+#        echo
+#        echo "You must have $AUTOHEADER installed to compile $PROJECT."
+#        echo "Download the appropriate package for your distribution,"
+#        echo "or get the source tarball at http://ftp.gnu.org/gnu/autoconf/"
+#        DIE=1
+#}
+
+($AUTOMAKE --version) < /dev/null > /dev/null 2>&1 || {
+        echo
+        echo "You must have $AUTOMAKE installed to compile $PROJECT."
+        echo "Download the appropriate package for your distribution,"
+        echo "or get the source tarball at http://ftp.gnu.org/gnu/automake/"
+        DIE=1
+}
+
+($AUTOCONF --version) < /dev/null > /dev/null 2>&1 || {
+        echo
+        echo "You must have $AUTOCONF installed to compile $PROJECT."
+        echo "Download the appropriate package for your distribution,"
+        echo "or get the source tarball at http://ftp.gnu.org/gnu/autoconf/"
+        DIE=1
+}
+
+if test "$DIE" -eq 1; then
+        exit 1
+fi
+
+test $TEST_TYPE $FILE || {
+        echo "You must run this script in the top-level $PROJECT directory"
+        exit 1
+}
+
+if test -z "$*"; then
+        echo "I am going to run ./configure with no arguments - if you wish "
+        echo "to pass any to it, please specify them on the $0 command line."
+fi
+
+#$LIBTOOLIZE --copy --force || echo "$LIBTOOLIZE failed"
+$ACLOCAL || echo "$ACLOCAL failed"
+#$AUTOHEADER || echo "$AUTOHEADER failed"
+$AUTOMAKE --add-missing --force-missing --copy || echo "$AUTOMAKE failed"
+$AUTOCONF || echo "$AUTOCONF failed"
+
+cd $ORIGDIR
+
+run_configure=true
+for arg in $*; do
+    case $arg in
+        --no-configure)
+            run_configure=false
+            ;;
+        *)
+            ;;
+    esac
+done
+
+if $run_configure; then
+    $srcdir/configure --enable-maintainer-mode "$@"
+    echo
+    echo "Now type 'make' to compile $PROJECT."
+else
+    echo
+    echo "Now run 'configure' and 'make' to compile $PROJECT."
+fi
Index: /branches/ccl_branches/ipponly-20191108/DataStoreServer/configure.ac
===================================================================
--- /branches/ccl_branches/ipponly-20191108/DataStoreServer/configure.ac	(revision 41111)
+++ /branches/ccl_branches/ipponly-20191108/DataStoreServer/configure.ac	(revision 41111)
@@ -0,0 +1,16 @@
+AC_PREREQ(2.59)
+
+AC_INIT([DataStoreServer], [1.0.0], [ipp-support@ifa.hawaii.edu])
+AC_CONFIG_SRCDIR([scripts])
+
+AM_INIT_AUTOMAKE([1.6 foreign dist-bzip2])
+AM_MAINTAINER_MODE
+
+AC_PROG_INSTALL
+
+AC_CONFIG_FILES([
+  Makefile
+  scripts/Makefile
+])
+
+AC_OUTPUT
Index: /branches/ccl_branches/ipponly-20191108/DataStoreServer/doc/IPPDataStore.tex
===================================================================
--- /branches/ccl_branches/ipponly-20191108/DataStoreServer/doc/IPPDataStore.tex	(revision 41111)
+++ /branches/ccl_branches/ipponly-20191108/DataStoreServer/doc/IPPDataStore.tex	(revision 41111)
@@ -0,0 +1,306 @@
+The purpose of this note is to describe the implementation of the IPP Data Store.
+Introduction
+The DataStore client interface is defined in document PSDC-940-010-02 which we will hereafter refer to as DSDoc. Basically the DataStore server provides clients with a directory listing and file transfer interface through HTTP. 
+The Data Store virtual file system is arranged in four levels
+1. The root of the Data Store.   Which contains
+2. One or more products.         Which contain
+3. One or more file sets.            Which contain
+4. One or more files.
+The Data Store provides specialized indexing for the first three levels as described in section 3.2 of DSDoc. 
+As an example the IPP Data Store is required to support a number of different products
+* Image Quality and data validity for OTIS
+* Transient Detections, Detectability results for MOPS
+* Postage Stamp Images
+* Postage Stamp Requests (internal to the web interface of the Postage  Stamp Server)
+
+Each product will be visible at the root of the Data Store. As described in DSDoc, HTTP Requests to URLs with basename index.txt return lists in a simple easy to parse format. For example, an http request to the URL http://ippdatastore/ds/index.txt might return the following list of products
+
+# productID  |Most recent |Time registered     |type     |Description                   |
+pstamprequest|none        |2008-02-28T09:01:54Z|psrequest|Postage Stamp Request         |
+pstampresults|test        |2008-03-01T02:18:15Z|psresults|Postage Stamp Request Results |
+ipp_otis     |tootis080305|2008-03-04T22:43:00Z|image    |IPP to OTIS Data Store        |
+ipp_mops     |tomops080305|2008-03-04T22:45:42Z|image    |IPP to MOPS Data Store        |
+
+If index.txt is omitted from the URL, the list is returned in a more readable html format complete with links into the products. The Most recent column gives the name of the file set most recently added to each product and Time registered gives the time that that fileset was added to the Data Store.
+An interesting feature of the interface is related to file set indexing. If the user sets the query string of the URL (the searchpart defined in rfc1738) to the name of an existing file set, the server will return a list that contains only those file sets that were registered later time than the given file set. This makes it easy for clients to find out whats new.
+
+For example if the URL http://ippdatastore/ds/pstampresults/index.txt returns
+# filesetID|time registered     |type     |
+fileset13  |2008-03-01T01:52:18Z|PSRESULTS|
+nfileset1  |2008-03-01T01:54:46Z|PSRESULTS|
+nfileset2  |2008-03-01T01:55:00Z|PSRESULTS|
+fileset43  |2008-03-01T02:01:29Z|PSRESULTS|
+test       |2008-03-01T02:18:15Z|PSRESULTS|
+
+the URL http://ippdatastore/ds/pstampresults/index.txt?nfileset2 would yield
+# filesetID|time registered     |type     |
+fileset43  |2008-03-01T02:01:29Z|PSRESULTS|
+test       |2008-03-01T02:18:15Z|PSRESULTS| 
+
+the set of filesets newer than nfileset2. This feature requires that the Data Stores implementation of the fileset lists be more sophisticated than the simple text file that the URL implies.
+There are other considerations that led us to not try and manage the Data Store contents using simple text file indexes. Since registration requires indexes at multiple levels to be changed, and since multiple clients can be manipulating the Data Store simultaneously, keeping text file indexes consistent just using files in the file system would be problematic.
+Instead we store the contents of the DataStore in an SQL database tables.  The indexes are built on demand using cgi scripts.
+Web Implementation
+Note: although we are beginning our discussion with the specifics of the web interface, this is probably the least complex and concrete part of the DS implementation.
+The web interface for the Data Store is implemented using Apache HTTP server.  The Data Store is configured by adding the file httpd-datastore.conf to the Apache configuration directory /etc/httpd/conf.d.
+ A few directives are used.
+<Directory "/var/www/html/ds/dsroot">
+
+  DirectoryIndex /ds-cgi/dsindex.cgi
+
+  Options +Includes
+
+  # files will be processed for server side includes if they have the
+  # executable bit set (note: this doesnt seem to be necessary)
+  XBitHack on
+
+  AddOutputFilter INCLUDES .txt
+
+  AllowOverride None
+
+  Order allow,deny
+  Allow from all
+</Directory>
+
+<Directory "/cgi-bin/ds-cgi">
+  Options ExecCGI
+
+
+  Order allow,deny
+  Allow from all
+</Directory>
+
+Alias       /ds/     /var/www/html/ds/dsroot/
+ScriptAlias /ds-cgi/ /var/www/cgi-bin/ds-cgi/
+
+This configuration file is written for a set up where the root of the Data Store file system is located at /var/www/html/ds/dsroot/ and http clients see the root of the Data Store at http://server/ds
+The cgi scripts live in /var/www/cgi-bin/ds-cgi which is aliased to /ds-cgi.
+The directories in the Data Store file system are arranged to match the data. That is, product productA is represented by a directory productA in the Data Store root directory. The directory productA contains a subdirectory for each registered file set and in these directories are either the file sets constituent files themselves or to their actual location.  NOTE: locating the files outside the Data Store is not implemented yet.
+The DirectoryIndex directive in the configuration file redirects index requests for all directories to the script dsindex.cgi.
+The index re-directions are intended to insure that only registered products and file sets are available for access over the web interface. Other directories and files may in fact be in the file systems (file sets under construction for example) but they arent visible.
+ISSUE: other directories in the file system arent listable. The dsindex script will return not found, but if an actual file name is known by the client then it will be accessible. Is this a problem?
+dsindex.cgi
+This script is simply
+#!/bin/sh
+PATH_TO_COMMAND/pstamp_runcommand.sh dsgetindex $REQUEST_URI -html 
+
+pstamp_runcommand.sh is a (now misnamed) wrapper script that sets up the environment to run IPP commands on behalf of the web server user apache. That program is listed in appendix A. dsgetindex is a perl script that computes the index corresponding to the URI by querying the data base. 
+The environment variable REQUEST_URI is the set by Apache to the requested URL. For example /ds or /ds/pstampresults
+
+
+
+Index.txt
+The Data Store implementation places a file named index.txt in each registered directory in the Data Store.  The contents of each of these files is the same:
+<!--#include virtual="/ds-cgi/dstxtindex.cgi" -->
+
+That is, index.txt redirects the web server to the cgi script dstxtindex.cgi which is yet another wrapper script
+#!/bin/sh
+PATH_TO_COMMAND/pstamp_runcommand.sh dsgetindex $REQUEST_URI;
+
+ (Note: there are probably better implementations for the index.txt redirection but I havent found any yet. This seems to work fine).
+
+The interesting bit of the web implementation: dsgetindex
+As might be inferred from the last couple of sections the interesting bit is contained in the program dsgetindex. 
+dsgetindex takes the REQUEST_URI from the web server and decides whether it is an index request for the Data Store root, a product, or an individual file set. Based on this level it runs one of the following commands to query the database for the appropriate index
+* dsrootindex
+* dsprodindex
+* dsfsindex
+If the parameter html is provided to dsgetindex the output of these commands is passed through a filter to create a user friendly HTML page.
+The 3 index programs are perl scripts that use the DBI package to access the mysql database. Currently IPP interfaces are only used for looking up configuration information. Thus it would be easy to modify this Data Store implementation to run outside of the IPP environment.
+NOTE: We need to add the statistics specified by the DSDoc.
+ 
+
+Data Store Index Tables
+The logical contents of the Data Store is represented by 4 SQL tables.
+* dsProduct
+* dsFileset
+* dsFile
+* dsFileType
+The tables are defined in the usual ipp style where each row has an integer id which is the primary key. 
+The product table is defined as
+mysql> describe dsProduct;
++-------------+--------------+------+-----+---------+----------------+
+| Field       | Type         | Null | Key | Default | Extra          |
++-------------+--------------+------+-----+---------+----------------+
+| prod_id     | bigint(20)   | NO   | PRI | NULL    | auto_increment | 
+| prod_name   | varchar(64)  | NO   | UNI |         |                | 
+| last_update | datetime     | YES  |     | NULL    |                | 
+| last_fs     | varchar(64)  | YES  |     | NULL    |                | 
+| type        | varchar(64)  | YES  |     | NULL    |                | 
+| description | varchar(255) | YES  |     | NULL    |                | 
+| prod_col_0  | varchar(64)  | YES  |     | NULL    |                | 
+| prod_col_1  | varchar(64)  | YES  |     | NULL    |                | 
+| prod_col_2  | varchar(64)  | YES  |     | NULL    |                | 
+| prod_col_3  | varchar(64)  | YES  |     | NULL    |                | 
+| prod_col_4  | varchar(64)  | YES  |     | NULL    |                | 
+| prod_col_5  | varchar(64)  | YES  |     | NULL    |                | 
+| prod_col_6  | varchar(64)  | YES  |     | NULL    |                | 
+| prod_col_7  | varchar(64)  | YES  |     | NULL    |                | 
++-------------+--------------+------+-----+---------+----------------+
+
+The columns named prod_col_0..7 are used to store the labels for any product specific columns.  For example the DSDoc defines several fields for the gpc1 product: telescope pointing, integration time, filter, airmass, comment.
+A fileset is represented in the database as
++--------------+-------------+------+-----+---------+----------------+
+| Field        | Type        | Null | Key | Default | Extra          |
++--------------+-------------+------+-----+---------+----------------+
+| prod_id      | bigint(20)  | YES  | MUL | NULL    |                | 
+| fileset_id   | bigint(20)  | NO   | PRI | NULL    | auto_increment | 
+| fileset_name | varchar(64) | NO   | UNI |         |                | 
+| reg_time     | datetime    | NO   |     |         |                | 
+| type         | varchar(64) | YES  |     | NULL    |                | 
+| prod_col_0   | varchar(64) | YES  |     | NULL    |                | 
+| prod_col_1   | varchar(64) | YES  |     | NULL    |                | 
+| prod_col_2   | varchar(64) | YES  |     | NULL    |                | 
+| prod_col_3   | varchar(64) | YES  |     | NULL    |                | 
+| prod_col_4   | varchar(64) | YES  |     | NULL    |                | 
+| prod_col_5   | varchar(64) | YES  |     | NULL    |                | 
+| prod_col_6   | varchar(64) | YES  |     | NULL    |                | 
+| prod_col_7   | varchar(64) | YES  |     | NULL    |                | 
++--------------+-------------+------+-----+---------+----------------+
+
+So all filesets in product 2 could be found with
+SELECT * from dsFileset where prod_id = 2;
+
+The columns named prod_col_n give the values of the product specific columns for the fileset.
+ISSUE: fileset_name is mared as UNIQUE but this is too restrictive. The name doesnt have to be unique in dsFileset, it only has to be unique in the product. We could manage this easily by storing prod_name/fileset_name instead of just the fileset_name instead of the. The current dsreg handles the uniqueness requirement by searching the product directory for an existing fileset.
+
+
+The individual files are represented in the table dsFile
+mysql> describe dsFile;
++------------+--------------+------+-----+---------+----------------+
+| Field      | Type         | Null | Key | Default | Extra          |
++------------+--------------+------+-----+---------+----------------+
+| fileset_id | bigint(20)   | YES  | MUL | NULL    |                | 
+| file_id    | bigint(20)   | NO   | PRI | NULL    | auto_increment | 
+| file_name  | varchar(255) | NO   |     |         |                | 
+| bytes      | bigint(20)   | YES  |     | NULL    |                | 
+| md5sum     | varchar(255) | YES  |     | NULL    |                | 
+| type       | varchar(64)  | YES  |     | NULL    |                | 
+| type_col_0 | varchar(64)  | YES  |     | NULL    |                | 
+| type_col_1 | varchar(64)  | YES  |     | NULL    |                | 
+| type_col_2 | varchar(64)  | YES  |     | NULL    |                | 
+| type_col_3 | varchar(64)  | YES  |     | NULL    |                | 
++------------+--------------+------+-----+---------+----------------+
+
+So the list of files in fileset 42 could be found with
+SELECT * from dsFile where fileset_id = 42;
+
+The columns type_col_n contain the values for any columns that are specific to a given file type.
+The names of these colums are contained in the dsFileType table
+mysql> describe dsFileType;
++------------+-------------+------+-----+---------+-------+
+| Field      | Type        | Null | Key | Default | Extra |
++------------+-------------+------+-----+---------+-------+
+| type       | varchar(64) | YES  |     | NULL    |       | 
+| type_col_0 | varchar(64) | YES  |     | NULL    |       | 
+| type_col_1 | varchar(64) | YES  |     | NULL    |       | 
+| type_col_2 | varchar(64) | YES  |     | NULL    |       | 
+| type_col_3 | varchar(64) | YES  |     | NULL    |       | 
++------------+-------------+------+-----+---------+-------+
+
+Currently weve identified the following file types
+* chip (a chip level image file)
+* psrequest (postage stamp request table)
+* psresults (postage stamp results table)
+* pstamp (postage stamp image)
+The chip type has one type specific column the chipname (ota name).
+TODO: we might want to discuss product and fileset types as well. For us they are just labels however.
+ 
+
+Data Store Management
+The script dsregproduct is used to add a product to the data store
+dsregproduct -product myProduct -type image description Data Store for my Images \               		-pc0 telescope pointing         -pc1 etime -pc2 f -pc3 airm \
+         --pc4 comments          
+
+By padding the strings in the product specific column labels the fileset listings will have more regular formatting.
+
+Filesets are added to the data store creating the fileset directory in the data store, populating it with the constituent files and then registering the file set by invoking the program dsreg
+dsreg --add -product myProduct -fileset firstFileset -type SKYFLAT \ 			-pc0 -109.192656 \19.751905 2000 -pc1 129.1 -pc2 g -pc3 1.06 \
+         -pc4 Twilight flat g 2/5 
+         
+dsreg reads the list of files from stdin (TODO: should we give a file argument?) Each line contains
+filename filetype [0  4 type_specific_columns]
+
+dsreg computes the md5sum for each file and looks up the files size and adds the appropriate entries to the database tables dsFile and dsFileset. 
+Filesets may be removed from the database using 
+Dsreg -del myProduct -fileset oldFileset
+
+The registration information is removed from the database. The files however are not removed. TODO: should they be?
+Appendix A
+Note: this program was originally written for the postage stamp server but has found other applications. (To be relocated/renamed)
+pstamp_runcommand.sh
+#!/bin/sh
+###
+#
+# pstamp_runcommand.sh: 
+#
+# Set up the ipp environment to run a ipp commands on behalf of the postage stamp
+# server and execute the command given by the argument list 
+# This script is used by cgi or php scripts.
+
+if [[ $# == 0 ]] ;then
+    echo "usage $0 command" >&2
+    #  EINVAL = 22
+    exit 22
+fi
+
+#### BEGIN LOCAL_CONFIGURATION
+
+# These variables need to be customized for a particular installation
+# XXX: why not pass these on on the command line or in the environment?
+export PSCONFDIR=/export/data0/bills/psconfig
+
+WORK_DIR=/export/data1/bills/pstamp/work
+CMD_DIR=/export/data0/bills/src/ipp/pstamp/scripts:/export/data0/bills/src/ipp/DataStoreServer/web/cgi
+
+#### END LOCAL_CONFIGURATION
+
+export HOME=$WORK_DIR
+
+cd $WORK_DIR
+status=$?
+if [[ $status != 0 ]] ; then
+    echo $0 cannot cd to WORK_DIR: $WORK_DIR status: $status >&2
+    exit $status
+fi
+
+###
+### configure the IPP
+###
+
+source $PSCONFDIR/psconfig.bash default
+status=$?
+if [[ $status != 0 ]] ; then
+    echo error setting up IPP environment >&2
+    exit $status
+fi
+
+###
+### Add our command directory to the path. 
+### XXX: This won't be necessary once the scripts get installed as part of the IPP
+###
+PATH=${CMD_DIR}:${PATH}
+#echo path: $PATH >&2
+#echo command: $* >&2
+
+## make sure we are able to run the desired command
+## XXX: This test is sort of redundant. 
+## We'd get a more useful error by just going ahead and trying the command
+## or by not suppressing the error output of which
+if [[ ! -x `which $1 2> /dev/null` ]] ;then
+    status=$?
+    echo command $1 not found >&2
+    exit $status
+fi
+
+#### Finally, execute the command given by the command line arguments
+
+$*
+
+
+
+
+
+
+
+
Index: /branches/ccl_branches/ipponly-20191108/DataStoreServer/scripts/Makefile.am
===================================================================
--- /branches/ccl_branches/ipponly-20191108/DataStoreServer/scripts/Makefile.am	(revision 41111)
+++ /branches/ccl_branches/ipponly-20191108/DataStoreServer/scripts/Makefile.am	(revision 41111)
@@ -0,0 +1,10 @@
+
+installdir = $(bindir)
+
+install_files = \
+	dsreg \
+        dsprodtool
+
+install_SCRIPTS = $(install_files)
+
+ACLOCAL_AMFLAGS = -I m4
Index: /branches/ccl_branches/ipponly-20191108/DataStoreServer/scripts/dsdbh.pm
===================================================================
--- /branches/ccl_branches/ipponly-20191108/DataStoreServer/scripts/dsdbh.pm	(revision 41111)
+++ /branches/ccl_branches/ipponly-20191108/DataStoreServer/scripts/dsdbh.pm	(revision 41111)
@@ -0,0 +1,20 @@
+# create a database handle based on the configuration given
+# in environment variables
+
+sub getDBHandle {
+    my $dbserver = $ENV{DBSERVER};
+    my $dbuser   = $ENV{DBUSER};
+    my $dbpass   = $ENV{DBPASSWORD};
+    my $dbname   = $ENV{DBNAME};
+
+    die "database enviornment not set up" unless defined($dbserver) and defined($dbuser)
+        and defined($dbpass) and defined($dbname);
+
+    my $dsn = "DBI:mysql:host=$dbserver;database=$dbname";
+
+    my $dbh = DBI->connect($dsn, $dbuser, $dbpass) or die "Cannot connect to server\n";
+
+    return $dbh;
+}
+
+return 1;
Index: /branches/ccl_branches/ipponly-20191108/DataStoreServer/scripts/dsfsindex
===================================================================
--- /branches/ccl_branches/ipponly-20191108/DataStoreServer/scripts/dsfsindex	(revision 41111)
+++ /branches/ccl_branches/ipponly-20191108/DataStoreServer/scripts/dsfsindex	(revision 41111)
@@ -0,0 +1,117 @@
+#!/usr/bin/env perl
+
+# list the filesets in a data store product
+# The only ipp specific code is the function that gets the database handle and the
+# exit status codes
+
+use strict;
+use warnings;
+
+use dsdbh;
+use DBI;
+
+my $PS_EXIT_CONFIG_ERROR = 3;
+my $PS_EXIT_DATA_ERROR = 5;
+
+my $fileset = pop;
+die("must specify fileset to list") unless defined $fileset;
+
+my $product = pop;
+die("must specify product to list") unless defined $product;
+
+my $dbh = getDBHandle();
+
+# Look up the product id
+my $prod_stmt = $dbh->prepare("SELECT prod_id FROM dsProduct WHERE prod_name = '$product'");
+$prod_stmt->execute();
+my $prod = $prod_stmt->fetchrow_hashref();
+if (! defined $prod) {
+    print STDERR "failed to find $product in product list\n";
+    exit ($PS_EXIT_DATA_ERROR);
+}
+
+my $prod_id = $prod->{prod_id};
+
+my $stmt = $dbh->prepare("SELECT fileset_id FROM dsFileset WHERE fileset_name = \'$fileset\'" .
+                    " AND prod_id = $prod_id");
+$stmt->execute();
+my $fs = $stmt->fetchrow_hashref();
+if (!defined $fs) {
+    print STDERR "failed to find $fileset in $product\n";
+    exit ($PS_EXIT_DATA_ERROR);
+}
+
+my $fs_id = $fs->{fileset_id};
+
+
+# get the headers for all of the type specific columns
+# a fileset may contain files with multiple types. We print out a new
+# header each time the type in the list changes.
+# save the header
+
+$stmt = $dbh->prepare("SELECT * FROM dsFileType");
+$stmt->execute();
+my %fileTypes;
+while( my @row = $stmt->fetchrow_array()) {
+    my $type = $row[0];
+    $fileTypes{$type} = \@row;
+}
+
+my $header = "# fileID          |bytes   |md5sum                          |type        |";
+
+$stmt = $dbh->prepare("SELECT * from dsFile WHERE fileset_id = $fs_id ORDER BY file_id");
+$stmt->execute();
+
+my $print_header=1;
+my $last_type = "";
+my $last_header = "";
+while( my $row = $stmt->fetchrow_hashref()) {
+    my $type = $row->{type};
+
+    # if the type changes print out a new header
+    if ($type ne $last_type) {
+        $print_header = 1;
+        $last_type = $type;
+    }
+    if ($print_header) {
+        # add the type specific columns
+        my $typeheader = "";
+        my $fileTypeDef = $fileTypes{$type};
+        for (my $i= 1; $i <=4; $i++) {
+            if (!defined $fileTypeDef->[$i]) {
+                last;
+            }
+            $typeheader .= sprintf "%-8s|", $fileTypeDef->[$i];
+        }
+        my $newheader = "$header$typeheader";
+        # if the header actually changed (due to type specific columns) print it out
+        if ($newheader ne $last_header) {
+            print "$newheader\n";
+            $last_header = $newheader;
+        }
+        $print_header = 0;
+    }
+
+    my $line = sprintf "%-18s|%-8s|%-32s|%-12s|", 
+        $row->{file_name}, $row->{bytes}, $row->{md5sum}, $row->{type};
+
+    # now add any type specific columns that are defined (8 character minimum width is arbitrary)
+    if (defined $row->{type_col_0}) {
+        $line .= sprintf "%-8s|", $row->{type_col_0};
+        if (defined $row->{type_col_1}) {
+            $line .= sprintf "%-8s|", $row->{type_col_1};
+            if (defined $row->{type_col_2}) {
+                $line .= sprintf "%-8s|", $row->{type_col_2};
+                if (defined $row->{type_col_3}) {
+                    $line .= sprintf "%-8s|", $row->{type_col_3};
+                }
+            }
+        }
+    }
+
+    print "$line\n";
+}
+if ($print_header) {
+    # empty file set
+    print "$header\n";
+}
Index: /branches/ccl_branches/ipponly-20191108/DataStoreServer/scripts/dsprodindex
===================================================================
--- /branches/ccl_branches/ipponly-20191108/DataStoreServer/scripts/dsprodindex	(revision 41111)
+++ /branches/ccl_branches/ipponly-20191108/DataStoreServer/scripts/dsprodindex	(revision 41111)
@@ -0,0 +1,146 @@
+#!/usr/bin/env perl
+
+# list the filesets in a data store product
+
+use strict;
+use warnings;
+
+use DBI;
+use dsdbh;
+
+my $PS_EXIT_CONFIG_ERROR = 3;
+my $PS_EXIT_DATA_ERROR = 5;
+my $product = shift;
+die("must specify product to list") unless defined $product;
+
+# optional fileset name. Ignore filesets up to and including $after_fileset
+my $after_fileset = shift;
+
+my $dbh = getDBHandle();
+
+my $prod_stmt = $dbh->prepare("SELECT * FROM dsProduct WHERE prod_name = '$product'");
+$prod_stmt->execute();
+my $prod = $prod_stmt->fetchrow_hashref();
+if (! defined $prod) {
+    print STDERR "failed to find $product in product list\n";
+    exit ($PS_EXIT_DATA_ERROR);
+}
+
+# first get the column names
+
+# we have at least 3 columns
+my @header_col = ("# filesetID", "time registered", "type");
+my $numCols = 3;
+
+# now add any product specific columns that are defined. Note if there is no header value
+# then any values in the rows will be ignored
+my $last_prod_col = -1;
+for (my $i = 0; $i < 8; $i++) {
+    my $col_key = "prod_col_$i";
+    my $col_name = $prod->{$col_key};
+
+    last if ! defined $col_name;
+
+    $header_col[$numCols++] = $col_name;
+    $last_prod_col = $i;
+}
+
+# now set up arrays for the values for each column. 
+my @column;
+for (my $i = 0; $i < $numCols; $i++) {
+
+    my @col = ($header_col[$i]);
+
+    $column[$i] = \@col;
+}
+    
+
+my $prod_id = $prod->{prod_id};
+
+my $stmt;
+my $after_clause = "";
+if ($after_fileset) {
+    # client wants only filesets registered after $after_fileset
+    # Since fileset_id is auto increment the desired filesets will have a larger fileset_id than
+    # the target. Look up it's fileset_id
+
+    $stmt = $dbh->prepare("SELECT fileset_id FROM dsFileset " .
+                            "WHERE prod_id = $prod_id AND fileset_name = \'$after_fileset\'");
+    $stmt->execute();
+    my $fs = $stmt->fetchrow_hashref();
+    if ($fs) {
+        $after_clause = " AND fileset_id > $fs->{fileset_id}";
+    } else {
+        # $after_fileset not found
+        #
+        # XXX: the spec doesn't say what should happen in this case.
+        # We used to follow the conductor implementation and return the whole list.
+        # but that is definitely not the right thing to do in our context.
+        # Exit with an error
+        exit 404;
+    }
+}
+        
+$stmt = $dbh->prepare("SELECT * FROM dsFileset WHERE prod_id = $prod_id AND hide = 0  $after_clause ORDER BY fileset_id");
+$stmt->execute();
+
+# we at least have the header row to output
+my $numRows = 1;
+while( my $row = $stmt->fetchrow_hashref()) {
+
+    $numRows++;
+    my $daytime = $row->{reg_time};
+    (my $date, my $time) = split " ", $daytime;
+
+    my $reg_time = $date . "T" . $time . "Z";
+
+    my $line = sprintf "%-11s|%-20s|%-9s|", $row->{fileset_name}, $reg_time, $row->{type};
+
+    # this probably would have been simpler had I used fetchrow array
+    push @{$column[0]}, $row->{fileset_name};
+    push @{$column[1]}, $reg_time;
+    push @{$column[2]}, $row->{type};
+    
+    for (my $i = 0; $i <= $last_prod_col; $i++) {
+        my $col_key = "prod_col_$i";
+        my $col_val = $row->{$col_key};
+
+        if (!defined($col_val)) {
+            $col_val = "null";
+        }
+        push @{$column[$i+3]}, $col_val;
+    }
+}
+
+# find the largest value width in each column
+my @col_widths;
+for (my $c = 0; $c < $numCols; $c++) {
+    $col_widths[$c] = 0;
+    my $col = $column[$c];
+    for (my $i = 0; $i < $numRows; $i++) {
+        my $val = $col->[$i];
+        if (!defined($val)) {
+            die "value for column $c in row $i is null";
+            next;
+        }
+        my $len = length($val);
+        if ($len > $col_widths[$c]) {
+            $col_widths[$c] = $len;
+        }
+    }
+}
+
+# print out the results
+for (my $r = 0; $r < $numRows; $r++) {
+    for (my $c = 0; $c < $numCols; $c++) {
+        my $val = $column[$c]->[$r];
+        my $width = $col_widths[$c];
+        if (defined($val)) {
+            printf "%-*s|", $width, $val;
+        } else {
+            die "value for column $c in row $r is null";
+        }
+    }
+    print "\n";
+}
+    
Index: /branches/ccl_branches/ipponly-20191108/DataStoreServer/scripts/dsprodtool
===================================================================
--- /branches/ccl_branches/ipponly-20191108/DataStoreServer/scripts/dsprodtool	(revision 41111)
+++ /branches/ccl_branches/ipponly-20191108/DataStoreServer/scripts/dsprodtool	(revision 41111)
@@ -0,0 +1,294 @@
+#!/usr/bin/env perl
+#
+# Data Store product  create and delete
+#
+
+use strict;
+use warnings;
+
+use Getopt::Long qw( GetOptions );
+use Pod::Usage qw( pod2usage );
+use DBI;
+use PS::IPP::Metadata::Config;
+use File::Copy;
+
+use Term::ReadKey;
+
+use PS::IPP::Config qw($PS_EXIT_SUCCESS
+		       $PS_EXIT_UNKNOWN_ERROR
+		       $PS_EXIT_SYS_ERROR
+		       $PS_EXIT_CONFIG_ERROR
+		       $PS_EXIT_PROG_ERROR
+		       $PS_EXIT_DATA_ERROR
+		       $PS_EXIT_TIMEOUT_ERROR
+		       metadataLookupStr
+		       metadataLookupBool
+		       caturi
+		       );
+my $product;
+
+# XXX allow group write permission. TODO: set group to something restrictive
+umask 2;
+
+my $ptype;
+my $description;
+
+my ($ps0, $ps1, $ps2, $ps3, $ps4, $ps5, $ps6, $ps7);
+
+my $dsroot;
+my $dbname;
+my $dbserver;
+my $dbpass;
+
+my $add;
+my $del;
+my $remove;
+
+my $we_created_dir = 0;
+
+GetOptions(
+        'add=s'         =>      \$add,
+        'del=s'         =>      \$del,
+	'type=s'	=>	\$ptype,
+        'description=s' =>      \$description,
+        'rm'            =>      \$remove,
+	'ps0=s'		=>	\$ps0,          # product specific columns 0 - 7
+	'ps1=s'		=>	\$ps1,
+	'ps2=s'		=>	\$ps2,
+	'ps3=s'		=>	\$ps3,
+	'ps4=s'		=>	\$ps4,
+	'ps5=s'		=>	\$ps5,
+	'ps6=s'		=>	\$ps6,
+	'ps7=s'		=>	\$ps7,
+        'dbname=s'      =>      \$dbname,
+        'dbserver=s'    =>      \$dbserver,
+        'dsroot=s'      =>      \$dsroot,
+) or pod2usage(2);
+
+my $err = "";
+
+# will exit if neither or both -add and -del were specified
+show_usage( "Must specify either --add or --del.")
+    unless $add xor $del;
+
+if ($add) {
+    $product = $add;
+    if (!$ptype) {
+        $err .= "need to specify Product type to add\n";
+    }
+    if (!$description) {
+        $err .= "need to specify Product description\n";
+    }
+
+} elsif ($del) {
+    $product = $del;
+    if (! -t STDIN) {
+        die "cannot delete product from script\n";
+    }
+    print "*** Delete the Data Store Product $product? ***\n";
+    print "*** to delete $product answer YES, and give password ***\n";
+    print "*** WARNING this action is permanant *** \n";
+    print "Delete? (YES/[n]): ";
+
+    my $line = ReadLine(0);
+    chomp $line;
+    exit 1 if !$line or ($line ne "YES");
+
+    print "password: ";
+    ReadMode('noecho');
+    $line = ReadLine(0);
+    ReadMode('normal');
+    print "\n";
+    chomp $line;
+    $dbpass = $line;
+}
+
+#
+# lookup the location of the Data Store
+#
+
+my $ipprc =  PS::IPP::Config->new(); # IPP Configuration
+my $siteConfig = $ipprc->{_siteConfig};
+if (!$dsroot) {
+    $dsroot = metadataLookupStr($ipprc->{_siteConfig}, 'DATA_STORE_ROOT');
+    if (!$dsroot) {
+        die("Data Store root directory not set");
+    }
+}
+
+my $root_index_script = "$dsroot/index.txt.template";
+if (!stat($root_index_script)) {
+    $err .= "Data Store not found at '$dsroot'.\n"
+}
+
+# bail if any of the above args were improper
+show_usage($err)
+    if ($err);
+
+$dbserver = metadataLookupStr($siteConfig, 'DS_DBSERVER') 
+    unless defined $dbserver;
+$dbname      = metadataLookupStr($siteConfig, 'DS_DBNAME')
+    unless defined $dbname;
+my $dbuser   = metadataLookupStr($siteConfig, 'DS_DBUSER');
+$dbpass   = metadataLookupStr($siteConfig, 'DS_DBPASSWORD') if !$dbpass;
+exit ($PS_EXIT_CONFIG_ERROR) unless defined $dbserver and $dbname and $dbuser and $dbpass;
+
+my $dsn = "DBI:mysql:host=$dbserver;database=$dbname";
+
+my $product_dir = "$dsroot/$product";
+my $index_script_name = "$product_dir/index.txt";
+
+if ($del) {
+    my $dbh = DBI->connect($dsn, $dbuser, $dbpass) or die "Cannot connect to server\n";
+    #
+    # delete product
+    #
+    my $stmt = $dbh->prepare("SELECT prod_id, last_fs FROM dsProduct WHERE prod_name = \'$product\'");
+    $stmt->execute();
+    my $prod_row = $stmt->fetchrow_hashref();
+
+    if (!$prod_row) {
+        die("product $product not found");
+    }
+    my $prod_id = $prod_row->{prod_id};
+
+
+    # loop over filesets in the product and delete them
+    $stmt = $dbh->prepare("SELECT fileset_id FROM dsFileset WHERE prod_id = $prod_id");
+    $stmt->execute();
+    my $fs_row;
+    while ($fs_row = $stmt->fetchrow_hashref()) {
+        my $fileset_id = $fs_row->{fileset_id};
+        $dbh->do("DELETE from dsFile where fileset_id = $fileset_id");
+        $dbh->do("DELETE from dsFileset where fileset_id = $fileset_id");
+    }
+    $dbh->do("DELETE from dsProduct where prod_id = $prod_id");
+
+    print "Product $product deleted.\n";
+
+    # if requested, remove the product directory
+    if ($remove) {
+        print "Removing files from $product.\n";
+        if (system "rm -r $product_dir") {
+            die("failed to remove $product_dir");
+        }
+    } else  {
+        # otherwise just zap the index script
+        unlink("$index_script_name");
+    }
+
+    exit 0;
+
+} else {
+    #
+    # add a new product
+    #
+    my $dbh = DBI->connect($dsn, $dbuser, $dbpass) or die "Cannot connect to server\n";
+
+    my $stmt = $dbh->prepare("SELECT prod_id FROM dsProduct WHERE prod_name = \'$product\'");
+    $stmt->execute();
+    my $row = $stmt->fetchrow_hashref();
+
+    if ($row) {
+        die("product $product already exists");
+    }
+
+    # set up the product directory
+    # if there is an old index file delete it
+    if (-e $index_script_name ) {
+        if (!unlink($index_script_name)) {
+            die("failed trying to remove old $index_script_name");
+        }
+    }
+    if (! -e $product_dir) {
+        $we_created_dir = 1;
+        if (!mkdir $product_dir) {
+            die("failed trying to make product directory $product_dir");
+        }
+    } else {
+        # directory alrady exists make sure that it's empty
+        my @dirlist = glob("$product_dir/*");
+        die ("existing product directory $product_dir is not empty") if scalar @dirlist;
+    }
+
+    if (!copy($root_index_script, $index_script_name)) {
+        print STDERR "failed trying to copy($root_index_script, $index_script_name)";
+        cleanup();
+        exit 1;
+    }
+
+    my $prodcolstr = make_prodcol_str($ps0, $ps1, $ps2, $ps3, $ps4, $ps5, $ps6, $ps7);
+
+    my $query = "INSERT INTO dsProduct (prod_name, last_update, type, description, " .
+                " prod_col_0, prod_col_1, prod_col_2, prod_col_3, prod_col_4, prod_col_5, prod_col_6, prod_col_7)" .
+                "VALUES('$product', UTC_TIMESTAMP(), '$ptype', '$description' $prodcolstr)"; 
+
+    my $count = $dbh->do($query);
+    if ($count == 0E0) {
+        print STDERR "failed to insert Product $product in database";
+        cleanup();
+        exit 1;
+    }
+
+
+
+    exit 0;
+}
+
+
+sub show_usage {
+    my $str = shift;
+
+    chomp $str;
+
+    my @tmp = split(/\//, $0);
+
+    pod2usage(
+        -msg => 
+"usage: $tmp[$#tmp] [--add prod_name |--del prod_name ] --type prod_type [options]
+
+Commands:
+
+    --add           Add a new Product
+    --del           Remove a Product.
+
+Options:
+    --type          Product type (required)
+    --description   Product Description (required)
+    --ps0 - ps7     Optional product specific data
+    --rm            with --del remove the Product directory from the Data Store
+    --dbname        select the Data Store's database name
+                    (usually required depending on configuration)
+
+
+    
+$str",
+        -exitval => 2
+    );
+}
+
+sub make_prodcol_str {
+
+    my @list = @_;
+    my $string = "";
+
+    foreach my $s (@list) {
+        if ($s) {
+            $string .= ", \'$s\'";
+        } else {
+            $string .= ", NULL";
+        }
+    }
+
+    return $string;
+}
+
+sub cleanup {
+    # don't delete the product directory unless we made it
+    if (!$we_created_dir) {
+        return 0;
+    }
+    if (system "rm -r $product_dir") {
+            print STDERR "failed to remove $product_dir";
+    }
+}
Index: /branches/ccl_branches/ipponly-20191108/DataStoreServer/scripts/dsreg
===================================================================
--- /branches/ccl_branches/ipponly-20191108/DataStoreServer/scripts/dsreg	(revision 41111)
+++ /branches/ccl_branches/ipponly-20191108/DataStoreServer/scripts/dsreg	(revision 41111)
@@ -0,0 +1,566 @@
+#!/usr/bin/env perl
+#
+# Data Store file set registration and de-registration program
+#
+
+use strict;
+use warnings;
+
+use Getopt::Long qw( GetOptions );
+use Pod::Usage qw( pod2usage );
+use Carp;
+use Digest::MD5::File qw( file_md5_hex );
+use DBI;
+use PS::IPP::Metadata::Config;
+use PS::IPP::Metadata::Stats;
+use PS::IPP::Metadata::List qw( parse_md_list );
+use File::Copy;
+use File::Basename;
+
+use PS::IPP::Config qw($PS_EXIT_SUCCESS
+                       $PS_EXIT_UNKNOWN_ERROR
+                       $PS_EXIT_SYS_ERROR
+                       $PS_EXIT_CONFIG_ERROR
+                       $PS_EXIT_PROG_ERROR
+                       $PS_EXIT_DATA_ERROR
+                       $PS_EXIT_TIMEOUT_ERROR
+                       metadataLookupStr
+                       metadataLookupBool
+                       caturi
+                       );
+my $product;
+my $fileset;
+my $filelist;
+my $empty;
+
+my $fstype;
+my ($ps0, $ps1, $ps2, $ps3, $ps4, $ps5, $ps6, $ps7);
+
+my $linkfiles;      # if set, put links to files in the Data Store. The actuall directory is $datapath
+my $copyfiles;      # if set, copy files from this directory to the Data Store fileset
+my $datapath;       # source for files required if $linkfiles or $copyfiles
+my $abspath;			# Path specified is absolute
+
+my $dbname;         # Database name
+my $dbserver;       # Database host
+my $dsroot;         # root directory of the data store
+
+my $add;
+my $del;
+my $remove;
+my $force;          # on --del don't return error if fileset doesn't exist
+
+my $verbose = 0;
+my $no_cleanup = 0; # if something goes wrong don't delete copied or linked files for debug
+
+# XXX allow group write permission: 
+# TODO: set the group to a more restrictive one than users
+umask 2;
+
+#
+# parse args
+#
+
+GetOptions(
+        'add=s'         =>      \$add,
+        'del=s'         =>      \$del,
+        'product=s'     =>      \$product,
+        'list=s'        =>      \$filelist,
+        'empty'         =>      \$empty,
+        'type=s'        =>      \$fstype,
+        'datapath=s'    =>      \$datapath,
+	'abspath'       =>      \$abspath,
+        'link'          =>      \$linkfiles,
+        'copy'          =>      \$copyfiles,
+        'rm'            =>      \$remove,
+        'force'         =>      \$force,
+        'ps0=s'         =>      \$ps0,          # product specific columns 0 - 7
+        'ps1=s'         =>      \$ps1,
+        'ps2=s'         =>      \$ps2,
+        'ps3=s'         =>      \$ps3,
+        'ps4=s'         =>      \$ps4,
+        'ps5=s'         =>      \$ps5,
+        'ps6=s'         =>      \$ps6,
+        'ps7=s'         =>      \$ps7,
+        'dbname=s'      =>      \$dbname,
+        'dbserver=s'    =>      \$dbserver,
+        'ds_dbname=s'   =>      \$dbname,       # some scripts use these names
+        'dbhost=s'      =>      \$dbserver,
+        'ds_dbserver=s' =>      \$dbserver,
+        'dsroot=s'      =>      \$dsroot,
+        'verbose'       =>      \$verbose,
+) or pod2usage(2);
+
+pod2usage( -msg => "Unknown option: @ARGV", -exitval => 2 ) if @ARGV;
+
+# collect all command line argument errors and only fail once below.
+my $err = "";
+
+$err .= "--product is required\n" unless defined $product;
+
+# will exit if neither --add and --del is specified or if they both are specified
+$err .= "Must specify either --add or --del.\n"
+    unless $add xor $del;
+
+if ($add) {
+    $fileset = $add;
+    $err .= "--type is required\n" unless defined $fstype;
+    $err .= "--list is required\n" unless defined $filelist or $empty;
+    if ($linkfiles and $copyfiles) {
+        $err .= "only one of --link and --copy is allowed\n";
+    }
+    if (($linkfiles or $copyfiles) and !$datapath and !$abspath) {
+        $err .= "need to specify datapath or abspath with --link and --copy\n";
+    }
+    if (!$abspath && $linkfiles && (substr($datapath, 0, 1) ne "/")) {
+        $err .= "datapath must begin with / when using --link\n";
+    }
+} else {
+    $fileset = $del;
+}
+
+
+#
+# lookup the location of the Data Store
+#
+
+my $ipprc =  PS::IPP::Config->new(); # IPP Configuration
+my $siteConfig = $ipprc->{_siteConfig};
+if (!$dsroot) {
+    $dsroot = metadataLookupStr($ipprc->{_siteConfig}, 'DATA_STORE_ROOT');
+    if (!$dsroot) {
+        &my_die("Data Store root directory not set", $PS_EXIT_CONFIG_ERROR);
+    }
+}
+
+if (!stat("$dsroot/index.txt")) {
+    $err .= "Data Store not found at '$dsroot'.\n"
+}
+
+# bail if any errors above
+show_usage($err) if ($err);
+
+show_usage("Invalid data store product '$product'.")
+    unless defined stat("$dsroot/$product/index.txt");
+
+$dbname      = metadataLookupStr($siteConfig, 'DS_DBNAME') unless defined $dbname;
+$dbserver    = metadataLookupStr($siteConfig, 'DS_DBSERVER') unless defined $dbserver;
+my $dbuser   = metadataLookupStr($siteConfig, 'DS_DBUSER');
+my $dbpass   = metadataLookupStr($siteConfig, 'DS_DBPASSWORD');
+exit ($PS_EXIT_CONFIG_ERROR) unless defined $dbserver and $dbname and $dbuser and $dbpass;
+
+my $dsn = "DBI:mysql:host=$dbserver;database=$dbname";
+# mysql cookbook says 'PrintError can interfere with failure detection in some cases'
+my %conn_attrs = (PrintError => 0, RaiseError => 1, AutoCommit => 0);
+
+my $dbh = DBI->connect_cached($dsn, $dbuser, $dbpass, \%conn_attrs)
+        or &my_die("Cannot connect to DB server\n", $PS_EXIT_SYS_ERROR);
+
+print STDERR "dbh: $dbh\n" if $verbose;
+
+my $fileset_dir = "$dsroot/$product/$fileset";
+my $index_script_name = "$fileset_dir/index.txt";
+
+if ($del) {
+    #
+    # delete fileset
+    #
+
+    my $stmt = $dbh->prepare("SELECT prod_id, last_fs FROM dsProduct WHERE prod_name = ?");
+    my $execute_status = $stmt->execute($product);
+    if (!$execute_status) {
+        print STDERR "failed to execute product lookup\n";
+        exit 2;
+    }
+
+    my $prod_row = $stmt->fetchrow_hashref();
+    my $prod_id = $prod_row->{prod_id};
+    my $old_last_fs = $prod_row->{last_fs};
+
+    if (!$prod_id) {
+        print STDERR "product '$product' not found\n";
+        if ($force) {
+            exit(0);
+        } else {
+            exit(1);
+        }
+    }
+    $stmt = $dbh->prepare("SELECT fileset_id,fileset_name FROM dsFileset" .
+                                 " WHERE fileset_name = ? AND prod_id = $prod_id");
+
+    $stmt->execute($fileset);
+    my $fs_row = $stmt->fetchrow_hashref();
+
+    if (!$fs_row) {
+        print STDERR "Fileset '$fileset' not found in $product.\n";
+        if ($force) {
+            exit 0;
+        } else {
+            exit 1;
+        }
+    }
+    $stmt->finish();
+
+    my $fileset_id = $fs_row->{fileset_id};
+
+    my $rows_affected;
+    eval {
+        $dbh->do("DELETE from dsFile where fileset_id = $fileset_id");
+        $rows_affected = $dbh->do("DELETE from dsFileset where fileset_id = $fileset_id");
+
+        if ($old_last_fs eq $fs_row->{fileset_name}) {
+            # print STDERR "deleting most recent fileset in $product\n";
+
+            $stmt = $dbh->prepare("SELECT fileset_name from dsFileset " .
+                        "WHERE prod_id = $prod_id ORDER BY reg_time DESC LIMIT 1");
+            $stmt->execute();
+            my $new_last_fs;
+            my $new_newest = $stmt->fetchrow_hashref();
+            if ($new_newest) {
+                $new_last_fs = $new_newest->{fileset_name};
+            } else {
+                $new_last_fs = undef;
+            }
+            $stmt->finish();
+            $dbh->do("UPDATE dsProduct SET last_fs = ? WHERE prod_id = $prod_id", undef, ($new_last_fs));
+        }
+        $dbh->commit();
+    };
+    if (!$rows_affected or $@) { # an error occured
+        print STDERR "transaction failed, rolling back error was:\n$@\n";
+        # roll back within eval to prevent rollback failure from terminating the script
+        eval {$dbh->rollback();};
+        exit 1;
+    }
+
+    $dbh->disconnect();
+
+    if ($remove and -e $fileset_dir) {
+        my $rc;
+        if (($rc = system "rm -rf $fileset_dir")) {
+            &my_die("failed to remove $fileset_dir error code: $rc", $PS_EXIT_UNKNOWN_ERROR);
+        }
+    } else  {
+        # just zap the index script
+        unlink("$index_script_name");
+    }
+    exit 0;
+
+} else {
+    #
+    # add a new fileset
+    #
+
+    if (!$datapath) {
+        $datapath = $fileset_dir;
+    }
+
+    my $stmt = $dbh->prepare("SELECT prod_id FROM dsProduct WHERE prod_name = ?");
+    $stmt->execute($product);
+    my $row = $stmt->fetchrow_hashref();
+    my $prod_id = $row->{prod_id};
+    $stmt->finish();
+
+    if (!$prod_id) {
+        &my_die("product $product not found", $PS_EXIT_UNKNOWN_ERROR);
+    }
+    my $count = $dbh->do("SELECT fileset_id FROM dsFileset WHERE fileset_name = ?" .
+                        " AND prod_id = $prod_id", undef, ($fileset));
+    if ($count != 0E0) {
+        print STDERR "Fileset '$fileset' already exists in product $product.\n";
+        exit 1;
+    }
+
+
+    # Note: There is a race condition here. Somebody could sneak in now and add a fileset with
+    # fileset_name = $fileset. So later we'll check again that only one fileset with the name exists
+
+    ## create the fileset directory
+    if (! -e $fileset_dir) {
+        if (!mkdir $fileset_dir) {
+            &my_die("failed trying to create fileset directory $fileset_dir", $PS_EXIT_SYS_ERROR);
+        }
+	if (! -d $fileset_dir ) {
+            &my_die("cannot access just created fileset directory $fileset_dir", $PS_EXIT_SYS_ERROR);
+	}
+    }
+
+    my @files;
+    if (!$empty) {
+        my $in;
+        if ($filelist eq "-") {
+            $in = *STDIN;
+        } else {
+            open $in, "<$filelist" or &my_die("cannot open filelist file $filelist\n", $PS_EXIT_UNKNOWN_ERROR);
+        }
+
+        my $lineno = 0;
+        while (<$in>) {
+            chomp;
+            $lineno++;
+
+            my $filename;
+            my $bytes;
+            my $md5sum;
+            my $filetype;
+
+            my @fields = split /\|/;
+            if (@fields < 4) {
+                &my_die("malformed input line at $filelist line $lineno: $_\n", $PS_EXIT_PROG_ERROR);
+            }
+            $filename = shift @fields;
+            $bytes    = shift @fields;  # if this is null it will be calculated below
+            $md5sum   = shift @fields;  # if this is null it will be calculated below
+            $filetype = shift @fields;
+
+            # make sure the length of the type specific columns is 8
+            while (@fields < 8) {
+                push @fields, undef;
+            }
+            my $ts_cols = [@fields];
+
+            my $hashref = {
+                'file'      => $filename,
+                'bytes'     => $bytes,
+                'md5sum'    => $md5sum,
+                'type'      => $filetype,
+                'ts_cols'   => $ts_cols,
+            };
+
+            push @files, $hashref;
+        }
+
+        &my_die("empty filelist\n", $PS_EXIT_PROG_ERROR) if (@files == 0);
+
+        # Prepare the fileset directory
+        if ($linkfiles or $copyfiles) {
+            eval {
+                ## then copy or symlink the files into place
+                foreach my $ref (@files) {
+                    my $src_path = (defined $abspath ? '' : "$datapath/") . "$ref->{file}";
+                    my $filename = fileparse($ref->{file}, ());
+                    my $dest = "$fileset_dir/$filename";
+
+                    my $src = $ipprc->file_resolve($src_path);
+                    if (!$src) {
+		    	print STDERR "failed to resolve file $src_path\n";
+			exit $PS_EXIT_SYS_ERROR;
+                    }
+		    if ( !-e $src or !-r $src) {
+		    	print STDERR "source file $src does not exist or is not accessible\n";
+			exit $PS_EXIT_SYS_ERROR;
+		    }
+		    if ( -e $dest ) {
+		    	print STDERR "destination $dest already exists removing\n";
+			if (! unlink $dest ) {
+                            print STDERR "failed trying to unlink $dest\n";
+			    exit $PS_EXIT_SYS_ERROR;
+			}
+		    }
+
+                    if ($linkfiles) {
+                        if (! symlink $src, $dest) {
+                            print STDERR "failed trying to link $src to $dest\n";
+			    exit $PS_EXIT_SYS_ERROR;
+                        }
+                    } else {
+                        if (!copy($src, $dest)) {
+                            &my_die("copy($src, $dest) failed", $PS_EXIT_UNKNOWN_ERROR);
+                        }
+                    }
+                }
+            };
+            if ($@) { # an error occured
+                print STDERR "error preparing the fileset directory: $@\n";
+
+                if (!$no_cleanup && system "rm -r $fileset_dir") {
+                    &my_die("failed to remove $fileset_dir", $PS_EXIT_UNKNOWN_ERROR);
+                }
+                exit $PS_EXIT_UNKNOWN_ERROR;
+            }
+        }
+
+        # now calculate the md5sum and file size if they weren't provided
+        foreach my $file (@files) {
+            my $filename = fileparse($file->{file}, ());
+            my $path = "$fileset_dir/$filename";
+            if (! -e $path ) {
+                &my_die("file $path not found", , $PS_EXIT_UNKNOWN_ERROR);
+            }
+            # get the size of the file
+            my @finfo = stat($path);
+            unless (@finfo) {
+                &my_die ("can't stat $path", $PS_EXIT_UNKNOWN_ERROR);
+            }
+            # if size was supplied make sure that it matches the actual
+            # size
+            my $current_size = $finfo[7];
+            if ($file->{bytes}) {
+                if ($file->{bytes} != $current_size) {
+
+#                   XXX we are getting to many instances where sizes and md5sums change between 
+#                   the time that a job runs and measures the value and when the filesets are registered.
+#                   So far it looks as though the files are actually ok.
+#                   For now just remeasure if size changes. Might want to do something more
+#                   clever and raise an error if the difference is larger than some fraction
+
+#                    &my_die("size on disk: $current_size does not match supplied"
+#                    . " size: $file->{bytes} for $path", $PS_EXIT_PROG_ERROR);
+
+                    print STDERR "\nWarning: size on disk for $path: $current_size does not match supplied size: $file->{bytes}.\nUsing current size.\n";
+                    $file->{bytes} = $current_size;
+                    $file->{md5sum} = undef;
+                }
+            } else {
+                $file->{bytes}  = $current_size;
+            }
+            if (!$file->{md5sum}) {
+                # Get MD5 sum
+                $file->{md5sum} = file_md5_hex($path);
+                &my_die("failed to compute valid md5sum for $path", $PS_EXIT_UNKNOWN_ERROR) if !$file->{md5sum};
+            }
+        }
+    }
+
+
+    if (! $dbh->ping()) {
+        # database connection has gone away, reconnect
+        $dbh->disconnect();
+        $dbh = DBI->connect_cached($dsn, $dbuser, $dbpass, \%conn_attrs)
+            or &my_die("Cannot connect to server\n", $PS_EXIT_SYS_ERROR);
+        print STDERR "ping failed new dbh: $dbh\n" if $verbose;
+    }
+
+    eval {
+        my $qstring = "INSERT into dsFileset" .
+                " (prod_id, fileset_name, reg_time, type," .
+                " prod_col_0, prod_col_1, prod_col_2, prod_col_3, prod_col_4, prod_col_5, " .
+                " prod_col_6, prod_col_7)" .
+                " VALUES(?, ?, UTC_TIMESTAMP(), ?, ?, ?, ?, ?, ?, ?, ?, ?)";
+
+        $count = $dbh->do($qstring, undef, ($prod_id, $fileset, $fstype, 
+                            $ps0, $ps1, $ps2, $ps3, $ps4, $ps5, $ps6, $ps7));
+
+        &my_die("failed to insert $fileset", $PS_EXIT_UNKNOWN_ERROR) if (!defined($count) or ($count == 0E0));
+
+        # I don't use last_insert_id() because I've seen some problems with it and
+        # the DBI perldoc says that it's use is problematic
+        my @fsids = $dbh->selectrow_array("SELECT fileset_id FROM dsFileset" .
+                        " WHERE fileset_name = '$fileset' AND prod_id = $prod_id");
+        my $nfs = @fsids;
+        if (!$nfs) {
+            &my_die("fileset '$fileset' missing even though we just added it", $PS_EXIT_UNKNOWN_ERROR);
+        } elsif ($nfs > 1) {
+            &my_die("Fileset '$fileset' already exists under $product.", $PS_EXIT_PROG_ERROR);
+        }
+        my $fileset_id = $fsids[0];
+
+        foreach my $ref (@files) {
+	    my $filename = fileparse($ref->{file}, ());
+            my $query = "INSERT INTO dsFile" .  
+                    " (fileset_id, file_id, file_name, bytes, md5sum, type," .
+                    " type_col_0, type_col_1, type_col_2, type_col_3, type_col_4, type_col_5," .
+                    " type_col_6, type_col_7)" .
+                    " VALUES($fileset_id, 0, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)";
+
+            my $aref = $ref->{ts_cols};
+            my $do_args =  [($filename, $ref->{bytes}, $ref->{md5sum}, $ref->{type}), @$aref];
+
+            $count = $dbh->do($query, undef, @$do_args);
+
+            if ($count == 0E0) {
+                &my_die("failed to insert $filename into $fileset", $PS_EXIT_UNKNOWN_ERROR);
+            }
+        }
+
+        $count = $dbh->do("UPDATE dsProduct SET last_update = UTC_TIMESTAMP(), last_fs = \'$fileset\'"
+                                . " WHERE prod_id = $prod_id");
+        if ($count == 0E0) {
+            &my_die("failed to update dsProduct $prod_id", $PS_EXIT_UNKNOWN_ERROR);
+        }
+
+        $dbh->commit();
+
+        # create the cgi script index.txt by making a copy of its parent's
+        if (!copy("$dsroot/$product/index.txt", $index_script_name)) {
+            &my_die("failed to copy index script to file set", $PS_EXIT_UNKNOWN_ERROR);
+        }
+    };
+    if ($@) { # an error occured
+        print STDERR "transaction failed, rolling back error was:\n$@\n";
+        eval {$dbh->rollback();};
+        cleanup();
+        exit 1;
+    }
+
+    print STDERR "calling disconnect before exit\n" if $verbose;
+    $dbh->disconnect();
+
+    exit 0;
+}
+
+
+sub show_usage {
+    my $str = shift;
+
+    chomp $str;
+
+    my @tmp = split(/\//, $0);
+
+    pod2usage(
+        -msg =>
+"usage: $tmp[$#tmp] --add fs_name |--del fs_name --product prod_name --type fs_type --list filelist [options]
+
+Commands:
+
+    --add       Add a new fileset with the given metadata to the specified product.
+                File information will be read per-line from the file specifed by the
+                list option.
+                Input lines are in the following format:
+
+                    fileID|size|md5sum|type|ts0|ts1|ts2|ts3
+
+                These entries are read until EOF is reached.
+
+                If either size or md5sum are left blank they will be calculated by dsreg
+
+                (The type specific fields ts[0-3]fields are optional and may be omitted)
+
+    --del       Remove the fileset fs_name.
+
+
+
+Options:
+    --product   the product for the given file set (required)
+    --list      file containing list of files (use - for STDIN) (required)
+    --link      Link files from datapath to Data Store
+    --copy      Copy files from datapath to Data Store
+    --datapath  path to source files for --copy or --link
+    --abspath   path to source files is absoloute (--datapath not needed)
+    --ps0 - ps7 Optional product specific data
+    --rm        with --del remove the fileset directory from the Data Store
+    --force     with --del do not return error status if the fileset does not exist
+    --dbname    select the Data Store's database name
+                (usually required depending on configuration)
+
+$str",
+        -exitval => 2
+    );
+}
+
+sub cleanup {
+    if (!$no_cleanup && ($linkfiles or $copyfiles)) {
+        if (system "rm -r $fileset_dir") {
+            print STDERR "failed to remove $fileset_dir";
+        }
+    } else {
+        unlink($index_script_name);
+    }
+}
+
+sub my_die {
+    my $msg = shift;
+    my $fault = shift;
+    carp $msg;
+    exit $fault;
+}
Index: /branches/ccl_branches/ipponly-20191108/DataStoreServer/scripts/dsrootindex
===================================================================
--- /branches/ccl_branches/ipponly-20191108/DataStoreServer/scripts/dsrootindex	(revision 41111)
+++ /branches/ccl_branches/ipponly-20191108/DataStoreServer/scripts/dsrootindex	(revision 41111)
@@ -0,0 +1,57 @@
+#!/usr/bin/env perl
+#
+# dsrootindex:  Extract the root data store index from the database.
+# The output is in DataStore index.txt format
+#
+
+use strict;
+use warnings;
+
+use dsdbh;
+
+use DBI;
+
+my $PS_EXIT_CONFIG_ERROR = 3;
+
+# set this variable to redirect listings of the root datastore directory
+# to the password protected php page on the postage stamp server web site.
+# Since the data store is restricted by IP address now this is no longer necessary
+my $redirect_root_to_pstamp = 0;
+if ($redirect_root_to_pstamp) {
+
+        print '
+        <html>
+        <head>
+        <meta HTTP-EQUIV="REFRESH" content="0; url=http://pstamp.ipp.ifa.hawaii.edu/dsroot.php">
+        </head>
+        </html>
+        ';
+
+        exit 0;
+}
+
+my $dbh = getDBHandle();
+
+my $stmt = $dbh->prepare("SELECT * FROM dsProduct ORDER BY type");
+$stmt->execute();
+
+print "# productID  |Most recent |Time registered     |type     |Description                     |\n";
+
+while( my $row = $stmt->fetchrow_hashref()) {
+    my $prod_id = $row->{prod_id};
+
+    my ($date, $time) = split / /, $row->{last_update};
+    my $datetime = $date . "T" . $time . "Z";
+
+    my $last_fs =  $row->{last_fs};
+    if (!$last_fs) {
+        # dsrootls is fussy when this field is blank
+        $last_fs = "none";
+    }
+
+    my $line = sprintf "%-13s|%-12s|%-20s|%-9s|%-32s|\n",  $row->{prod_name}, $last_fs, $datetime,
+        $row->{type}, $row->{description};
+
+    # XXX EAM : security by obfuscation
+    print $line;
+}
Index: /branches/ccl_branches/ipponly-20191108/DataStoreServer/scripts/installscripts
===================================================================
--- /branches/ccl_branches/ipponly-20191108/DataStoreServer/scripts/installscripts	(revision 41111)
+++ /branches/ccl_branches/ipponly-20191108/DataStoreServer/scripts/installscripts	(revision 41111)
@@ -0,0 +1,84 @@
+#!/usr/bin/env perl
+
+# simple install program for the DataStoreServer cgi scripts
+# arguments --cgidir    gives the path for the cgi scripts
+#           --dsroot    gives the path to the root of the data store data area
+#
+# This software is not installed as part of the IPP
+# user invoking must have appropriate permissions to modify the destination directories
+# script should be run from the DataStoreServer directory
+
+
+
+use warnings;
+use strict;
+
+use Getopt::Long qw( GetOptions :config auto_help auto_version gnu_getopt );
+use Pod::Usage qw( pod2usage );
+
+my ( $cgidir, $dsroot );
+
+# the char to the right of the bar may be used as a single - alias for the longer name
+# for example --input and -i are equivalent
+GetOptions(
+	   'cgidir=s'    => \$cgidir,
+           'dsroot=s'    => \$dsroot,
+) or pod2usage( 2 );
+
+pod2usage( -msg => "Unknown option: @ARGV", -exitval => 2 ) if @ARGV;
+pod2usage( -msg => "Required options: --cgidir --dsroot",
+           -exitval => 3)
+    unless defined $cgidir and defined $dsroot; 
+
+die "run from DataStoreServer directory\n" if ((!-d "scripts") || (!-d "web"));
+
+die "directory $cgidir does not exist\n" if (!-d $cgidir);
+die "directory $dsroot does not exist\n" if (!-d $dsroot);
+
+# cgi scripts invoked by the web server
+my @cgi_list = qw(
+dodsfits
+dsdbh.pm
+dsfits.cgi
+dsgetindex
+dsindex.cgi
+dstxtindex.cgi
+);
+
+# scripts to do the database operations. Invoked by the cgi scripts
+my @scripts_list = qw(
+dsdbh.pm
+dsfsindex
+dsprodindex
+dsrootindex
+);
+
+# index.txt file for the root of the data store.
+my @dsroot_list = qw(
+index.txt.template
+);
+
+print "installing Data Store Server cgi scripts\n";
+
+foreach my $file (@dsroot_list) {
+    my $cmd = "cp web/cgi/$file $dsroot";
+    system("$cmd") == 0 or die "failed to install $file in $dsroot";
+}
+if (! -e "$dsroot/ds") {
+    system("ln -s . $dsroot/ds") == 0 or die "failed to set up $dsroot/ds";
+}
+
+foreach my $file (@scripts_list) {
+    my $cmd = "cp scripts/$file $cgidir";
+    system("$cmd")  == 0 or die "failed to install $file in $dsroot";
+}
+
+foreach my $file (@cgi_list) {
+    my $cmd = "cp web/cgi/$file $cgidir";
+    system("$cmd") == 0 or die "failed to install $file in $cgidir";
+}
+
+if (! -e "$cgidir/dsshellconfig") {
+    system( "cp web/cgi/dsshellconfig $cgidir")== 0 or die "failed to install dsshellconfig in $cgidir";
+    print "\nplease edit\n\t$cgidir/dsshellconfig\nto set up your installation\n";
+}
Index: /branches/ccl_branches/ipponly-20191108/DataStoreServer/scripts/tabledefs.sql
===================================================================
--- /branches/ccl_branches/ipponly-20191108/DataStoreServer/scripts/tabledefs.sql	(revision 41111)
+++ /branches/ccl_branches/ipponly-20191108/DataStoreServer/scripts/tabledefs.sql	(revision 41111)
@@ -0,0 +1,85 @@
+DROP TABLE IF EXISTS dsFileType;
+DROP TABLE IF EXISTS dsFile;
+DROP TABLE IF EXISTS dsFileset;
+DROP TABLE IF EXISTS dsProduct;
+
+#
+# 
+
+CREATE TABLE dsProduct (
+    prod_id     BIGINT(20) NOT NULL AUTO_INCREMENT PRIMARY KEY,
+    prod_name   VARCHAR(64) NOT NULL UNIQUE,
+    last_update datetime,
+    last_fs     VARCHAR(64),
+    type        VARCHAR(64),
+    description VARCHAR(255),
+    prod_col_0  VARCHAR(64),        # labels for product specific columns
+    prod_col_1  VARCHAR(64),        # these appear in the header list
+    prod_col_2  VARCHAR(64),
+    prod_col_3  VARCHAR(64),
+    prod_col_4  VARCHAR(64),
+    prod_col_5  VARCHAR(64),
+    prod_col_6  VARCHAR(64),
+    prod_col_7  VARCHAR(64)
+) Engine=InnoDB DEFAULT CHARSET=latin1;
+   
+
+CREATE TABLE dsFileset (
+    prod_id     BIGINT(20),
+    fileset_id  BIGINT(20) NOT NULL AUTO_INCREMENT PRIMARY KEY,
+    fileset_name VARCHAR(64) NOT NULL,
+    reg_time    datetime NOT NULL,
+    hide        TINYINT DEFAULT 0,
+    type        VARCHAR(64),
+    prod_col_0  VARCHAR(64),        # values for product specific columns
+    prod_col_1  VARCHAR(64),
+    prod_col_2  VARCHAR(64),
+    prod_col_3  VARCHAR(64),
+    prod_col_4  VARCHAR(64),
+    prod_col_5  VARCHAR(64),
+    prod_col_6  VARCHAR(64),
+    prod_col_7  VARCHAR(64),
+    FOREIGN KEY  (prod_id) REFERENCES dsProduct(prod_id)
+) Engine=InnoDB DEFAULT CHARSET=latin1;
+
+
+CREATE TABLE dsFile (
+    fileset_id  BIGINT(20),
+    file_id     BIGINT(20) NOT NULL AUTO_INCREMENT PRIMARY KEY,
+    file_name   VARCHAR(255) NOT NULL,
+    bytes       BIGINT(20),
+    md5sum      VARCHAR(255),
+    type        VARCHAR(64),
+    type_col_0  varchar(64),        # value for type specific columns
+    type_col_1  varchar(64),
+    type_col_2  varchar(64),
+    type_col_3  varchar(64),
+    type_col_4  varchar(64),
+    type_col_5  varchar(64),
+    type_col_6  varchar(64),
+    type_col_7  varchar(64),
+    FOREIGN KEY (fileset_id) REFERENCES dsFileset(fileset_id)
+) Engine=InnoDB DEFAULT CHARSET=latin1;
+
+#
+# labels for the type specific columns for each Data Store file type
+#
+
+CREATE TABLE dsFileType (
+    type        VARCHAR(64),
+    type_col_0  varchar(64),
+    type_col_1  varchar(64),
+    type_col_2  varchar(64),
+    type_col_3  varchar(64)
+) Engine=InnoDB DEFAULT CHARSET=latin1;
+
+# type specific column for type chip
+INSERT INTO dsFileType (type, type_col_0) VALUES('chip', 'chipname');
+INSERT INTO dsFileType (type, type_col_0) VALUES('dbinfo', 'component');
+INSERT INTO dsFileType (type, type_col_0) VALUES('text', 'component');
+INSERT INTO dsFileType (type, type_col_0) VALUES('tgz', 'component');
+
+# none of these types have any type specific columns
+#INSERT INTO dsFileType (type) VALUES('psrequest');
+#INSERT INTO dsFileType (type) VALUES('psresults');
+#INSERT INTO dsFileType (type) VALUES('pstamp');
Index: /branches/ccl_branches/ipponly-20191108/DataStoreServer/web/cgi/dodsfits
===================================================================
--- /branches/ccl_branches/ipponly-20191108/DataStoreServer/web/cgi/dodsfits	(revision 41111)
+++ /branches/ccl_branches/ipponly-20191108/DataStoreServer/web/cgi/dodsfits	(revision 41111)
@@ -0,0 +1,42 @@
+#!/usr/bin/env perl
+
+use strict;
+use CGI ':standard';
+
+my $DS_DIR = $ENV{"DS_ROOT"};
+
+my $uri = $ENV{'REQUEST_URI'};
+
+$uri = $ARGV[0];
+#print STDERR "uri is $uri\n";
+
+(my $left, my $param) = split(/\,/, $uri);
+
+#print STDERR "left: $left param: $param\n";
+
+if ($left !~ /\.fits$/) {
+	print header( -status => '404 Not Found' );
+	exit;
+}
+
+my @path = split(/\//, $left);
+
+my $fn = @path[$#path];
+my $fpath = $DS_DIR.'/'.$left;
+
+
+
+print header(	-type => 'image/fits',
+				-attachment => $fn,
+				-content_length => -s $fpath,
+			);
+
+
+open F, $DS_DIR.'/'.$left;
+
+binmode(F);
+binmode(STDOUT);
+
+print while (<F>);
+
+close F;
Index: /branches/ccl_branches/ipponly-20191108/DataStoreServer/web/cgi/dsdbh.pm
===================================================================
--- /branches/ccl_branches/ipponly-20191108/DataStoreServer/web/cgi/dsdbh.pm	(revision 41111)
+++ /branches/ccl_branches/ipponly-20191108/DataStoreServer/web/cgi/dsdbh.pm	(revision 41111)
@@ -0,0 +1,20 @@
+# create a database handle based on the configuration given
+# in environment variables
+
+sub getDBHandle {
+    my $dbserver = $ENV{DBSERVER};
+    my $dbuser   = $ENV{DBUSER};
+    my $dbpass   = $ENV{DBPASSWORD};
+    my $dbname   = $ENV{DBNAME};
+
+    die "database enviornment not set up" unless defined($dbserver) and defined($dbuser)
+        and defined($dbpass) and defined($dbname);
+
+    my $dsn = "DBI:mysql:host=$dbserver;database=$dbname";
+
+    my $dbh = DBI->connect($dsn, $dbuser, $dbpass) or die "Cannot connect to server\n";
+
+    return $dbh;
+}
+
+return 1;
Index: /branches/ccl_branches/ipponly-20191108/DataStoreServer/web/cgi/dsfits.cgi
===================================================================
--- /branches/ccl_branches/ipponly-20191108/DataStoreServer/web/cgi/dsfits.cgi	(revision 41111)
+++ /branches/ccl_branches/ipponly-20191108/DataStoreServer/web/cgi/dsfits.cgi	(revision 41111)
@@ -0,0 +1,3 @@
+#!/bin/bash
+. ./dsshellconfig
+./dodsfits $*
Index: /branches/ccl_branches/ipponly-20191108/DataStoreServer/web/cgi/dsgetindex
===================================================================
--- /branches/ccl_branches/ipponly-20191108/DataStoreServer/web/cgi/dsgetindex	(revision 41111)
+++ /branches/ccl_branches/ipponly-20191108/DataStoreServer/web/cgi/dsgetindex	(revision 41111)
@@ -0,0 +1,310 @@
+#!/usr/bin/env perl
+#
+# dsgetindex: Generate response to http GET for a DataStore index
+#
+# The only required argument is the REQUEST_URI.
+# The number of elements in the URI tell us whether we are listing
+# the data store root, a product, or a fileset.
+#
+# if a second argument -html is provided, the output is in HTML format
+# otherwise plain text is used
+#
+# The only thing specific to the ipp used here is the exit codes.
+
+use warnings;
+use strict;
+
+use CGI ':standard';
+use IPC::Cmd 0.36 qw( can_run run );
+
+my $PS_EXIT_CONFIG_ERROR = 3;
+my $PS_EXIT_DATA_ERROR = 5;
+
+my $redirect_root_to_pstamp = 0;
+
+my $uri = shift;
+
+if (!$uri) {
+    warn("need uri");
+    exit ($PS_EXIT_CONFIG_ERROR);
+}
+
+my $DS_ROOT = $ENV{DS_ROOT};
+
+die("DS_ROOT not found in the environment") unless defined($DS_ROOT);
+
+my @path;
+
+my $html_mode;
+my $arg = shift;
+if ($arg && ($arg eq "-html")) {
+    $html_mode = 1;
+}
+
+#
+# split up the uri into componnts.
+#
+# XXX: Find a simpler way to do this
+
+# collapse any multipe slashes in uri
+$uri = File::Spec->canonpath($uri);
+
+# split the directories from the file
+my $directories;
+my $file = "";
+my $query_str;
+
+if ($html_mode) {
+    # there is no file on the url
+
+    # print STDERR "html mode\n";
+
+    ($directories, $query_str) = split/\?/, $uri;
+
+    # used by the pretty printer
+    @path = split(/\//, $directories);
+    # shift off the empty bit
+    shift @path;
+
+} else {
+    my $volume;
+    ( $volume, $directories, $file) = File::Spec->splitpath($uri);
+
+    # find the query string if any
+    # (This is only relevant for fileset queries but we follow conductor and do not enforse that restriction)
+    ($file, $query_str) = split/\?/, $file;
+
+    if ($file ne "index.txt") {
+        warn("unexepected index file name: $file");
+        exit ($PS_EXIT_CONFIG_ERROR);
+    }
+}
+
+if (!$query_str) {
+    $query_str = "";
+}
+
+my @dirs = split /\//, $directories;
+
+# shift out the empty string at left of the first /
+shift @dirs;
+# shift out 'ds'
+shift @dirs;
+
+# now we're left with either nothing, a product, or a product and a fileset
+my $product = shift @dirs;
+my $fileset = shift @dirs;
+
+if (@dirs) {
+    # XXX need to output an error page
+    warn("unexpected extra directories: @dirs in uri: $uri");
+    exit ($PS_EXIT_CONFIG_ERROR);
+}
+
+my $program;
+if ($fileset) {
+    $program = "dsfsindex";
+} elsif ($product) {
+    $program = "dsprodindex";
+    $fileset .= " $query_str";
+} else {
+    $program = "dsrootindex";
+    $fileset = "";
+    $product = "";
+}
+
+my $missing_tools;
+my $index_program = can_run($program) or (warn "Can't find $program" and $missing_tools = 1);
+if ($missing_tools) {
+    #warn("Can't find required tools.");
+    exit ($PS_EXIT_CONFIG_ERROR);
+}
+
+{
+    my $command = "$index_program $product $fileset";
+
+    my ( $success, $error_code, $full_buf, $stdout_buf, $stderr_buf ) = 
+        run(command => $command, verbose => 0);
+
+    if ($success) {
+        if ($html_mode) {
+            html_index($stdout_buf);
+        } else {
+            print header('text/plain', '200 OK');
+            print @$stdout_buf;
+        }
+        $error_code = 0 if !defined $error_code;
+    } else {
+        if (0 && $html_mode) {
+            XXX: TODO
+
+        } else {
+            # since we're invoked as a redirect from a text file browsers are expecting
+            # 'text/plain' format
+            print header('text/plain', '404 ERROR');
+#           print header('text/plain', '410 GONE');
+#            print header('text/plain', '200 OK');
+            print @$stderr_buf;
+            print STDERR @$stderr_buf;
+#            exit 410;
+        }
+    }
+    exit $error_code >> 8;
+}
+
+
+sub html_index {
+
+    my $out_array_ref = shift;
+    my $outBuf = join( "", @$out_array_ref);
+
+    #print header('text/plain', '200 OK');
+
+    #print "outputting from html_index\n";
+    #print @$out_buf;
+    pprint_pre();
+    my @lines = split /^/, $outBuf;
+    foreach my $line (@lines) {
+        pprint($line);
+    }
+    pprint_post();
+}
+
+#
+# XXX:
+# quick hacky pretty printer
+# Adapted from Eric's Data Store mock up
+#
+
+#sub print_free_space {
+#    use Filesys::Df;
+#	# get free space
+#	my $ref = df($DS_ROOT);
+#	printf '<p>Free space: %.2f G (%.1f%%)</p>',
+#		$ref->{bfree}/(1024*1024),
+#		100.0*$ref->{bfree}/$ref->{blocks};
+#        print "\n";
+#}
+sub pprint_pre {
+
+    print header('text/html', '200 OK');
+    my $name;
+    # note $fileset is not so we look at the name of the index script to determine the level
+    if ($program eq "dsfsindex") {
+        $name = $fileset;
+    } elsif ($program eq "dsprodindex") {
+        $name = "$product";
+    } else {
+        $name = "";
+    }
+    print start_html("IPP Data Store $name");
+
+
+	# return link
+        if (!$redirect_root_to_pstamp || $program ne "dsrootindex") {
+            print a({-href=>"./index.txt"}, "Text Version");
+        }
+
+	if ($#path >= 1) {
+            # including the up links in the data store display causes 
+            # wget -r to follow them up which makes a mess
+            # Turn them off if the user agent looks like wget
+            my $display_up_links = (lc($ENV{HTTP_USER_AGENT}) =~ /wget/) ? 0 : 1;
+            if ($display_up_links) {
+                print "&nbsp&nbsp&nbsp&nbsp\n";
+                my $up = join('/', @path[0..$#path-1]);
+                if ($redirect_root_to_pstamp and $program eq "dsprodindex") {
+                    print a({-href=>"http://pstamp.ipp.ifa.hawaii.edu/dsroot.php"}, "Up to $up");
+                } elsif ($program eq 'dsrootindex') {
+                    # no up link in dsroot
+                } else {
+                    print a({-href=>".."}, "Up to $up");
+                }
+            }
+	}
+        print "</p>\n";
+
+
+    print '<table cellpadding="3">';
+}
+
+
+sub pprint_post {
+    print '</table>';
+    print p();
+
+#    print_free_space();
+    print end_html();
+}
+
+
+sub pprint {
+    my $line = shift;
+
+    my $nolink;
+    my $celltag = 'td';
+
+	# assume a comment line means its the header
+    if ($line =~ /^\s*#/) {
+        $celltag = 'th';
+        $nolink = 1;
+    }
+
+    my @toks = split(/\|/, $line);
+
+    print '<tr>';
+
+    print "<$celltag>";
+
+    if ($nolink) {
+       print $toks[0];
+    } else {
+         # First column is link to an entity down one level in data store (product, fileset, file)
+	 # assumes id is always first field
+	 my $wpath = "./$toks[0]";
+
+         # if this is a file request, use a download cgi
+         my $fits = 0;
+         if ($wpath =~ /\.fits\s*$/) {
+             # This doesn't work through the proxy
+             # The /ds-cgi gets tried on alala
+             #    $wpath = '/ds-cgi/dsfits.cgi?'.substr($wpath, 4); # strip' /ds/'
+             $fits = 1;
+         }
+
+        if ($fits) {
+            print a({-href=>$wpath, -type=>'image/x-fits'}, $toks[0]);
+        } else {
+            print a({-href=>$wpath}, $toks[0]);
+        }
+
+        if ($program eq 'dsrootindex') {
+            # for root listing make most recent fileset a link as well unless there are no filesets
+            my $fileset = $toks[1];
+            $fileset =~ s/^\s+//;
+            $fileset =~ s/\s+$//;
+            if ($fileset ne 'none') {
+                print "</$celltag>";
+                print "<$celltag>";
+                # drop the product from the list of tokens
+                my $product = shift @toks;
+                # elmiminate any whitesapace
+                $product =~ s/^\s+//;
+                $product =~ s/\s+$//;
+                print a({-href=>"./$product/$fileset"}, $fileset);
+            }
+        }
+    }
+    shift @toks;
+
+    print "</$celltag>";
+
+    # foreach my $val (@toks[1..$#toks]) {
+    foreach my $val (@toks) {
+		print "<$celltag>";
+		print $val;
+		print "</$celltag>";
+    }
+
+    print '</tr>';
+}
Index: /branches/ccl_branches/ipponly-20191108/DataStoreServer/web/cgi/dsindex.cgi
===================================================================
--- /branches/ccl_branches/ipponly-20191108/DataStoreServer/web/cgi/dsindex.cgi	(revision 41111)
+++ /branches/ccl_branches/ipponly-20191108/DataStoreServer/web/cgi/dsindex.cgi	(revision 41111)
@@ -0,0 +1,3 @@
+#!/bin/bash
+. ./dsshellconfig
+dsgetindex $REQUEST_URI -html
Index: /branches/ccl_branches/ipponly-20191108/DataStoreServer/web/cgi/dsshellconfig
===================================================================
--- /branches/ccl_branches/ipponly-20191108/DataStoreServer/web/cgi/dsshellconfig	(revision 41111)
+++ /branches/ccl_branches/ipponly-20191108/DataStoreServer/web/cgi/dsshellconfig	(revision 41111)
@@ -0,0 +1,15 @@
+# set up some environment needed by the scripts
+export PERL5LIB=INSERT_VALUE_HERE
+
+# IPC::Cmd::can_run routine doesn't find programs in ds-cgi
+# unless . is in the path
+PATH=.:${PATH}
+
+# setting these here allow the data store to run independnt of the ipp configuration
+export DBSERVER=INSERT_VALUE_HERE
+export DBNAME=INSERT_VALUE_HERE
+export DBPASSWORD=INSERT_VALUE_HERE
+export DBUSER=INSERT_VALUE_HERE
+
+export DS_ROOT=INSERT_VALUE_HERE
+
Index: /branches/ccl_branches/ipponly-20191108/DataStoreServer/web/cgi/dstxtindex.cgi
===================================================================
--- /branches/ccl_branches/ipponly-20191108/DataStoreServer/web/cgi/dstxtindex.cgi	(revision 41111)
+++ /branches/ccl_branches/ipponly-20191108/DataStoreServer/web/cgi/dstxtindex.cgi	(revision 41111)
@@ -0,0 +1,3 @@
+#!/bin/bash
+. ./dsshellconfig
+./dsgetindex $REQUEST_URI;
Index: /branches/ccl_branches/ipponly-20191108/DataStoreServer/web/cgi/findskycalcmf.pl
===================================================================
--- /branches/ccl_branches/ipponly-20191108/DataStoreServer/web/cgi/findskycalcmf.pl	(revision 41111)
+++ /branches/ccl_branches/ipponly-20191108/DataStoreServer/web/cgi/findskycalcmf.pl	(revision 41111)
@@ -0,0 +1,167 @@
+#!/usr/bin/env perl
+
+# findsskycalcmf.pl Locate a skycal cmf file for a skycell.
+
+use strict;
+use warnings;
+
+use DBI;
+use File::Basename;
+use Getopt::Long qw( GetOptions :config auto_help auto_version gnu_getopt );
+use Pod::Usage qw( pod2usage );
+
+my $verbose;
+
+my $skycal_id;
+my $skycell_id;
+my $tess_id;
+my $filter;
+my $release;
+
+# NOTE: We do not use the ipp configuration to simplify running from a CGI script
+my $dbname = "gpc1";
+my $dbserver = "scidbm";
+my $dbuser = "ippuser";
+my $dbpassword = "ippuser";
+
+
+GetOptions(
+    'skycell_id=s'  =>      \$skycell_id,
+    'tess_id=s'     =>      \$tess_id,
+    'filter=s'      =>      \$filter,
+    'skycal_id=s'   =>      \$skycal_id,
+    'release=s'     =>      \$release,
+    'dbname=s'      =>      \$dbname,
+    'verbose|v'     =>      \$verbose,
+) or pod2usage (2);
+
+pod2usage( -msg => "Required options: (--tess_id and --skycell_id and --filter) or  --skycal_id",
+           -exitval => 3) 
+        unless $skycal_id or ($filter and $tess_id and $skycell_id);
+
+my ($filename, $cmf);
+if (defined $skycal_id) {
+    ($filename, $cmf) = find_cmf_by_skycal_id($skycal_id)
+} elsif (defined $tess_id and defined $skycell_id and defined $filter) {
+    ($filename, $cmf) = find_cmf_by_skycell($tess_id, $skycell_id, $filter, $release);
+} else {
+    # should have been trapped above
+    die "not enough parameters\n";
+}
+
+
+# FIX THESE PATHS!
+# set up the environment
+$ENV{NEB_SERVER}="http://nebserver.ipp.ifa.hawaii.edu:80/nebulous";
+$ENV{PERL5LIB} .= ":/home/panstarrs/bills/psconfig/debug.lin64/lib:/home/panstarrs/bills/psconfig/debug.lin64/lib/perl5";
+
+my $neb_locate = "/home/panstarrs/bills/psconfig/debug.lin64/bin/neb-locate -p";
+if ($cmf) {
+    my $resolved = `$neb_locate $cmf`;
+    if (!$resolved) {
+        print STDERR "failed to resolve $cmf\n";
+        exit 2 if (!$resolved) 
+    }
+    # HERE IS THE OUTPUT FOR A SUCCESSFUL LOOKUP
+    print "$filename $resolved";
+} else {
+    # the find function prints an appropriate error if it fails
+    exit 2;
+}
+
+exit 0;
+
+sub open_db {
+    my $dsn = "DBI:mysql:host=$dbserver;database=$dbname";
+
+    my $dbh = DBI->connect($dsn, $dbuser, $dbpassword) 
+        or die "Cannot connect to database.\n";
+
+    return $dbh;
+}
+
+sub find_cmf_by_skycal_id {
+    my $skycal_id = shift;
+
+    my $query = "SELECT * from skycalRun JOIN skycalResult USING(skycal_id) WHERE skycal_id = $skycal_id";
+
+    my $dbh = open_db();
+    my $stmt = $dbh->prepare($query);
+    if (!$stmt->execute()) {
+        print "DBI error\n";
+        return undef;
+    }
+    my $results = $stmt->fetchrow_hashref();
+    if (!$results) {
+        print "results for skycal_id: $skycal_id not found\n";
+        return undef;
+    }
+
+    if ($results->{fault}) {
+        my $fault = $results->{fault};
+        $fault .= " (GONE)" if $fault == 26;
+        print "camRun $skycal_id has fault $fault\n";
+        return undef;
+    }
+
+    my $path_base = $results->{path_base};
+
+    my $cmf = $path_base . ".cmf";
+
+    return parse_filename($cmf);
+}
+
+sub find_cmf_by_skycell {
+    my ($tess_id, $skycell_id, $filter, $release) = @_;
+
+    my $where_clause = " (tess_id = '$tess_id' AND skycell_id = '$skycell_id' and filter LIKE '$filter%')";
+
+    # prioritize by release priority values
+    my $order = "priority DESC";
+    if ($release) {
+        $where_clause .= " AND ippRelease.release_name = '$release'";
+    }
+
+    my $query = "SELECT skycal_id, skycalResult.quality, skycalResult.fault, skycalResult.path_base, priority\n"
+    . " FROM ippRelease JOIN relStack using(rel_id) JOIN skycalResult using(skycal_id) "
+    . " JOIN stackRun USING(stack_id, tess_id, skycell_id, filter)"
+    . " WHERE $where_clause \n ORDER by $order limit 1;";
+
+    print STDERR "$query\n" if $verbose;
+
+    my $dbh = open_db();
+    my $stmt = $dbh->prepare($query);
+    if (!$stmt->execute()) {
+        print "DBI error\n";
+        return undef;
+    }
+    my $results = $stmt->fetchrow_hashref();
+    if (!$results) {
+        print "results not found\n";
+        return undef;
+    }
+    my $skycal_id = $results->{skycal_id};
+    if ($results->{fault}) {
+        my $fault = $results->{fault};
+        if ($fault == 26) {
+            $fault .= " (GONE)";
+        }
+        print "skycalResult $skycal_id has fault $fault\n";
+        return undef;
+    } elsif ($results->{quality} != 0) {
+        print "skycalRun $skycal_id has poor quality $results->{quality}\n";
+        return undef;
+    }
+
+
+    my $path_base = $results->{path_base};
+
+    my $cmf = $path_base . ".cmf";
+    return parse_filename($cmf);
+}
+
+sub parse_filename {
+    my $cmf = shift;
+    my $filename = basename($cmf);
+    return ($filename, $cmf);
+}
Index: /branches/ccl_branches/ipponly-20191108/DataStoreServer/web/cgi/findsmf.pl
===================================================================
--- /branches/ccl_branches/ipponly-20191108/DataStoreServer/web/cgi/findsmf.pl	(revision 41111)
+++ /branches/ccl_branches/ipponly-20191108/DataStoreServer/web/cgi/findsmf.pl	(revision 41111)
@@ -0,0 +1,213 @@
+#!/usr/bin/env perl
+
+# findsmf.pl Locate an smf file for an exposure either by cam_id, exp_id, or exp_name
+#
+# If cam_id is used, the smf for that run is selected.
+#
+# If selecting by exposure the camRun is selected by the following rules
+# If a release name is supplied the smf from that release is selected
+# If a data_group is supplied the value is used in the lookup (LIKE comparison)
+# Otherwise all CamRuns for the exposure are considered.
+# If multiple camRuns are found the one selected is the first row in the list formed with
+# the following SQL ordering
+#       ORDER BY quality, priority DESC, camRun.cam_id DESC
+#   runs with zero quality 
+#   runs with an entry in the released exposure table ordered by priority
+#   the latest camRun
+# For best results use the release tables.
+
+use strict;
+use warnings;
+
+use DBI;
+use File::Basename;
+use Getopt::Long qw( GetOptions :config auto_help auto_version gnu_getopt );
+use Pod::Usage qw( pod2usage );
+
+my $verbose;
+
+my $cam_id;
+my $exp_id;
+my $exp_name;
+my $data_group;
+my $release;
+my $getMagicked;
+
+# NOTE: We do not use the ipp configuration to simplify running from a CGI script
+my $dbname = "gpc1";
+my $dbserver = "scidbm";
+my $dbuser = "ippuser";
+my $dbpassword = "ippuser";
+
+
+GetOptions(
+    'exp_name=s'    =>      \$exp_name,
+    'exp_id=s'      =>      \$exp_id,
+    'cam_id=s'      =>      \$cam_id,
+    'release=s'     =>      \$release,
+    'data_group=s'  =>      \$data_group,
+    'dbname=s'      =>      \$dbname,
+    'verbose|v'     =>      \$verbose,
+) or pod2usage (2);
+
+pod2usage( -msg => "Required options: --exp_name --exp_id or --cam_id",
+           -exitval => 3) 
+        unless $exp_name or $exp_id or $cam_id;
+
+my ($filename, $smf, $fallback);
+if (defined $cam_id) {
+    ($filename, $smf, $fallback) = find_smf_by_cam_id($cam_id, $getMagicked);
+} else {
+    ($filename, $smf, $fallback) = find_smf_by_exposure($exp_name, $exp_id, $release, $data_group, $getMagicked);
+}
+
+# set up the environment
+$ENV{NEB_SERVER}="http://nebserver.ipp.ifa.hawaii.edu:80/nebulous";
+$ENV{PERL5LIB} .= ":/home/panstarrs/bills/psconfig/debug.lin64/lib:/home/panstarrs/bills/psconfig/debug.lin64/lib/perl5";
+
+my $neb_locate = "/home/panstarrs/bills/psconfig/debug.lin64/bin/neb-locate -p";
+if ($smf) {
+    my $resolved = `$neb_locate $smf`;
+    if (!$resolved) {
+        print STDERR "failed to resolve $smf\n";
+        if ($fallback) {
+            print STDERR " falling back to $fallback\n";
+            $resolved = `$neb_locate $fallback`;
+        }
+        exit 2 if (!$resolved) 
+    }
+    # HERE IS THE OUTPUT FOR A SUCCESSFUL LOOKUP
+    print "$filename $resolved";
+} else {
+    # the find function prints an appropriate error if it fails
+    exit 2;
+}
+
+exit 0;
+
+sub open_db {
+    my $dsn = "DBI:mysql:host=$dbserver;database=$dbname";
+
+    my $dbh = DBI->connect($dsn, $dbuser, $dbpassword) 
+        or die "Cannot connect to database.\n";
+
+    return $dbh;
+}
+
+sub find_smf_by_cam_id {
+    my $cam_id = shift;
+    my $getMagicked = shift;
+
+    my $query = "SELECT * from camRun JOIN camProcessedExp USING(cam_id) WHERE cam_id = $cam_id";
+
+    my $dbh = open_db();
+    my $stmt = $dbh->prepare($query);
+    if (!$stmt->execute()) {
+        print "DBI error\n";
+        return undef;
+    }
+    my $results = $stmt->fetchrow_hashref();
+    if (!$results) {
+        print "results for cam_id: $cam_id not found\n";
+        return undef;
+    }
+    my $magicked = $results->{magicked};
+
+    if ($results->{fault}) {
+        my $fault = $results->{fault};
+        $fault .= " (GONE)" if $fault == 26;
+        print "camRun $cam_id has fault $fault\n";
+        return undef;
+    }
+
+    my $path_base = $results->{path_base};
+
+    my $smf = $path_base . ".smf";
+
+    return parse_filename($smf, ($magicked and !$getMagicked));
+}
+
+sub find_smf_by_exposure {
+    my $exp_name = shift;
+    my $exp_id = shift;
+    my $release = shift;
+    my $data_group = shift;
+    my $getMagicked = shift;
+
+    my $where_clause = "";
+    if ($exp_id) {
+        $where_clause .= "exp_id = $exp_id";
+    } elsif ($exp_name) {
+        $where_clause .= "exp_name = '$exp_name'";
+    }
+
+    my $joinrelease = "";
+    # prioritize good quality runs, with higher release priority values, and finally newer cam_id
+    my $order = "quality, priority DESC, camRun.cam_id DESC";
+    my $selectmore = "";
+    if ($release) {
+        $where_clause .= " AND ippRelease.release_name = '$release'";
+    } elsif ($data_group) {
+        # ignore data_group if release is supplied
+        $where_clause .= " AND camRun.data_group LIKE '$data_group'";
+    } else {
+        # since we have no qualifiers on the selection omit faulted runs so that we have a chance
+        # to find a good one
+        $where_clause .= " AND camProcessedExp.fault = 0";
+    }
+        
+    my $query = "SELECT cam_id, quality, camRun.state, camProcessedExp.fault, camRun.magicked, camProcessedExp.path_base, IFNULL(priority, 0)\n"
+    . " FROM camRun JOIN camProcessedExp USING(cam_id) join chipRun using(chip_id) join rawExp using(exp_id)\n"
+    . " LEFT JOIN relExp using(exp_id, cam_id) LEFT JOIN ippRelease USING(rel_id)\n"
+    . " WHERE $where_clause \n ORDER by $order limit 1;";
+
+    print STDERR "$query\n" if $verbose;
+
+    my $dbh = open_db();
+    my $stmt = $dbh->prepare($query);
+    if (!$stmt->execute()) {
+        print "DBI error\n";
+        return undef;
+    }
+    my $results = $stmt->fetchrow_hashref();
+    if (!$results) {
+        if ($exp_name) {
+            print "results for exp_name: $exp_name not found\n";
+        } else {
+            print "results for exp_id: $exp_id not found\n";
+        }
+        return undef;
+    }
+    my $cam_id = $results->{cam_id};
+    my $magicked = $results->{magicked};
+    if ($results->{fault}) {
+        my $fault = $results->{fault};
+        if ($fault == 26) {
+            $fault .= " (GONE)";
+        }
+        print "camRun $cam_id has fault $fault\n";
+        return undef;
+    } elsif ($results->{quality} != 0) {
+        print "camRun $cam_id has poor quality $results->{quality}\n";
+        return undef;
+    }
+
+
+    my $path_base = $results->{path_base};
+
+    my $smf = $path_base . ".smf";
+    return parse_filename($smf, ($magicked and !$getMagicked));
+}
+
+sub parse_filename {
+    my $smf = shift;
+    my $getUncensored = shift;
+    my $filename = basename($smf);
+    my $fallback;
+    if ($getUncensored) {
+        my $dirname = dirname($smf);
+        $fallback = $smf;
+        $smf = "$dirname/SR_$filename";
+    }
+    return ($filename, $smf, $fallback);
+}
Index: /branches/ccl_branches/ipponly-20191108/DataStoreServer/web/cgi/index.txt.template
===================================================================
--- /branches/ccl_branches/ipponly-20191108/DataStoreServer/web/cgi/index.txt.template	(revision 41111)
+++ /branches/ccl_branches/ipponly-20191108/DataStoreServer/web/cgi/index.txt.template	(revision 41111)
@@ -0,0 +1,1 @@
+<!--#include virtual="/ds-cgi/dstxtindex.cgi" -->
Index: /branches/ccl_branches/ipponly-20191108/DataStoreServer/web/cgi/listsmfs.pl
===================================================================
--- /branches/ccl_branches/ipponly-20191108/DataStoreServer/web/cgi/listsmfs.pl	(revision 41111)
+++ /branches/ccl_branches/ipponly-20191108/DataStoreServer/web/cgi/listsmfs.pl	(revision 41111)
@@ -0,0 +1,172 @@
+#!/usr/bin/env perl
+
+# listsmf.pl Print a listing of information about smf files for processing meeting
+# certain selection criteria.
+
+use strict;
+use warnings;
+
+use DBI;
+use File::Basename;
+use Getopt::Long qw( GetOptions :config auto_help auto_version gnu_getopt );
+use Pod::Usage qw( pod2usage );
+
+my $verbose;
+
+my $cam_id;
+my $exp_id;
+my $exp_name;
+my $filter;
+my $label;
+my $data_group;
+my $dateobs_min;
+my $dateobs_max;
+my $release;
+my $getMagicked;
+
+# NOTE: We do not use the ipp configuration to get this information in order to simplify running from a CGI script
+# XXX: probably should be using some local file though.
+# XXX: take a look at my module dsdbh which has the function getDBHandle();
+#
+my $dbname = "gpc1";
+my $dbserver = "scidbm";
+my $dbuser = "ippuser";
+my $dbpassword = "ippuser";
+
+
+my $defaultToToday = 1;
+
+GetOptions(
+    'label=s'       =>      \$label,
+    'data_group=s'  =>      \$data_group,
+    'dateobs_min=s' =>      \$dateobs_min,
+    'dateobs_max=s' =>      \$dateobs_max,
+    'release=s'     =>      \$release,
+    'filter=s'      =>      \$filter,
+    'dbname=s'      =>      \$dbname,
+
+    'exp_name=s'    =>      \$exp_name,
+    'exp_id=s'      =>      \$exp_id,
+    'cam_id=s'      =>      \$cam_id,
+    'verbose|v'     =>      \$verbose,
+) or pod2usage (2);
+
+
+# if data_group is supplied we don't need to default the date
+if ($data_group) {
+    $defaultToToday = 0;
+}
+
+# XXX: since this is going to be used in a cgi script we should carefully vet the supplied
+# date format.
+if (!$dateobs_min) {
+    # default to today
+    if ($dateobs_max) {
+        print "Must supply begin date if max date is supplied.\n";
+        exit 2;
+    }
+    if ($defaultToToday) {
+        my ($day, $month, $year) = (gmtime)[3,4,5];
+
+        $dateobs_min = sprintf "%4d-%02d-%02dT00:00:00Z", 1900+$year, 1+$month, $day; 
+        print "$dateobs_min\n" if $verbose;
+    }
+} else {
+    # add times if not supplied
+    if (!($dateobs_min =~ 'T')) {
+        $dateobs_min .= 'T00:00:00Z';
+        print "$dateobs_min\n" if $verbose;
+    }
+    if (!($dateobs_max =~ 'T')) {
+        $dateobs_max .= 'T23:59:59Z';
+        print "$dateobs_max\n" if $verbose;
+    }
+}
+
+
+my $dbh = open_db();
+
+my $query = 'SELECT cam_id, camRun.label, camRun.state, quality, release_name, camProcessedExp.path_base,'
+    . ' exp_name, exp_id, filter, dateobs, comment'
+    . ' FROM camRun JOIN camProcessedExp USING(cam_id) JOIN chipRun using(chip_id) JOIN rawExp USING(exp_id)'
+    . ' LEFT JOIN relExp USING(cam_id, exp_id) LEFT JOIN ippRelease USING(rel_id)'
+    . " WHERE ";
+
+my $and = '';;
+if ($data_group) {
+    $query .= " camRun.data_group = '$data_group'";
+    $and = ' AND';
+}
+
+if ($dateobs_min) {
+    $query .= "$and dateobs > '$dateobs_min'";
+}
+
+if ($dateobs_max) {
+    $query .= " AND dateobs <= '$dateobs_max'";
+}
+
+# Should I use "LIKE" qualifiers here?
+if ($label) {
+    $query .= " AND camRun.label = '$label'";
+}
+
+if ($release) {
+    if ((uc($release) eq 'NONE') or (uc($release) eq 'NULL')) {
+        $query .= " AND release_name IS NULL";
+    } else {
+        $query .= " AND release_name = '$release'";
+    }
+}
+
+if ($filter) {
+    # if single character filter is supplied append wild card 
+    # character to the filter string
+    if (length($filter) == 1) {
+        $filter .= '%';
+    }
+    $query .= " AND filter LIKE '$filter'";
+}
+
+print "$query\n" if $verbose;
+
+my $statement = $dbh->prepare($query);
+
+$statement->execute();
+
+my $numRows = 0;
+
+# Print Header line as a comment.
+# XXX: rather than fine tuning the spacing on this I should have used the
+# same format line that I use for the data below.
+
+print
+'#cam_id smf_name                           quality state   exp_name    label                release        filter      obs_date   obs_time  comment' . "\n";
+#1780291 o7616g0016o.1131500.cm.1780291.smf       0 full    o7616g0016o QUB.nightlyscience   none           z.00000     2016-08-16 05:57:32 'Transient PS16cgx z band cell 45 65'
+
+while ( my $row = $statement->fetchrow_hashref() ) {
+    $numRows++;
+
+    my $smf_name = basename($row->{path_base}) . '.smf';
+    my $release_name = $row->{release_name};
+    $release_name = 'none' unless $release_name;
+
+    printf "%ld %s   %5d %-7s %s %-20s %-14s %-11s %s  '%s'\n",
+            $row->{cam_id}, $smf_name, $row->{quality}, $row->{state}, 
+            $row->{exp_name}, $row->{label}, $release_name, $row->{filter}, 
+            $row->{dateobs}, $row->{comment};
+}
+
+print "$numRows found\n" if $verbose;
+
+exit 0;
+
+sub open_db {
+    my $dsn = "DBI:mysql:host=$dbserver;database=$dbname";
+
+    my $dbh = DBI->connect($dsn, $dbuser, $dbpassword) 
+        or die "Cannot connect to database $dbname.\n";
+
+    return $dbh;
+}
+
Index: /branches/ccl_branches/ipponly-20191108/DataStoreServer/web/conf/httpd-datastore.conf
===================================================================
--- /branches/ccl_branches/ipponly-20191108/DataStoreServer/web/conf/httpd-datastore.conf	(revision 41111)
+++ /branches/ccl_branches/ipponly-20191108/DataStoreServer/web/conf/httpd-datastore.conf	(revision 41111)
@@ -0,0 +1,41 @@
+#
+# Root of the data store (where the directory hierarchy lives
+#
+<Directory "/data/ipp000.0/datastore/dsroot">
+
+  DirectoryIndex /ds-cgi/dsindex.cgi
+
+  #Options None
+
+  # turn on server side includes
+  Options +Includes
+
+  # files will be processed for server side includes if they have the
+  # executable bit set
+  XBitHack on
+
+  AddType text/plain .txt
+  AddOutputFilter INCLUDES .txt
+
+  AllowOverride None
+
+  Order allow,deny
+  Allow from all
+</Directory>
+
+
+#
+# Location of the cgi scripts
+#
+
+<Directory "/data/ipp000.0/datastore/ds-cgi">
+  Options ExecCGI
+
+
+  Order allow,deny
+  Allow from all
+</Directory>
+
+Alias       /ds     /data/ipp000.0/datastore/dsroot/
+ScriptAlias /ds-cgi /data/ipp000.0/datastore/ds-cgi/
+
Index: /branches/ccl_branches/ipponly-20191108/DataStoreServer/web/doc/smfs/getsmf.html
===================================================================
--- /branches/ccl_branches/ipponly-20191108/DataStoreServer/web/doc/smfs/getsmf.html	(revision 41111)
+++ /branches/ccl_branches/ipponly-20191108/DataStoreServer/web/doc/smfs/getsmf.html	(revision 41111)
@@ -0,0 +1,117 @@
+<html>
+<head>
+<title>IPP SMF Access Tools</title>
+</head>
+
+<body>
+<H1>
+Access to IPP smf files
+</H1>
+
+<a href=index.html>Go back up.</a><br>
+
+<h2>
+getsmf</h2>
+
+IPP smf files contain the results of camera stage processing. 
+They are FITS format files containg tables with the photometric and
+astronometric measurements for the
+sources detected in a single exposure.
+<br>
+<br>
+getsmf.php is a tool that may be used to retrieve an IPP smf file
+given certain parameters. 
+<br><br>The url of the tool is
+<pre>    http://misc.ipp.ifa.hawaii.edu/getsmf.php</pre>
+
+When this page is accessed without parameters it simply displays an 
+error message. 
+With valid parameters the tool returns the contents of the selected
+smf file. 
+<br> <br>
+Note: Obviously displaying the contents of a FITS file in a web browser is
+not very useful.
+getsmf.php is intended to be accessed directly using HTTP. 
+It is easy to write scripts that use the command line tools curl or wget
+to retrieve smf files using this tool.
+<br><br>
+For example, the following two equivalent unix commands copy the contents of 
+the smf containing the results from the cameraRun with cam_id=1780283) 
+to a local files:
+<pre>
+    curl 'http://misc.ipp.ifa.hawaii.edu/getsmf.php?cam_id=1780283' -o 1780283.smf
+
+    wget 'http://misc.ipp.ifa.hawaii.edu/getsmf.php?cam_id=1780283' -O 1780283-wget.smf
+</pre>
+
+The previous method allows the user to control the name of the local copy.
+If one would like to use the original file name supplied by the server
+the commands may be changed to:
+<pre>
+    curl 'http://misc.ipp.ifa.hawaii.edu/getsmf.php?cam_id=1780283' -O --remote-header-name
+
+    wget 'http://misc.ipp.ifa.hawaii.edu/getsmf.php?cam_id=1780283' --content-disposition
+
+</pre>
+In this example the name of the downloaded file is "o7616g0008o.1131492.cm.1780283.smf".
+<br><br>
+<b>Note:</b> The quote characters in these commands are needed to prevent
+the '?' characters from being interpreted by user's shell.
+
+<h3>parameters</h3>
+There are a few parameters that may be used to select specific smf files.
+Parameters are supplied by appending them to the base url following
+a '?' character. 
+<br><br>
+If multiple parameters are needed
+they should be separated by a '&' character.
+<br><br>
+<h4>cam_id=value</h4>
+The examples above demonstrated selection by cam_id. When a
+cam_id is supplied any other parameters supplied are ignored.
+
+<h4>exp_name=value</h4>
+Selects an smf file for an exposure with the given name. <br><br>
+For example supplying the parameter: <pre>exp_name=o7616g0008o</pre>
+gets the same result as the examples listed above. (As of the time
+of this writing).
+<br>
+Note that exposures may be processed multiple times. If no other selection
+parameters are found, the smf for the most recent processing is selected.
+<h4>release=value</h4>
+When used with exp_name, the release parameter specifies the 
+ipp release (processing version) for the smf file.<br>
+For example: <pre>exp_name=o5275g0401o&release=3PI.PV3</pre> retrieves the
+smf processed for exposure o5275g0401o in 3PI.PV3.
+If the release were changed to 3PI.nightly
+the smf from the original nightly procecessing would be retrieved. 
+(At the time of this writing, if the release parameter were omitted
+the 3PI.PV3 version would also be retrieved since it is currently
+the latest processing.)
+<h4>data_group=value</h4>
+This is a rarely used parameter.
+With exp_name, chooses smfs from camRuns processed as part of 
+the supplied data group. 
+
+<h3>Errors and Exceptions</h3>
+If an smf matching the supplied parameters is not found in the IPP, a message
+is included in the response. The http request succeeds.
+<br>
+After a user retrieves a file in this manner it should be checked 
+to insure that it
+is actually a FITS format file. 
+
+For example the following getsmf request asks for a non-existent smf:
+<pre>
+    curl 'http://misc.ipp.ifa.hawaii.edu/getsmf.php?cam_id=99999999999' -o test.smf
+</pre>
+After this command succeeds, the file test.smf is not a FITS file.
+Instead it contains text. This fact and its contents may be found 
+with the following 
+commands.
+<pre>
+    $ file test.smf
+    test.smf: ASCII text
+    $ cat test.smf
+    Could not find smf for cam_id: 99999999999.
+</pre>
Index: /branches/ccl_branches/ipponly-20191108/DataStoreServer/web/doc/smfs/index.html
===================================================================
--- /branches/ccl_branches/ipponly-20191108/DataStoreServer/web/doc/smfs/index.html	(revision 41111)
+++ /branches/ccl_branches/ipponly-20191108/DataStoreServer/web/doc/smfs/index.html	(revision 41111)
@@ -0,0 +1,29 @@
+<html>
+<head>
+<title>IPP-Misc: SMF Access</title>
+</head>
+
+<body>
+<H1>
+IPP-Misc: SMF Access
+</H1>
+<p>
+This page contains links to information about accessing
+IPP <b>SMF</b> files
+which contain the results of IPP camera stage processing.
+<p>
+These files are FITS format files containing multiple binary table extension 
+sections. These which contain the photometric and astronometric measurements 
+for the sources detected in a single Pan-STARRS exposure.
+<p>
+These files are the inital inputs to the PS calibration process.
+<p>
+The following  links describe how to use the IPP-MISC SMF Access tools
+to discover and retrive SMF files.
+<ul>
+<li><a href=listsmfs.html>Get a list of SMF files.</a>
+<li><a href=getsmf.html>Get individual SMF files.</a>
+<li><a href=scripting.html>Scripting: Putting it together.</a>
+</ul>
+
+
Index: /branches/ccl_branches/ipponly-20191108/DataStoreServer/web/doc/smfs/listsmfs.html
===================================================================
--- /branches/ccl_branches/ipponly-20191108/DataStoreServer/web/doc/smfs/listsmfs.html	(revision 41111)
+++ /branches/ccl_branches/ipponly-20191108/DataStoreServer/web/doc/smfs/listsmfs.html	(revision 41111)
@@ -0,0 +1,141 @@
+<html>
+<head>
+<title>
+listsmf
+</title>
+</head>
+
+<body>
+<H1>
+listsmfs
+</H1>
+
+<a href=index.html>Go back up.</a><br>
+<p>
+listsmf.php is a tool that may be used to retrieve certain information
+about SMF files from the IPP database.
+<br>The url of the tool is
+<a href=http://misc.ipp.ifa.hawaii.edu/listsmfs.php>http://misc.ipp.ifa.hawaii.edu/listsmfs.php</a>
+
+<p>
+The list returned is controlled by parameters supplied 
+as part of the http request used to access the page.
+
+By default listsmfs displays the list of
+smfs corresponding to the PS1 science exposures collected on
+the current date (UTC). The output may be fine tuned using parameters.
+The use of parameters is discussed <A HREF=#parameters>below</a>.
+<p>
+Here is some sample output.
+<pre>
+#cam_id smf_name                           quality state   exp_name    label                release        filter      obs_date   obs_time  comment
+1783121 o7622g0062o.1134677.cm.1783121.smf       0 full    o7622g0062o OSS.nightlyscience   SSS.nightly    i.00000     2016-08-22 07:43:12  'OSSR.R19S6.16.Q.i ps1_32_2599 visit 1'
+1783120 o7622g0061o.1134678.cm.1783120.smf       0 full    o7622g0061o OSS.nightlyscience   SSS.nightly    i.00000     2016-08-22 07:42:16  'OSSR.R19S6.16.Q.i ps1_32_2493 visit 1'
+1783122 o7622g0063o.1134679.cm.1783122.smf       0 full    o7622g0063o OSS.nightlyscience   SSS.nightly    i.00000     2016-08-22 07:44:08  'OSSR.R19S6.16.Q.i ps1_32_2614 visit 1'
+... and so on.
+</pre>
+
+<p>
+The first line, which begins with the "comment" character '#', is a
+header line. This line lists the names of the columns shown
+in the subsequent lines. 
+<p>
+If no exposures are found that match the supplied parameters (and their
+defaults) only the header line is displayed.
+<p>The first 4 columns are the most important. They are very useful 
+for constructing requests to retrieve individual SMFs using 
+<a href=getsmf.html>getsmf.php</a>.
+
+<h2>List of Columns</h2>
+
+<h3>cam_id</h3>
+The IPP camera run identifier.
+<h3>smf_name</h3>
+The base name of the SMF file for the exposure (if any).
+<h3>quality</h3>
+The quality value for the camera processing. A non-zero value indicates
+a problem with the processing. For example: unsuccessful astrometric
+solution. In the event of a run with non zero quality
+no SMF file will be available.
+<h3>state</h3>
+The current state of the camera run. 
+Runs that have completed processing will have
+the state "full". SMF files will not be available for runs with any 
+state other than full. Runs with state "new" may finish processing 
+at a later time.
+<h3>exp_name</h3>
+The exposure name of the source exposure. (Also known as "Frame Name"
+in PSPS and other contexts.)
+<h3>label</h3>
+The processing label for the camRun.
+<h3>release</h3>
+The Release that contains the exposures. Examples might be
+3PI.nightly or SSS.nightly.
+If the exposure has not been assigned to a release this column will have
+the value "none".
+<h3>filter</h3>
+The filter used by the camera for the exposure.
+<h3>obs_date</h3>
+Date of the observation (UTC).
+<h3>obs_time</h3>
+Time of the observation (UTC).
+<h3>comment</h3>
+The comment string for the exposure. Note that single quote characters
+have been added around the comment string as delimiters to 
+facilitate parsing.
+
+<A NAME="parameters">
+<h2>Parameters</h2>
+</a>
+As mentioned above, listsmfs gives a list of the
+exposures for the current (UTC) day by default.
+Parameters may be used to
+control the selection. Parameters are provided by appending
+an HTML query string to the request URL.
+<p>
+For example the sample output shown above was generated by a request
+with the parameter string '?release=SSS.nightly' appended to the URL 
+(on the date that this page was written of course).
+<p>
+If multiple parameters are to be supplied they should be separated
+by a '&' character. For example '?release=SSS.nightly&filter=r'
+would list exposures with the named release in the r band filter..
+<p>
+Here is the list of the parameters.
+
+
+<h3>date_min and date_max</h3>
+The date/time limits for observations to be included by the listing. 
+The format for the datetime is 
+
+<pre>    YYYY-MM-DDTHH:MM:SS</pre> 
+where the '-', 'T', and ':' characters are literal and the other characters are
+numeric values for year, month, day, hours, minutes, and seconds.
+<p>
+For example 'date_min=2016-08-22T08:00:00&date_max=2016-08-22T09:00:00'
+would select exposures observed between 0800 UTC and before 0900 UTC
+on Augst 22.
+<p>
+<b>Notes on date limits.</b><p>
+The time portion may be omitted. For example the parameter pair 
+'date_min=2016-08-21&date_max=2016-08-22' will cause all exposures observed
+on 2016-08-21 or 2016-08-22 to be listed.
+<p>
+If date_min is not supplied, any date_max value supplied is silently ignored.
+Instead page uses the defaults for the date parameter: exposures for the current date are listed (if any).
+
+<h3>filter</h3>
+Only exposures with the corresponding filter are listed. If a single 
+character value is supplied for filter, exposures from all filters that 
+begin with that letter are listed.
+
+<h3>label</h3>
+The list is restricted to camRuns with the supplied label.
+
+<h3>release</h3>
+The list is restrictred to exposures with supplied release name. 
+
+<h3>data_group</h3>
+The list is restrictred to camRuns with the supplied data group.
+Note: This parameter is expected to be rarely used.
+
Index: /branches/ccl_branches/ipponly-20191108/DataStoreServer/web/doc/smfs/scripting.html
===================================================================
--- /branches/ccl_branches/ipponly-20191108/DataStoreServer/web/doc/smfs/scripting.html	(revision 41111)
+++ /branches/ccl_branches/ipponly-20191108/DataStoreServer/web/doc/smfs/scripting.html	(revision 41111)
@@ -0,0 +1,151 @@
+<html>
+<head>
+<title>Scripting IPP SMF Access</title>
+</head>
+
+<body>
+<H1>
+Scripting IPP SMF Access
+</H1>
+
+<a href=index.html>Go back up.</a><br>
+
+<p>
+The process of retrieving SMF files is straightforward.
+<ul>
+<li>Discover the list of files of interest.
+<li>Build valid commands to retrieve them from the IPP to local storage.
+<li>Check the results for validity.
+<li>Start working on your science :).
+</ul>
+
+<p>
+This page is primarily concerned with the first two items on this list.
+
+<p> 
+The two pages listsmfs.php and getsmf.php have a relatively straightforward
+interface. UNIX command line tools such as curl or wget may be used to
+access the interface.
+
+<h2>Sample PERL script</h2>
+<p>
+The perl script below gives an example of how to use curl to get
+a list of the current day's smf files from listsmf.php and print curl
+commands that may be used to download the files.
+
+<pre>
+        #!/usr/bin/env perl
+
+        use strict;
+        use warnings;
+
+        my $verbose = 0;
+
+        my $misc_url = 'http://misc.ipp.ifa.hawaii.edu';
+        my $list_url = "$misc_url/listsmfs.php";
+        my $get_url = "$misc_url/getsmf.php";
+
+        my $silent = ' --silent';
+        if ($verbose) {
+            $silent = '';
+        }
+
+        my $list_cmd = "curl $silent http://misc.ipp.ifa.hawaii.edu/listsmfs.php";
+
+        my $output = `$list_cmd`;
+
+        if ($? != 0) {
+            my $rc = $?;
+            print STDERR "Ooops something went wrong with the command $list_cmd\n";
+            print STDERR "command returned $rc\n";
+            exit $rc >> 8;
+        }
+
+        my @lines = split "\n", $output;
+
+        foreach my $line (@lines) {
+            # skip the header line and any others that begin with '#'
+            next if $line =~ '#';
+
+            # split the line at the single quote character that preceeds the
+            # comment string
+            my ($left, $right) = split "'", $line;
+            
+            # the right side of this split is the comment string ...
+            my $comment = $right;
+            
+            # ... and the left side of this split contains a string
+            # with the other parameters.
+            # The other parameters are separated by whitespace
+
+            # NOTE: One might have thought we could parse the header string 
+            # and find the column names but that implies more than the
+            # listsmf "interface" commits to at this time.
+
+            my ($cam_id, $smf_name, $quality, $state, $exp_name, $label, $release, $filter, $obs_date, $obs_time) = split " ", $left;
+
+            if ($quality != 0) {
+                print STDERR "camRun $cam_id had bad quality $quality\n";
+                next;
+            }
+
+            if ($state ne 'full') {
+                print STDERR "camRun is in state $state\n";
+                next;
+            }
+
+            # generate and print out a curl command that could be used to
+            # download the smf file to the current directory
+            my $this_cmd = "curl $silent '$get_url?cam_id=$cam_id' --output $smf_name";
+
+            print "$this_cmd\n";
+        }
+        # all done
+
+</pre>
+When executed the resulting output will look something like this:
+<pre>
+        # 1783100 o7622g0010o.1134626.cm.1783100.smf 0 ICECUBE-160731A.01 i band dither 1 visit 1
+        curl  --silent 'http://misc.ipp.ifa.hawaii.edu/getsmf.php?cam_id=1783100' --output o7622g0010o.1134626.cm.1783100.smf
+        # 1783101 o7622g0011o.1134627.cm.1783101.smf 0 ICECUBE-160731A.01 i band dither 2 visit 1
+        curl  --silent 'http://misc.ipp.ifa.hawaii.edu/getsmf.php?cam_id=1783101' --output o7622g0011o.1134627.cm.1783101.smf
+        # 1783102 o7622g0012o.1134628.cm.1783102.smf 0 ICECUBE-160731A.01 i band dither 3 visit 1
+        curl  --silent 'http://misc.ipp.ifa.hawaii.edu/getsmf.php?cam_id=1783102' --output o7622g0012o.1134628.cm.1783102.smf
+        # 1783103 o7622g0013o.1134629.cm.1783103.smf 0 ICECUBE-160731A.01 i band dither 4 visit 1
+        curl  --silent 'http://misc.ipp.ifa.hawaii.edu/getsmf.php?cam_id=1783103' --output o7622g0013o.1134629.cm.1783103.smf
+        # 1783104 o7622g0014o.1134630.cm.1783104.smf 0 ICECUBE-160731A.01 i band dither 5 visit 1
+        curl  --silent 'http://misc.ipp.ifa.hawaii.edu/getsmf.php?cam_id=1783104' --output o7622g0014o.1134630.cm.1783104.smf
+</pre>
+
+<p>
+There are two lines for each exposure. The first (which begins with
+ a '#' character) lists prints selected metadata
+for the smf and its source exposure. The second line gives
+a curl command that would copy the file to the user's current directory.
+<p> Note, while not suggested, one could take the output of listsmfs.php
+and source it into a UNIX shell to execute the commands.
+<p>
+This script demonstrates how to
+<ul>
+<li>Use curl to get a list of the smfs for the current day's exposures
+<li>Split the lines of the output into its columns (including the comment string)
+<li>Examining the state and quality values to determine whether or not an SMF exists for the exposure
+<li>Printing a curl command line which when exectuted with copy the SMF to the current directory
+</ul>
+<p>
+This simple script shows the most important steps necessary for successful
+smf retrieval.
+
+<h2>What about errors?</h2>
+<p>
+For the sake of speed we chose to not include any information that
+might be useful for detecting errors in the download process. The database
+does contain anything (a checksum for example)
+which could be used to gain confidence that a file was transmitted
+successfully.
+
+<p>
+In practice so far this has not shown to be an issue. getsmf.php has been
+used to download hundreds of thousands of smf files. When users have
+detected problem files, simply trying the download again seems to correct
+the problem.
Index: /branches/ccl_branches/ipponly-20191108/DataStoreServer/web/php/getskycalcmf.php
===================================================================
--- /branches/ccl_branches/ipponly-20191108/DataStoreServer/web/php/getskycalcmf.php	(revision 41111)
+++ /branches/ccl_branches/ipponly-20191108/DataStoreServer/web/php/getskycalcmf.php	(revision 41111)
@@ -0,0 +1,132 @@
+<?php // getskycalcmf.php
+// Simple cmf retrieval program
+// To download from the page use wget command line
+// wget 'http://ippc17/ipp-misc/getskycalcmf.php?release=3PI.PV3&skycell_id=skycell.2386.085&filter=i' --content-disposition
+// or 
+// curl --remote-name --remote-header-name --location 'http://misc.ipp.ifa.hawaii.edu/getskycalcmf.php?tess_id=RINGS.V3&skycell_id=skycell.2386.085&filter=i'
+// This will get the highest prirority (latest) release
+// For the cmf specfic release add something like    &release=3PI.PV3
+// To just list use wget 'http://ippc17/ipp-misc/getskycalcmf.php?release=3PI.PV3&skycell_id=skycell.2386.085&filter=i&list=1'
+
+$error_string = "";
+$debug = 0;
+
+$rvar_tess_id = getVar('tess_id');
+$rvar_skycell_id = getVar('skycell_id');
+$rvar_filter = getVar('filter');
+$rvar_release = getVar('release');
+
+$rvar_stack_id = getVar('stack_id');
+$rvar_skycal_id = getVar('skycal_id');
+$rvar_list = getVar('list');
+
+$command = "/data/ippc17.0/datastore/ds-cgi/findskycalcmf.pl";
+
+# skycal_id takes priority
+if ($skycal_id) {
+    $command .= " --skycal_id $skycal_id";
+} else if ($rvar_stack_id) {
+    $command .= " --stack_id $stack_id";
+} else if ($rvar_tess_id && $rvar_skycell_id && $rvar_filter) {
+    $command .= " --tess_id $rvar_tess_id --skycell_id $rvar_skycell_id --filter $rvar_filter%";
+} else {
+    $command = "";
+}
+
+if ($command) {
+    if ($rvar_release) {
+        if ($rvar_release == "3PI.GR1") {
+            $rvar_release = "3PI.PV1";
+        }
+        $command .= " --release $rvar_release";
+    } 
+}
+
+$gotFile = 0;
+if ($command) {
+    if ($debug) {
+        echo "<br>$command\n<br>";
+    }
+    $command = escapeshellcmd($command);
+    $output = array();
+
+    exec($command, $output, $command_status);
+
+    if ($command_status == 0) {
+        // we only expect one line of output
+        $len = count($output);
+        if ($len == 1) {
+            list($filename, $pathname) = explode(" ", $output[0]);
+            if ($filename && $pathname) {
+                if (!$debug) {
+                    $gotFile = 1;
+                } else {
+                    echo "$filename $pathname\n";
+                    echo "$command\n";
+                }
+            }
+        } else {
+            echo "<br>unexpected output from $command: $output[0] $output[1]\n";
+        }
+    } else {
+        if ($debug) {
+            echo "<br>command failed $command_status\n";
+        }
+    }
+} else {
+    // something went wrong with the paramters. Error message produced below.
+}
+
+if ($gotFile) {
+    if ($rvar_list) {
+        echo "cmf : $filename\n";
+        // echo "<br>path: $pathname\n<br>";
+    } else {
+        // All systems are go. Time to write the output.
+        // First set up the header
+        header('Content-type: application/fits');
+        header("Content-Disposition: attachment; filename=\"$filename\"");
+        $filesize = filesize($pathname);
+        header("Content-Length: $filesize");
+        header('Expires: now');
+
+        // copy the contents of the file to the stream
+        readfile($pathname);
+    }
+} else {
+    // XXX: Figure out how to stop wget from redirecting these error
+    // messages to the nasty filename
+    $error_string="Could not find cmf";
+    if ($rvar_skycal_id) {
+        $error_string .= " for skycal_id: $rvar_skycal_id";
+    } elseif ($rvar_skycell_id and $rvar_tess_id and $rvar_filter) {
+        $error_string .= " for tess_id $rvar_tess_id skycell_id: $rvar_skycell_id filter: $rvar_filter";
+    } else {
+        $error_string .= ".<br>Not enough parameters supplied";
+    }
+        
+    echo "$error_string.\n";
+}
+
+function getVar($var) {
+    if ($_SERVER['REQUEST_METHOD'] == 'POST') {
+        $rvar = $_POST[$var];
+    } else {
+        $rvar = $_GET[$var];
+    }
+    $rvar = stripslashes($rvar);
+    $rvar = htmlentities($rvar);
+    $rvar = strip_tags($rvar);
+    return $rvar;
+}
+
+
+if ($list) {
+    // print lots of information from the PHP installation
+    // phpinfo(-1);
+
+    // print the most useful variables
+    //    phpinfo(32);
+}
+
+?>
Index: /branches/ccl_branches/ipponly-20191108/DataStoreServer/web/php/getsmf.php
===================================================================
--- /branches/ccl_branches/ipponly-20191108/DataStoreServer/web/php/getsmf.php	(revision 41111)
+++ /branches/ccl_branches/ipponly-20191108/DataStoreServer/web/php/getsmf.php	(revision 41111)
@@ -0,0 +1,129 @@
+<?php // getsmf.php
+// Simple smf retrieval program
+// To download from the page use wget command line
+// wget 'http://ippc17/ipp-misc/getsmf.php?exp_name=o5732g0036o' --content-disposition
+// To just list use wget 'http://ippc17/ipp-misc/getsmf.php?exp_name=o5732g0036o&list=1'
+
+
+$error_string = "";
+$debug = 0;
+
+$rvar_exp_name = getVar('exp_name');
+$rvar_cam_id = getVar('cam_id');
+$rvar_exp_id = getVar('exp_id');
+$rvar_release = getVar('release');
+$rvar_data_group = getVar('data_group');
+$rvar_list = getVar('list');
+
+$command = "/data/ippc17.0/datastore/ds-cgi/findsmf.pl";
+
+# cam_id takes priority
+if ($rvar_cam_id) {
+    $command .= " --cam_id $rvar_cam_id";
+} else if ($rvar_exp_name) {
+    $command .= " --exp_name $rvar_exp_name";
+} else if ($rvar_exp_id) {
+    $command .= " --exp_id $rvar_exp_id";
+} else {
+    $command = "";
+}
+
+if ($command) {
+    if ($rvar_release) {
+        if ($rvar_release == "3PI.GR1") {
+            $rvar_release = "3PI.PV1";
+        }
+        $command .= " --release $rvar_release";
+    } 
+    if ($rvar_data_group) {
+        $command .= " --data_group $rvar_data_group";
+    } 
+}
+
+$gotFile = 0;
+if ($command) {
+    if ($debug) {
+        echo "<br>$command\n<br>";
+    }
+    $command = escapeshellcmd($command);
+    $output = array();
+
+    exec($command, $output, $command_status);
+
+    if ($command_status == 0) {
+        // we only expect one line of output
+        $len = count($output);
+        if ($len == 1) {
+            list($filename, $pathname) = explode(" ", $output[0]);
+            if ($filename && $pathname) {
+                if (!$debug) {
+                    $gotFile = 1;
+                } else {
+                    echo "$filename $pathname\n";
+                    echo "$command\n";
+                }
+            }
+        } else {
+            echo "<br>unexpected output from $command: $output[0] $output[1]\n";
+        }
+    } else {
+        if ($debug) {
+            echo "<br>command failed $command_status\n";
+        }
+    }
+} else {
+}
+
+if ($gotFile) {
+    if (!$rvar_list) {
+        // All systems are go. Time to write the output.
+        // First set up the header
+        header('Content-type: application/fits');
+        header("Content-Disposition: attachment; filename=\"$filename\"");
+        $filesize = filesize($pathname);
+        header("Content-Length: $filesize");
+        header('Expires: now');
+
+        // copy the contents of the file to the stream
+        readfile($pathname);
+    } else {
+        echo "smf : $filename\n";
+        # echo "path: $pathname\n";
+    }
+} else {
+    // XXX: Figure out how to stop wget from redirecting these error
+    // messages to the nasty filename
+    $error_string="Could not find smf";
+    if ($rvar_cam_id) {
+        $error_string .= " for cam_id: $rvar_cam_id";
+    } elseif ($rvar_exp_name) {
+        $error_string .= " for exposure: $rvar_exp_name";
+    } else {
+        $error_string .= ". No cam_id or exp_name provided";
+    }
+        
+    echo "$error_string.\n";
+}
+
+function getVar($var) {
+    if ($_SERVER['REQUEST_METHOD'] == 'POST') {
+        $rvar = $_POST[$var];
+    } else {
+        $rvar = $_GET[$var];
+    }
+    $rvar = stripslashes($rvar);
+    $rvar = htmlentities($rvar);
+    $rvar = strip_tags($rvar);
+    return $rvar;
+}
+
+
+if ($list) {
+    // print lots of information
+    // phpinfo(-1);
+
+    // print the most useful variables
+    //    phpinfo(32);
+}
+
+?>
Index: /branches/ccl_branches/ipponly-20191108/DataStoreServer/web/php/listsmfs.php
===================================================================
--- /branches/ccl_branches/ipponly-20191108/DataStoreServer/web/php/listsmfs.php	(revision 41111)
+++ /branches/ccl_branches/ipponly-20191108/DataStoreServer/web/php/listsmfs.php	(revision 41111)
@@ -0,0 +1,149 @@
+<?php // listsmfs.php
+// Simple smf listing program
+
+
+$error_string = "";
+$debug = 0;
+$list  = 0;  # another debugging tool. list phpinfo
+
+$rvar_date_min = getVar('date_min');
+$rvar_date_max = getVar('date_max');
+$rvar_filter   = getVar('filter');
+$rvar_label    = getVar('label');
+$rvar_data_group = getVar('data_group');
+$rvar_release  = getVar('release');
+$rvar_camera   = getVar('camera');
+
+# these params aren't yet used (and may not be)
+$rvar_exp_name = getVar('exp_name');
+$rvar_cam_id = getVar('cam_id');
+$rvar_exp_id = getVar('exp_id');
+
+$command = "/data/ippc17.0/datastore/ds-cgi/listsmfs.pl";
+
+# max date is ignored unless min date is supplied
+if ($rvar_date_min) {
+    $command .= " --dateobs_min $rvar_date_min";
+    if ($rvar_date_max) {
+        $command .= " --dateobs_max $rvar_date_max";
+    }
+}
+
+if ($rvar_filter) {
+    $command .= " --filter $rvar_filter";
+}
+
+if ($rvar_label) {
+    $command .= " --label $rvar_label";
+}
+
+if ($rvar_release) {
+    $command .= " --release $rvar_release";
+} 
+
+if ($rvar_data_group) {
+    $command .= " --data_group $rvar_data_group";
+} 
+
+if ($rvar_camera) {
+    $command .= " --dbname $rvar_camera";
+}
+
+
+
+
+$agent = $_SERVER['HTTP_USER_AGENT'];
+$pos_curl = stripos($agent, 'curl');
+$pos_wget = stripos($agent, 'wget');
+
+if ($pos_curl === false && $pos_wget === false) {
+    $not_browser = 0;
+} else {
+    $not_browser = 1;
+}
+
+$submitter_ip_addr = $_SERVER['HTTP_X_FORWARDED_FOR'];
+$remote_ip_addr = $_SERVER['REMOTE_ADDR'];
+if ($submitter_ip_addr  === false) {
+    $submitter_ip_addr = $remote_ip_addr;
+}
+if (!$submitter_ip_addr) {
+    $submitter_ip_addr = $remote_ip_addr;
+}
+
+# OUTPUT begins here
+
+# not sure whether this really makes a difference
+if ($not_browser) {
+    echo header('text/plain', '200 OK');
+} else {
+    echo header('text/html', '200 OK');
+}
+
+if ($list) {
+    # debug mode to list phpinfo, set the title
+    # to prevent title from being phpinfo
+    echo "<head> <title>smf list</title></head>\n";
+}
+
+if ($debug) {
+    echo "pos_curl: $pos_curl pos_wget: $pos_wget\n";
+}
+
+
+if ($command) {
+    $command = escapeshellcmd($command);
+    if ($debug) {
+        $agent = $_SERVER['HTTP_USER_AGENT'];
+        echo "HTTP_USER_AGENT is: $agent\n<br>";
+        echo "<br>$command\n<br>";
+    }
+    $output = array();
+
+    # RUN the command
+    exec($command, $output, $command_status);
+
+    if ($command_status == 0) {
+        if (!$not_browser) {
+            # when viewing in a web browser without this the newlines get lost
+            echo "<pre>";
+        }
+
+        # output the list
+        foreach( $output as $line) {
+            echo "$line\n";
+        }
+    } else {
+        if ($debug) {
+            echo "<br>command failed $command_status\n";
+        }
+    }
+} else {
+}
+
+
+function getVar($var) {
+    if ($_SERVER['REQUEST_METHOD'] == 'POST') {
+        $rvar = $_POST[$var];
+    } else {
+        $rvar = $_GET[$var];
+    }
+    $rvar = stripslashes($rvar);
+    $rvar = htmlentities($rvar);
+    $rvar = strip_tags($rvar);
+    return $rvar;
+}
+
+
+if ($list) {
+    // print lots of information
+    // phpinfo(-1);
+       print "</pre>\n";
+       print "<p>Submitter IP ADDR: $submitter_ip_addr\n";
+       print "<br>Remote IP ADDR: $remote_ip_addr\n";
+
+    // print the most useful variables
+       phpinfo(32);
+}
+
+?>
