#!/usr/bin/env php
<?php
//-------------------------------------------------------------------
// Copyright (c) 2006 Patrick Mueller <pmuellr@yahoo.com>
//
// Permission is hereby granted, free of charge, to any person obtaining a
// copy of this software and associated documentation files (the "Software"),
// to deal in the Software without restriction, including without limitation
// the rights to use, copy, modify, merge, publish, distribute, sublicense,
// and/or sell copies of the Software, and to permit persons to whom the
// Software is furnished to do so, subject to the following conditions:
// 
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
// 
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
// DEALINGS IN THE SOFTWARE.
//-------------------------------------------------------------------

//-------------------------------------------------------------------
// This program is a command-line utility to exercise the Amazon 
// S3 Service: http://aws.amazon.com/s3
//-------------------------------------------------------------------
// 2006-10-13: pjm - 1.0 - initial version 
// 2006-10-16: pjm - 1.1 - quote etag for If-Match
//-------------------------------------------------------------------

$ProgramName baseName(__FILE__);
$ProgramVers '1.1';

// default option values
$opts = array();
$opts['public']      = false;
$opts['file']        = NULL;
$opts['use-if']      = false;
$opts['etag']        = NULL;
$opts['prefix']      = NULL;
$opts['delimiter']   = NULL;
$opts['content-type'] = NULL;

// parse parameters
parseArgs($argv$parms$opts);

@
$cmd    $parms[0];
@
$bucket $parms[1];
@
$object $parms[2];
@
$file   $parms[3];

// check for help
if (isset($opts['?'])) printHelp();
if (
$cmd == '?')       printHelp();
if (
$cmd == '')        printHelp();

// normalize the bucket and object names
# $bucket = urlencode($bucket);
# $object = urlencode($object);

// pull in requirements; doing it here so help will run without it
require 'Crypt/HMAC.php';

// we'll invoke the commands via some indirection, so we need
// an array of the valid commands
$cmds = array("lsbuckets"
    
"mkbucket""lsbucket""rmbucket"
    
"mkobject""getobject""putobject""rmobject");

// get the AWS keys from the environment
$opts['aws-key']        = getenv("AWS_ACCESS_KEY");
$opts['aws-secret-key'] = getenv("AWS_SECRET_ACCESS_KEY");

if (
'' == $opts['aws-key'])        logError("Env var not set: AWS_ACCESS_KEY");
if (
'' == $opts['aws-secret-key']) logError("Env var not set: AWS_SECRET_ACCESS_KEY");
    
// if the command is one we know about, run it    
if (in_array($cmd$cmds)) {
    
$cmd($bucket$object$file$opts);
}

// if the command isn't one we know, complain
else {
    
logError("unknown command '$cmd'");
}

//-------------------------------------------------------------------
// Return a list of the buckets for this user.
//-------------------------------------------------------------------
function lsbuckets($bucket$object$file$opts) {
    
issueHttpRequest("GET"""NULL$opts);
}

//-------------------------------------------------------------------
// Create a new bucket.
//-------------------------------------------------------------------
function mkbucket($bucket$object$file$opts) {
    
issueHttpRequest("PUT"$bucketNULL$opts);
}

//-------------------------------------------------------------------
// Return a list of objects in this bucket.
//-------------------------------------------------------------------
function lsbucket($bucket$object$file$opts) {
    
$queryStrings = array();
    if (
'' != $opts['prefix'])    $queryStrings[] = 'Prefix='    urlencode($opts['prefix']);
    if (
'' != $opts['delimiter']) $queryStrings[] = 'Delimiter=' urlencode($opts['delimiter']);
    
    if (
count($queryStrings)) {
        
$opts['query-string'] = implode('&'$queryStrings);
    }
    
    
issueHttpRequest("GET""$bucket"NULL$opts);
}

//-------------------------------------------------------------------
// Delete a bucket.
//-------------------------------------------------------------------
function rmbucket($bucket$object$file$opts) {
    
issueHttpRequest("DELETE"$bucketNULL$opts);
}

//-------------------------------------------------------------------
// Create a new object.
//-------------------------------------------------------------------
function mkobject($bucket$object$file$opts) {
    if (!isset(
$opts['use-if'])) return putobject($bucket$object$opts);
    
    if (
NULL == $opts['content-type']) $opts['content-type'] = 'application/octet-stream';

    if (
$opts['use-if']) {
        
$header "If-None-Match: *";
        
$opts['if-header'] = $header;
    }
    
    
issueHttpRequest("PUT""$bucket/$object"$file$opts);
}

