Browse files

Changed to a request/response model and added initial implementation …

…of S3 and Http.
  • Loading branch information...
1 parent 48e5b4f commit e37c8dd783536d91d79308c841c9682ab2d55e8a @lox committed Feb 15, 2010
View
3 .gitmodules
@@ -0,0 +1,3 @@
+[submodule "lib/simpletest"]
+ path = lib/simpletest
+ url = git://github.com/lox/simpletest.git
View
101 classes/Facade/AbstractRequest.php
@@ -0,0 +1,101 @@
+<?php
+
+/**
+ * A basic request object
+ */
+abstract class Facade_AbstractRequest implements Facade_Request
+{
+ const BUFFER_SIZE = '512000'; // 500Kb
+
+ private $_headers;
+ private $_stream;
+
+ /**
+ * Constructor
+ */
+ public function __construct($headers=array())
+ {
+ $this->_headers = new Facade_HeaderCollection($headers);
+ }
+
+ /* (non-phpdoc)
+ * @see Facade_Request::setContentFile()
+ */
+ public function setContentFile($file)
+ {
+ if(!is_file($file))
+ {
+ throw new Facade_Exception("$file isn't a file");
+ }
+
+ return $this->setContentStream(fopen($file,'r'), filesize($file));
+ }
+
+ /* (non-phpdoc)
+ * @see Facade_Request::setContentStream()
+ */
+ public function setContentStream($stream, $length=null)
+ {
+ if($length) $this->setContentLength($length);
+ $this->_stream = $stream;
+ return $this;
+ }
+
+ /* (non-phpdoc)
+ * @see Facade_Request::setContentString()
+ */
+ public function setContentString($string)
+ {
+ if(!$fp = fopen('php://temp/maxmemory:'.self::BUFFER_SIZE, 'w+'))
+ {
+ throw new Facade_Exception("Failed to create temp file stream");
+ }
+
+ // write to the buffer
+ fwrite($fp, $string);
+ rewind($fp);
+
+ return $this->setContentStream($fp, strlen($string));
+ }
+
+ /**
+ * Returns the content stream, or an exception if it's not yet been set
+ */
+ protected function getContentStream()
+ {
+ return $this->_stream;
+ }
+
+ /* (non-phpdoc)
+ * @see Facade_Request::setHeader()
+ */
+ public function setHeader($header)
+ {
+ $this->getHeaders()->set($header);
+ return $this;
+ }
+
+ /* (non-phpdoc)
+ * @see Facade_Request::getHeaders()
+ */
+ public function getHeaders()
+ {
+ return $this->_headers;
+ }
+
+ /* (non-phpdoc)
+ * @see Facade_Request::hasHeader()
+ */
+ public function hasHeader($header)
+ {
+ return $this->getHeaders()->contains($header);
+ }
+
+ /**
+ * Sets the length of the content
+ */
+ public function setContentLength($length)
+ {
+ return $this->setHeader('Content-Length: '.$length);
+ }
+}
View
78 classes/Facade/ClassLoader.php
@@ -0,0 +1,78 @@
+<?php
+
+/**
+ * Generic classloader
+ */
+class Facade_ClassLoader
+{
+ private $_paths = array();
+
+ /**
+ * Registers this class as an SPL class loader.
+ */
+ public function register()
+ {
+ spl_autoload_register(array($this, 'loadClass'));
+ return $this;
+ }
+
+ /**
+ * Unregisters this class as an SPL class loader, does not attempt to
+ * unregister include_path entries.
+ */
+ public function unregister()
+ {
+ spl_autoload_unregister(array($this, 'loadClass'));
+ return $this;
+ }
+
+ /**
+ * SPL autoload function, loads a class file based on the class name.
+ *
+ * @param string
+ */
+ public function loadClass($className)
+ {
+ if (class_exists($className, false) || interface_exists($className, false))
+ {
+ return false;
+ }
+
+ $classFile = preg_replace('#_#', '/', $className).'.php';
+
+ foreach ($this->_paths as $path)
+ {
+ $classPath = "$path/$classFile";
+
+ if (file_exists($classPath)) {
+ require $classPath;
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * Prepends one or more items to the include path of the class loader and
+ * the php include path.
+ * @param mixed $items Path or paths as string or array
+ */
+ public function includePaths($path)
+ {
+ $paths = is_array($path) ? $path : array($path);
+ $this->_paths = array_merge($paths,$this->_paths);
+ return $this;
+ }
+
+ /**
+ * Exports the classloader path into the PHP system include path
+ */
+ public function export()
+ {
+ $systemPaths = explode(PATH_SEPARATOR, get_include_path());
+ set_include_path(implode(PATH_SEPARATOR,
+ array_merge($systemPaths,$this->_paths)));
+ return $this;
+ }
+}
View
3 classes/Facade/Exception.php
@@ -0,0 +1,3 @@
+<?php
+
+class Facade_Exception extends Exception {}
View
70 classes/Facade/Header.php
@@ -0,0 +1,70 @@
+<?php
+
+/**
+ * A header belonging to am {@link Facade_Request} or a {@link Facade_Response}
+ * @author Paul Annesley <paul@annesley.cc>
+ * @licence http://www.opensource.org/licenses/mit-license.php
+ * @see http://github.com/pda/bringit
+ */
+class Facade_Header
+{
+ const CRLF = "\r\n";
+
+ private $_name;
+ private $_value;
+
+ /**
+ * @param string $name
+ * @param string $value
+ */
+ public function __construct($name, $value)
+ {
+ $normalizer = new Facade_HeaderCaseNormalizer();
+ $this->_name = $normalizer->normalize($name);
+ $this->_value = $value;
+ }
+
+ /**
+ * The case-normalized name of the header.
+ * @return string
+ */
+ public function getName()
+ {
+ return $this->_name;
+ }
+
+ /**
+ * The value of the header.
+ * @return string
+ */
+ public function getValue()
+ {
+ return $this->_value;
+ }
+
+ /**
+ * The full header string, e.g. 'Example-Header: Some Value'
+ * @return string
+ */
+ public function __toString()
+ {
+ return sprintf(
+ '%s: %s%s',
+ $this->getName(),
+ $this->getValue(),
+ self::CRLF
+ );
+ }
+
+ /**
+ * Creates a header from a string representing a single header.
+ * @param string $headerString
+ * @return
+ */
+ public static function fromString($headerString)
+ {
+ $headerString = trim($headerString);
+ list($name, $value) = explode(': ', trim($headerString), 2);
+ return new self($name, $value);
+ }
+}
View
27 classes/Facade/HeaderCaseNormalizer.php
@@ -0,0 +1,27 @@
+<?php
+
+/**
+ * Normalizes the capitalization of header names e.g. Content-Type.
+ * @author Paul Annesley <paul@annesley.cc>
+ * @licence http://www.opensource.org/licenses/mit-license.php
+ * @see http://github.com/pda/bringit
+ */
+class Facade_HeaderCaseNormalizer
+{
+ /**
+ * @param string $string
+ * @return string
+ */
+ public function normalize($string)
+ {
+ return preg_replace_callback('#\w+#', array($this, '_callback'), $string);
+ }
+
+ /**
+ * Callback for preg_replace in self::normalize()
+ */
+ private function _callback($matches)
+ {
+ return ucwords($matches[0]);
+ }
+}
View
172 classes/Facade/HeaderCollection.php
@@ -0,0 +1,172 @@
+<?php
+
+/**
+ * A collection of {@link Facade_Header} objects
+ */
+class Facade_HeaderCollection implements IteratorAggregate
+{
+ private $_headers=array();
+
+ /**
+ * Constructor
+ * @param $headers Facade_Header[]
+ */
+ public function __construct($headers=array())
+ {
+ foreach($headers as $header) $this->add($header);
+ }
+
+ /**
+ * Adds a header
+ * @param mixed either string in "Header: Value" format or {@link Facade_Header}
+ * @chainable
+ */
+ function add($header)
+ {
+ // convert to object form
+ if(is_string($header)) $header = Facade_Header::fromString($header);
+
+ $this->_headers[] = $header;
+ return $this;
+ }
+
+ /**
+ * Sets a header
+ * @param mixed either string in "Header: Value" format or {@link Facade_Header}
+ * @chainable
+ */
+ function set($header)
+ {
+ // convert to object form
+ if(is_string($header)) $header = Facade_Header::fromString($header);
+
+ return $this
+ ->remove($header->getName())
+ ->add($header)
+ ;
+ }
+
+ /**
+ * Removes a header from the collection
+ * @param string the name of a header
+ * @chainable
+ */
+ function remove($name)
+ {
+ $normalizer = new Facade_HeaderCaseNormalizer();
+ $name = $normalizer->normalize($name);
+
+ foreach($this->_headers as $idx=>$header)
+ {
+ if($header->getName() == $name)
+ {
+ unset($this->_headers[$idx]);
+ }
+ }
+
+ return $this;
+ }
+
+ /**
+ * Gets a single header value
+ * @return string
+ */
+ function value($name, $default=false)
+ {
+ $values = $this->values($name);
+ return count($values) ? $values[0] : $default;
+ }
+
+ /**
+ * Gets an array of the values for a header
+ * @return array
+ */
+ function values($name)
+ {
+ $normalizer = new Facade_HeaderCaseNormalizer();
+ $name = $normalizer->normalize($name);
+ $values = array();
+
+ foreach($this->_headers as $header)
+ {
+ if($header->getName() == $name)
+ {
+ $values[] = $header->getValue();
+ }
+ }
+
+ return $values;
+ }
+
+ /**
+ * Whether the collection contains a specific header
+ * @return bool
+ */
+ public function contains($name)
+ {
+ $values = $this->values($name);
+ return count($values);
+ }
+
+ /**
+ * Returns a collection of headers where then name matches the pattern
+ * @return Facade_HeaderCollection
+ */
+ public function filter($pattern)
+ {
+ $result = array();
+
+ foreach($this->_headers as $header)
+ {
+ if(preg_match($pattern, $header->getName()))
+ {
+ $result[] = $header;
+ }
+ }
+
+ return new Facade_HeaderCollection($result);
+ }
+
+ /**
+ * Sorts the collection by the passed callback, defaults to sorting by key
+ * @chainable
+ */
+ public function sort($callback=null)
+ {
+ $headers = $this->_headers;
+ $callback = $callback ? $callback : array($this,'_compareNames');
+ usort($headers, $callback);
+
+ return new Facade_HeaderCollection($headers);
+ }
+
+ /**
+ * Returns an array of the string versions of headers
+ * @return array
+ */
+ public function toArray($crlf=true)
+ {
+ $headers = array();
+
+ foreach($this->_headers as $header)
+ {
+ $string = $header->__toString();
+ $headers[] = $crlf ? $string : rtrim($string);
+ }
+
+ return $headers;
+ }
+
+ /* (non-phpdoc)
+ * @see IteratorAggregate::getIterator
+ */
+ public function getIterator()
+ {
+ return new ArrayIterator(array_values($this->_headers));
+ }
+
+ private function _compareNames($a, $b)
+ {
+ return strcmp($a->getName(), $b->getName());
+ }
+}
View
73 classes/Facade/Http/Request.php
@@ -0,0 +1,73 @@
+<?php
+
+/**
+ * An HTTP 1.0 request
+ */
+class Facade_Http_Request extends Facade_AbstractRequest
+{
+ const METHOD_PUT='PUT';
+ const METHOD_GET='GET';
+ const METHOD_POST='POST';
+ const METHOD_HEAD='HEAD';
+
+ private $_socket;
+ private $_method;
+ private $_path;
+
+ /**
+ * Constructor
+ */
+ public function __construct($socket, $method, $path)
+ {
+ parent::__construct();
+ $this->_socket = $socket;
+ $this->_method = $method;
+ $this->_path = $path;
+ $this->_headers = array();
+ }
+
+ /**
+ *
+ */
+ public function setContentType($mimetype)
+ {
+ return $this->setHeader('Content-Type: '.$mimetype);
+ }
+
+ /**
+ *
+ */
+ public function setDate($timestamp)
+ {
+ return $this->setHeader('Date: '. gmdate('D, d M Y H:i:s T', $timestamp));
+ }
+
+ /* (non-phpdoc)
+ * @see Facade_Request::send()
+ */
+ public function send()
+ {
+ $headers = $this->getHeaders();
+
+ // set some defaults
+ if(!$headers->contains('Date')) $this->setDate(time());
+ if(!$headers->contains('Host')) $this->setHeader('Host: ' .$this->_socket->getHost());
+
+ // write the pre-amble
+ $this->_socket->writeRequest(
+ $this->_method,
+ $this->_path,
+ $this->getHeaders()
+ );
+
+ // most requests have a content stream
+ if($headers->contains('Content-Length'))
+ {
+ $this->_socket->copy($this->getContentStream(),
+ $headers->value('Content-Length'));
+ }
+
+ // build a response
+ return new Facade_Http_Response($this->_socket);
+ }
+}
View
106 classes/Facade/Http/Response.php
@@ -0,0 +1,106 @@
+<?php
+
+/**
+ * A response from an HTTP server
+ */
+class Facade_Http_Response implements Facade_Response
+{
+ private $_socket;
+ private $_headers;
+ private $_status;
+
+ /**
+ * Constructor
+ */
+ public function __construct($socket, $exception=true)
+ {
+ $this->_socket = $socket;
+ $this->_status = $this->_socket->readStatus();
+ $this->_headers = $this->_socket->readHeaders();
+
+ // throw an exception if the request failed
+ if($exception && !$this->isSuccessful() && !$this->_socket->isEof())
+ {
+ throw new Facade_Exception(
+ "Request failed: ".$this->getStatusMessage(),
+ $this->getStatusCode()
+ );
+ }
+ }
+
+ /**
+ * Whether the request was successful (returned a 200 response)
+ * @return bool
+ */
+ public function isSuccessful()
+ {
+ return $this->_status[0] == 200;
+ }
+
+ /**
+ * Gets the status code from the HTTP response
+ * @return int
+ */
+ public function getStatusCode()
+ {
+ return intval($this->_status[0]);
+ }
+
+ /**
+ * Gets the status message from the HTTP response
+ * @return string
+ */
+ public function getStatusMessage()
+ {
+ return $this->_status[1];
+ }
+
+ /* (non-phpdoc)
+ * @see Facade_Response
+ */
+ public function getHeaders()
+ {
+ return $this->_headers;
+ }
+
+ /**
+ * Gets the content of the response as a string
+ * @return string
+ */
+ public function getContentString()
+ {
+ if($this->getHeaders()->contains('Content-Length'))
+ {
+ // not sure if this is needed, but seemed sensible
+ return stream_get_contents($this->getContentStream(),
+ $this->getHeaders()->value('Content-Length'));
+ }
+ else
+ {
+ return stream_get_contents($this->getContentStream());
+ }
+ }
+
+ /**
+ * Gets the content of the response as a stream
+ * @return stream
+ */
+ public function getContentStream()
+ {
+ if($this->_socket->isEof())
+ {
+ throw new Facade_Exception("Response has no content");
+ }
+
+ return $this->_socket->getStream();
+ }
+
+ /**
+ * Gets the content of the response as an xml document
+ * @return SimpleXMLElement
+ */
+ public function getContentXml()
+ {
+ return new SimpleXMLElement($this->getContentString());
+ }
+}
View
196 classes/Facade/Http/Socket.php
@@ -0,0 +1,196 @@
+<?php
+
+/**
+ * An wrapper around a socket with http related methods
+ * @author Lachlan Donald <lachlan@ljd.cc>
+ */
+class Facade_Http_Socket
+{
+ private $_socket;
+ private $_timeout;
+ private $_host;
+ private $_port;
+ private $_debug=true;
+
+ /**
+ * Constructor
+ */
+ public function __construct($host, $port, $timeout=30)
+ {
+ $this->_host = $host;
+ $this->_port = $port;
+
+ // open the tcp socket
+ if(!$this->_socket = @fsockopen($this->_host, $this->_port, $errno, $errstr, $timeout))
+ {
+ throw new Exception("Failed to connect to $this->_host: $errstr");
+ }
+ }
+
+ /**
+ * Closes the socket
+ */
+ public function close()
+ {
+ fclose($this->_socket);
+ }
+
+ /**
+ * Basic socket read
+ * @return the number of bytes read
+ */
+ public function read($bytes=1024)
+ {
+ return fread($this->_socket, $bytes);
+ }
+
+ /**
+ * Reads a line until a carriage-return and newline is encountered
+ * @return string the line read
+ */
+ public function readLine()
+ {
+ $line = '';
+
+ while(!feof($this->_socket) && substr($line,-2) != "\r\n")
+ {
+ $line .= $this->read(1);
+ }
+
+ if($this->_debug) printf("<<< %s%s",trim($line),(php_sapi_name()=='cli'?"\n":'<br />'));
+ return $line;
+ }
+
+ /**
+ * Reads a line and parses the HTTP status response
+ */
+ public function readStatus()
+ {
+ if(!preg_match('#^HTTP/1.\d (\d+) (.+?)$#',trim($this->readLine()),$m))
+ {
+ throw new Exception("Malformed HTTP response from S3");
+ }
+
+ return array($m[1], $m[2]);
+ }
+
+ /**
+ * Reads lines and parses HTTP response headers from a stream
+ * @return Facade_HeaderCollection
+ */
+ public function readHeaders()
+ {
+ $headers = new Facade_HeaderCollection();
+
+ // read until headers are over
+ while(($line = $this->readLine()) !== "\r\n")
+ {
+ if(!preg_match("#^(.+?):(.+?)$#",trim($line),$m))
+ {
+ throw new Exception("Malformed HTTP header");
+ }
+
+ $headers->add($line);
+ }
+
+ return $headers;
+ }
+
+ /**
+ * Reads from the socket until EOF is encountered, or $maxbytes is met
+ */
+ public function readAll($maxbytes=-1)
+ {
+ return stream_get_contents($this->_socket, $maxbytes);
+ }
+
+ /**
+ * Basic socket write
+ * @chainable
+ */
+ public function write($line)
+ {
+ if($this->_debug) printf(">>> %s%s",trim($line),(php_sapi_name()=='cli'?"\n":'<br />'));
+ fwrite($this->_socket, $line);
+ return $this;
+ }
+
+ /**
+ * Writes an HTTP 1.0 request preamble to the socket
+ * @param string the HTTP method
+ * @param string the path to the object
+ * @param Facade_HeaderCollection
+ * @chainable
+ */
+ public function writeRequest($method, $path, $headers)
+ {
+ // use HTTP 1.0 for now
+ $this->write(sprintf("%s %s HTTP/1.0\r\n",$method, $path));
+
+ // write headers
+ foreach($headers->toArray() as $line) $this->write($line);
+
+ $this->write("\r\n");
+ return $this;
+ }
+
+ /**
+ * Copies a stream's contents (until EOF or $maxbytes) into this socket
+ * @chainable
+ */
+ public function copy($stream, $maxbytes=-1)
+ {
+ stream_copy_to_stream($stream, $this->_socket, $maxbytes);
+ return $this;
+ }
+
+ /**
+ * Determines if the socket is at the EOF
+ * @return bool
+ */
+ public function isEof()
+ {
+ return feof($this->_socket);
+ }
+
+ /**
+ * Gets the socket as a php stream
+ * @return stream
+ */
+ public function getStream()
+ {
+ return $this->_socket;
+ }
+
+ /**
+ * Gets the host that the socket is connecting to
+ * @return string
+ */
+ public function getHost()
+ {
+ return $this->_host;
+ }
+
+ /**
+ * Attempts to figure out the length of a stream
+ */
+ public static function getStreamLength($stream)
+ {
+ $metadata = stream_get_meta_data($stream);
+ $position = ftell($stream);
+ $length = false;
+
+ if(isset($metadata['uri']))
+ {
+ return filesize($metadata['uri']) - ftell($stream);
+ }
+ else if($metadata['seekable'])
+ {
+ fseek($stream, 0, SEEK_END);
+ $length = ftell($stream);
+ fseek($stream, $position, SEEK_SET);
+ }
+
+ return $length;
+ }
+}
View
49 classes/Facade/Request.php
@@ -0,0 +1,49 @@
+<?php
+
+/**
+ * A request made to a store
+ */
+interface Facade_Request
+{
+ /**
+ * Set the input to use a file
+ * @chainable
+ */
+ public function setContentFile($file);
+
+ /**
+ * Sets the content to send as a php stream, with an optional length
+ * @chainable
+ */
+ public function setContentStream($stream, $length=null);
+
+ /**
+ * Sets the content to send as a string
+ * @chainable
+ */
+ public function setContentString($string);
+
+ /**
+ * Sets the length of the content
+ * @chainable
+ */
+ public function setContentLength($bytes);
+
+ /**
+ * Sets a header
+ * @param either a string containing the header or a {@link Facade_Header}
+ */
+ public function setHeader($header);
+
+ /**
+ * Gets the collection of headers
+ * @return Facade_HeaderCollection
+ */
+ public function getHeaders();
+
+ /**
+ * Sends the request
+ * @return Facade_Response
+ */
+ public function send();
+}
View
37 classes/Facade/Response.php
@@ -0,0 +1,37 @@
+<?php
+
+/**
+ * A response from a store
+ */
+interface Facade_Response
+{
+ /**
+ * Whether the request was successful
+ * @return bool
+ */
+ public function isSuccessful();
+
+ /**
+ * Gets the headers from the response
+ * @return Facade_HeaderCollection
+ */
+ public function getHeaders();
+
+ /**
+ * Gets the content of the response as a string
+ * @return string
+ */
+ public function getContentString();
+
+ /**
+ * Gets the content of the response as a stream
+ * @return stream
+ */
+ public function getContentStream();
+
+ /**
+ * Gets the content of the response as an xml document
+ * @return SimpleXMLElement
+ */
+ public function getContentXml();
+}
View
83 classes/Facade/S3.php
@@ -0,0 +1,83 @@
+<?php
+
+/**
+ * A simple Amazon S3 store implementation
+ */
+class Facade_S3 implements Facade_Store
+{
+ private $_key;
+ private $_secret;
+ private $_timeout;
+
+ /**
+ * Constructor
+ * @param string AWS Access Key ID
+ * @param string AWS Secret Key
+ */
+ public function __construct($key, $secret, $timeout=30)
+ {
+ $this->_key = $key;
+ $this->_secret = $secret;
+ $this->_timeout = $timeout;
+ }
+
+ /* (non-phpdoc)
+ * @see Facade_Store::put()
+ */
+ public function put($path)
+ {
+ return $this
+ ->buildRequest(Facade_Http_Request::METHOD_PUT, $path);
+ }
+
+ /* (non-phpdoc)
+ * @see Facade_Store::get()
+ */
+ public function get($path)
+ {
+ return $this
+ ->buildRequest(Facade_Http_Request::METHOD_GET, $path);
+ }
+
+ /* (non-phpdoc)
+ * @see Facade_Store::head()
+ */
+ public function head($path)
+ {
+ return $this
+ ->buildRequest(Facade_Http_Request::METHOD_HEAD, $path);
+ }
+
+ /* (non-phpdoc)
+ * @see Facade_Store::post()
+ */
+ public function post($path, $data)
+ {
+ throw new BadMethodCallException(__METHOD__ . ' not implemented');
+ }
+
+ /* (non-phpdoc)
+ * @see Facade_Store::delete()
+ */
+ public function delete($path)
+ {
+ throw new BadMethodCallException(__METHOD__ . ' not implemented');
+ }
+
+
+ /**
+ * Builds an S3 request
+ */
+ private function buildRequest($method, $path)
+ {
+ return new Facade_S3_Request(
+ new Facade_Http_Socket('s3.amazonaws.com',80,$this->_timeout),
+ $this->_key,
+ $this->_secret,
+ $method,
+ $path
+ );
+ }
+}
+
+
View
139 classes/Facade/S3/Request.php
@@ -0,0 +1,139 @@
+<?php
+
+/**
+ * A request sent to Amazon's S3 service
+ */
+class Facade_S3_Request extends Facade_AbstractRequest
+{
+ private $_accesskey;
+ private $_method;
+ private $_secret;
+ private $_socket;
+ private $_path;
+
+ /**
+ * Constructor
+ */
+ public function __construct($socket, $accesskey, $secret, $method, $path)
+ {
+ parent::__construct();
+ $this->_accesskey = $accesskey;
+ $this->_secret = $secret;
+ $this->_socket = $socket;
+ $this->_method = $method;
+ $this->_path = $path;
+ }
+
+ /**
+ *
+ */
+ public function setContentType($mimetype)
+ {
+ return $this->setHeader('Content-Type: '.$mimetype);
+ }
+
+ /**
+ *
+ */
+ public function setAcl($acl)
+ {
+ return $this->setHeader('x-amz-acl: '.$acl);
+ }
+
+ /**
+ *
+ */
+ public function setDate($timestamp)
+ {
+ return $this->setHeader('Date: '. gmdate('D, d M Y H:i:s T', $timestamp));
+ }
+
+ /**
+ * Sends the request
+ */
+ public function send()
+ {
+ $headers = $this->getHeaders();
+
+ // set some defaults
+ if(!$headers->contains('Date')) $this->setDate(time());
+ if(!$headers->contains('Host')) $this->setHeader('Host: ' .$this->_socket->getHost());
+ if(!$headers->contains('x-amz-acl')) $this->setHeader('x-amz-acl: private');
+
+ // add the amazon signature
+ $headers->set(sprintf('Authorization: AWS %s:%s',
+ $this->_accesskey, $this->signature()));
+
+ // write the pre-amble
+ $this->_socket->writeRequest(
+ $this->_method,
+ $this->_path,
+ $this->getHeaders()
+ );
+
+ // most requests have a content stream
+ if($headers->contains('Content-Length'))
+ {
+ $this->_socket->copy($this->getContentStream(),
+ $headers->value('Content-Length'));
+ }
+
+ // build a response
+ return new Facade_S3_Response($this->_socket);
+ }
+
+ // ---------------------------------------------------------
+ // signature helper methods
+
+ private function signature()
+ {
+ $headers = $this->getHeaders();
+ $date = $headers->value('Date');
+ $md5 = $headers->value('Content-MD5');
+ $type = $headers->value('Content-Type');
+
+ // canonicalize the amazon headers
+ $amazonHeaders = $headers->filter('/^x-amz/i')->sort();
+ $canonicalized = '';
+
+ foreach ($amazonHeaders as $header)
+ $canonicalized .= strtolower($header->getName()).':'.$header->getValue()."\n";
+
+ // build the string to sign
+ $plaintext = sprintf("%s\n%s\n%s\n%s\n%s%s",
+ $this->_method,
+ $md5,
+ $type,
+ $date,
+ $canonicalized,
+ $this->_path
+ );
+
+ return $this->base64($this->hmacsha1( $this->_secret, $plaintext));
+ }
+
+ /**
+ * @see http://pear.php.net/package/Crypt_HMAC/
+ */
+ private function hmacsha1($key, $data)
+ {
+ if (strlen($key) > 64)
+ $key = pack("H40", sha1($key));
+ if (strlen($key) < 64)
+ $key = str_pad($key, 64, chr(0));
+ $ipad = (substr($key, 0, 64) ^ str_repeat(chr(0x36), 64));
+ $opad = (substr($key, 0, 64) ^ str_repeat(chr(0x5C), 64));
+ return sha1($opad . pack("H40", sha1($ipad . $data)));
+ }
+
+ /**
+ * Returns the base64 version of a string
+ */
+ private function base64($str)
+ {
+ $ret = "";
+ for($i = 0; $i < strlen($str); $i += 2)
+ $ret .= chr(hexdec(substr($str, $i, 2)));
+ return base64_encode($ret);
+ }
+}
View
113 classes/Facade/S3/Response.php
@@ -0,0 +1,113 @@
+<?php
+
+/**
+ * A response from Amazon's S3 service
+ */
+class Facade_S3_Response implements Facade_Response
+{
+ private $_socket;
+ private $_headers;
+ private $_status;
+
+ /**
+ * Constructor
+ */
+ public function __construct($socket)
+ {
+ $this->_socket = $socket;
+ $this->_status = $this->_socket->readStatus();
+ $this->_headers = $this->_socket->readHeaders();
+
+ // throw an exception if the request failed
+ if(!$this->isSuccessful() && !$this->_socket->isEof())
+ {
+ $response = $this->getContentXml();
+
+ throw new Facade_Exception(
+ "S3 request failed: {$response->Message}",
+ $this->getStatusCode()
+ );
+ }
+ }
+
+ /**
+ * Whether the request was successful (returned a 200 response)
+ * @return bool
+ */
+ public function isSuccessful()
+ {
+ return $this->_status[0] == 200;
+ }
+
+ /**
+ * Gets the status code from the HTTP response
+ * @return int
+ */
+ public function getStatusCode()
+ {
+ return intval($this->_status[0]);
+ }
+
+ /**
+ * Gets the status message from the HTTP response
+ * @return string
+ */
+ public function getStatusMessage()
+ {
+ return $this->_status[1];
+ }
+
+ /* (non-phpdoc)
+ * @see Facade_Response
+ */
+ public function getHeaders()
+ {
+ return $this->_headers;
+ }
+
+ /**
+ * Gets the content of the response as a string
+ * @return string
+ */
+ public function getContentString()
+ {
+ if($this->getHeaders()->contains('Content-Length'))
+ {
+ // not sure if this is needed, but seemed sensible
+ return stream_get_contents($this->getContentStream(),
+ $this->getHeaders()->value('Content-Length'));
+ }
+ else
+ {
+ return stream_get_contents($this->getContentStream());
+ }
+ }
+
+ /**
+ * Gets the content of the response as a stream
+ * @return stream
+ */
+ public function getContentStream()
+ {
+ if($this->_socket->isEof())
+ {
+ throw new Contests_Aws_S3Exception("Response has no content");
+ }
+
+ return $this->_socket->getStream();
+ }
+
+ /**
+ * Gets the content of the response as an xml document
+ * @return SimpleXMLElement
+ */
+ public function getContentXml()
+ {
+ if($this->getHeaders()->value('Content-Type') != 'application/xml')
+ {
+ throw new Contests_Aws_S3Exception("Response is not xml");
+ }
+
+ return new SimpleXMLElement($this->getContentString());
+ }
+}
View
45 classes/Facade/Store.php
@@ -0,0 +1,45 @@
+<?php
+
+/**
+ * A set of primitive actions that can be made against any facade store. These
+ * primatives return {@link Facade_Response} objects subtyped for the specific
+ * storage system.
+ */
+interface Facade_Store
+{
+ /**
+ * Gets an object from a store.
+ * @param mixed {@link Facade_Path} or string
+ * @return Facade_Request
+ */
+ public function get($path);
+
+ /**
+ * Gets an object from a store.
+ * @param mixed {@link Facade_Path} or string
+ * @return Facade_Request
+ */
+ public function put($path);
+
+ /**
+ * Sends data to the store
+ * @param mixed {@link Facade_Path} or string
+ * @param array key value data to send to the store
+ * @return Facade_Request
+ */
+ public function post($path, $data);
+
+ /**
+ * Deletes an object from the store
+ * @param mixed {@link Facade_Path} or string
+ * @return Facade_Request
+ */
+ public function delete($path);
+
+ /**
+ * Gets an object's headers from the store
+ * @param mixed {@link Facade_Path} or string
+ * @return Facade_Request
+ */
+ public function head($path);
+}
1 lib/simpletest
@@ -0,0 +1 @@
+Subproject commit 53fced5b9f94927d556d730420679987d81eb155
View
1 README.md → readme.md
@@ -8,6 +8,7 @@ caching.
The backends supported will initially be:
* [Amazon S3][3]
+* HTTP
* Filesystem
Other backends which will follow:
View
48 test-s3.php
@@ -0,0 +1,48 @@
+#!/usr/bin/env php
+<?php
+
+define('BASEDIR',dirname(__FILE__));
+require_once(BASEDIR.'/classes/Facade/ClassLoader.php');
+
+$classLoader = new Facade_ClassLoader();
+$classLoader->includePaths(array(BASEDIR.'/classes'))->register();
+
+// show help
+if(in_array('-h', $argv) || in_array('--help', $argv) || count($argv) <> 3)
+{
+ echo "uploads a file to S3, then downloads it again.\n";
+ echo "\nusage: $argv[0] (bucket) (filename)\n\n";
+ echo "\n";
+ exit(1);
+}
+
+// s3 auth details are in the shell env
+if(!isset($_ENV['AWS_ACCESS_KEY_ID']) || !isset($_ENV['AWS_SECRET_ACCESS_KEY']))
+{
+ die("AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY must be set in shell environment");
+}
+
+$file = $argv[2];
+$bucket = $argv[1];
+$objectName = basename($file);
+
+$s3 = new Facade_S3(
+ $_ENV['AWS_ACCESS_KEY_ID'],
+ $_ENV['AWS_SECRET_ACCESS_KEY']
+ );
+
+$response = $s3
+ ->put(sprintf("/%s/%s",$bucket,$objectName))
+ ->setContentFile($file)
+ ->setContentType('image/jpeg')
+ ->setHeader('Content-MD5: '.base64_encode(md5_file($file, true)))
+ ->send();
+
+$response = $s3
+ ->get(sprintf("/%s/%s",$bucket,$objectName))
+ ->send();
+
+if(strlen($response->getContentString()) != filesize($file))
+{
+ die("response size doesn't match sent size");
+}

0 comments on commit e37c8dd

Please sign in to comment.