//-------------------------------------------------------------------
// Return an object.
//-------------------------------------------------------------------
function getobject($bucket$object$file$opts) {
    
issueHttpRequest("GET""$bucket/$object"NULL$opts);
}

//-------------------------------------------------------------------
// Update an object.
//-------------------------------------------------------------------
function putobject($bucket$object$file$opts) {

    if (
NULL == $opts['content-type']) $opts['content-type'] = 'application/octet-stream';
    
    if (
$opts['use-if']) {
        
$eTag   '"' $opts['etag'] . '"';
        
$header "If-Match: $eTag";
        
$opts['if-header'] = $header;
    }
    
    
issueHttpRequest("PUT""$bucket/$object"$file$opts);
}

//-------------------------------------------------------------------
// Delete an object.
//-------------------------------------------------------------------
function rmobject($bucket$object$file$opts) {
    
issueHttpRequest("DELETE""$bucket/$object"NULL$opts);
}

//-------------------------------------------------------------------
// Issue the HTTP request; this is where the heavy lifting occurs.
//-------------------------------------------------------------------
function issueHttpRequest($verb$uri$file$opts) {
    global 
$fileContents;
    
    if (
'/' == $uri$uri '';
    
    
// get the acl for this request (useful only for PUTs)
    
if ($opts['public']) $acl 'public-read';
    else                 
$acl 'private';
    
    
// set up soemm of the url-ish bits
    
$host    "s3.amazonaws.com";
    
$baseURL "https://$host/";
    
$url     $baseURL $uri;

    if (isset(
$opts['query-string'])) $url .= '?' $opts['query-string'];
    
    
$contentType $opts['content-type'];
    if (
NULL == $contentType$contentType "";
    
    
// get the AWS keys
    
$awsAccessKey       $opts['aws-key'];
    
$awsSecretAccessKey $opts['aws-secret-key'];
     
    
// calculate the AWS authentication signature
    
$gmDate     gmdate(DATE_RFC822);
    
$hashSource "$verb\n\n$contentType\n$gmDate\nx-amz-acl:$acl\n/$uri";
    
$hashEngine = new Crypt_HMAC($awsSecretAccessKey"sha1");
    
$hash       $hashEngine->hash($hashSource);
    
$authSig    base64_encode(hex2str($hash));

    
// build the array of headers for libcurl
    
$headers = array(
        
"Server: $host",
        
"Date: $gmDate",
        
"Authorization: AWS $awsAccessKey:$authSig",
        
"x-amz-acl: $acl"
        
);

    if (
'' !== $contentType) {
        
$headers[] = "Content-Type: $contentType";
    }
    
    if (isset(
$opts['if-header'])) {
        
$headers[] = $opts['if-header'];
    }
    
    if (
NULL !== $file) {
        
$contentLength filesize($file);
        
$fileHandle    fopen($file,"rb");
        
$headers[]     = "Content-Length: $contentLength";
    }

    echo 
"-----------------------------------------------------\n";
    echo 
"Request\n";
    echo 
"-----------------------------------------------------\n";
    echo 
"$verb $url\n";
    foreach (
$headers as $header) {
        if (
=== strpos($header,"Authorization:")) {
            echo 
"Authorization: AWS xxxx:yyyy\n";
            continue;
        }
        
        echo 
"$header\n";
    }
    echo 
"\n";
    
    
// set up the curl request
    
$curl curl_init();
    
curl_setopt($curlCURLOPT_URL,           $url);
    
curl_setopt($curlCURLOPT_HEADER,        true);
    
curl_setopt($curlCURLOPT_CUSTOMREQUEST$verb);
    
curl_setopt($curlCURLOPT_HTTPHEADER,    $headers);

    if (
NULL !== $file) {
        
curl_setopt($curlCURLOPT_INFILE,       $fileHandle);
        
curl_setopt($curlCURLOPT_INFILESIZE,   $contentLength);
        
curl_setopt($curlCURLOPT_UPLOAD,       true);
        
curl_setopt($curlCURLOPT_READFUNCTION"curlReadRequestData");
    }
    
    
// run it, and close curl
    
echo "-----------------------------------------------------\n";
    echo 
"Response\n";
    echo 
"-----------------------------------------------------\n";
    
    
curl_exec($curl);
    
curl_close($curl);
    
    if (
NULL !== $file) {
        
fclose($fileHandle);
    }

}

function 
curlReadRequestData($curl$fileHandle$length) {
#    echo "in curl_readFunction()\n";
    
return fread($fileHandle$length);
}

//-------------------------------------------------------------------
// Convert a series of hex digits in a string into the actual 
// string value.  
//-------------------------------------------------------------------
function hex2str($hexString) {
    
$string "";
    
    for (
$i=0$i<strlen($hexString); $i+=2) {
        
$hex substr($hexString,$i,2);
        
$string .= chr(hexdec($hex));
    }
    
    return 
$string;
}

//-------------------------------------------------------------------
// Parse command line into parameters, and options.
// The $opts variable should be an array that is pre-filled with 
// default options; each option has a value associated with it.  
// Options that are really binary (was the option used) have a 
// boolean value.
//-------------------------------------------------------------------
function parseArgs($argv, &$parms, &$opts) {

    
// initialize parms to be returned
    
$parms = array();
    
    
// a regex to look for the "-option=value" pattern in a parm
    
$pattern '/-+([^=]*)(=(.*))?/';
    
    
// iterate through command-line parameter
    
for ($i=1$i<count($argv); $i++) {
        
$arg $argv[$i];
        
        
// see if the parameter is an option or not
        
$matched preg_match($pattern$arg$matches);
        
        
// not an option, add it to the $parms array
        
if (== $matched) {
            
$parms[] = $arg;
        }
        
        
// is an option, get the value, if specified, and
        // set in the $opts array
        
else {
            
$key $matches[1];
            
            
// there was an option value; set it
            
if (isset($matches[3])) {
                
$val $matches[3];
            }
            
            
// no option value; default to '' unless it's
            // a 'binary' option, then set to true.
            
else {
                if (
false === $opts[$key]) $val true;
                else                       
$val '';            
            }
            
            
$opts[$key] = $val;
        }
    }
}

//-------------------------------------------------------------------
// print an error message, then exit
//-------------------------------------------------------------------
function logError($message) {
    global 
$ProgramName;
    
    echo 
"$ProgramName: $message\n";
    exit(
1);
}

//-------------------------------------------------------------------
// print some help text, then exit
//-------------------------------------------------------------------
function printHelp() {
    global 
$ProgramName;
    global 
$ProgramVers;
    
    echo <<<END_HELP
$ProgramName $ProgramVers - a command-line interface to Amazon S3 

usage:
   $ProgramName 
{options} command

command:
   lsbuckets
   mkbucket  
[bucketName]
   lsbucket  
[bucketName]
   rmbucket  
[bucketName]
   mkobject  
[bucketName] [objectName] [file]
   getobject 
[bucketName] [objectName]
   putobject 
[bucketName] [objectName] [file]
   rmobject  
[bucketName] [objectName]

options:
   -public
      For mkBucket, mkObject, and putObject, make the bucket or
      object available publically for read access.
   -file\=
[fileName]
      For mkObject and putObject, specify a file to be used as 
      the content to be written.
   -use-if
      For mkObject and putObject, use If- HTTP headers to ensure 
      the content isn't inadvertantly overwritten.  For pubObject,
      the -etag option must also be used.  If this option is not
      used, mkobject and putobject are synonymous.
   -etag\=
[etagValue] 
      For putObject with the -use-if option, specifies the etag
      value to be used with the If-Match HTTP header.
   -prefix\=
[prefix]
      For lsbucket, uses the prefix option.
   -delimiter\=
[delimiter]
      For lsbucket, uses the delimiter option.
   -content-type\=
[mime type]
      For mkobject and putobject, indicates the content type of
      the object.  If not specified, 'application/octet-stream'
      will be used.

pre-reqs:
   - Your AWS key and secret key are available in the environment variables
      AWS_ACCESS_KEY
      AWS_SECRET_ACCESS_KEY
   - The PEAR Crypt_HMAC module.  Install via: 
        pear install Crypt_HMAC

END_HELP;

    exit();
}

?>