diff --git a/README.txt b/README.txt new file mode 100644 index 00000000..a7feb553 --- /dev/null +++ b/README.txt @@ -0,0 +1,97 @@ +AMAZON S3 PHP CLASS + + +USING THE CLASS + + +OO method (e,g; $s3->getObject(...)): +$s3 = new S3(awsAccessKey, awsSecretKey); + +Statically (e,g; S3::getObject(...)): +S3::setAuth(awsAccessKey, awsSecretKey); + + + +OBJECTS + + +Put an object from a string: + $s3->putObject($string, $bucketName, $uploadName, S3::ACL_PUBLIC_READ) + Legacy function: $s3->putObjectString($string, $bucketName, $uploadName, S3::ACL_PUBLIC_READ) + + + +Put an object from a file: + $s3->putObject($s3->inputFile($file, false), $bucketName, $uploadName, S3::ACL_PUBLIC_READ) + Legacy function: $s3->putObjectFile($uploadFile, $bucketName, $uploadName, S3::ACL_PUBLIC_READ) + + + +Put an object from a resource (buffer/file size is required): + Please note: the resource will be fclose()'d automatically + $s3->putObject($s3->inputResource(fopen($file, 'rb'), filesize($file)), $bucketName, $uploadName, S3::ACL_PUBLIC_READ) + + + +Get an object: + $s3->getObject($bucketName, $uploadName) + + + +Save an object to file: + $s3->getObject($bucketName, $uploadName, $saveName) + + + +Save an object to a resource of any type: + $s3->getObject($bucketName, $uploadName, fopen('savefile.txt', 'wb')) + + + +Delete an object: + $s3->deleteObject($bucketName, $uploadName) + + + +BUCKETS + + +Get a list of buckets: + $s3->listBuckets() // Simple bucket list + $s3->listBuckets(true) // Detailed bucket list + + +Create a public-read bucket: + $s3->putBucket($bucketName, S3::ACL_PUBLIC_READ) + + +Get the contents of a bucket: + $s3->getBucket($bucketName) + + +Delete a bucket: + $s3->deleteBucket($bucketName) + + + + +KNOWN ISSUES + + Files larger than 2GB are not supported on 32 bit systems due to PHP’s signed integer problem + + + +MORE INFORMATION + + + Project URL (please report bugs here): + http://undesigned.org.za/2007/10/22/amazon-s3-php-class + + + Amazon S3 documentation: + http://docs.amazonwebservices.com/AmazonS3/2006-03-01/ + + + + +EOF! \ No newline at end of file diff --git a/S3.php b/S3.php new file mode 100644 index 00000000..0601f1c6 --- /dev/null +++ b/S3.php @@ -0,0 +1,903 @@ +getResponse(); + if ($rest->error === false && $rest->code !== 200) + $rest->error = array('code' => $rest->code, 'message' => 'Unexpected HTTP status'); + if ($rest->error !== false) { + trigger_error(sprintf("S3::listBuckets(): [%s] %s", $rest->error['code'], $rest->error['message']), E_USER_WARNING); + return false; + } + $results = array(); //var_dump($rest->body); + if (!isset($rest->body->Buckets)) return $results; + + if ($detailed) { + if (isset($rest->body->Owner, $rest->body->Owner->ID, $rest->body->Owner->DisplayName)) + $results['owner'] = array( + 'id' => (string)$rest->body->Owner->ID, 'name' => (string)$rest->body->Owner->ID + ); + $results['buckets'] = array(); + foreach ($rest->body->Buckets->Bucket as $b) + $results['buckets'][] = array( + 'name' => (string)$b->Name, 'time' => strtotime((string)$b->CreationDate) + ); + } else + foreach ($rest->body->Buckets->Bucket as $b) $results[] = (string)$b->Name; + + return $results; + } + + + /* + * Get contents for a bucket + * + * If maxKeys is null this method will loop through truncated result sets + * + * @param string $bucket Bucket name + * @param string $prefix Prefix + * @param string $marker Marker (last file listed) + * @param string $maxKeys Max keys (maximum number of keys to return) + * @return array | false + */ + public static function getBucket($bucket, $prefix = null, $marker = null, $maxKeys = null) { + $rest = new S3Request('GET', $bucket, ''); + if ($prefix !== null && $prefix !== '') $rest->setParameter('prefix', $prefix); + if ($marker !== null && $prefix !== '') $rest->setParameter('marker', $marker); + if ($maxKeys !== null && $prefix !== '') $rest->setParameter('max-keys', $maxKeys); + $response = $rest->getResponse(); + if ($response->error === false && $response->code !== 200) + $response->error = array('code' => $response->code, 'message' => 'Unexpected HTTP status'); + if ($response->error !== false) { + trigger_error(sprintf("S3::getBucket(): [%s] %s", $response->error['code'], $response->error['message']), E_USER_WARNING); + return false; + } + + $results = array(); + + $lastMarker = null; + if (isset($response->body, $response->body->Contents)) + foreach ($response->body->Contents as $c) { + $results[(string)$c->Key] = array( + 'name' => (string)$c->Key, + 'time' => strToTime((string)$c->LastModified), + 'size' => (int)$c->Size, + 'hash' => substr((string)$c->ETag, 1, -1) + ); + $lastMarker = (string)$c->Key; + //$response->body->IsTruncated = 'true'; break; + } + + + if (isset($response->body->IsTruncated) && + (string)$response->body->IsTruncated == 'false') return $results; + + // Loop through truncated results if maxKeys isn't specified + if ($maxKeys == null && $lastMarker !== null && (string)$response->body->IsTruncated == 'true') + do { + $rest = new S3Request('GET', $bucket, ''); + if ($prefix !== null) $rest->setParameter('prefix', $prefix); + $rest->setParameter('marker', $lastMarker); + + if (($response = $rest->getResponse(true)) == false || $response->code !== 200) break; + if (isset($response->body, $response->body->Contents)) + foreach ($response->body->Contents as $c) { + $results[(string)$c->Key] = array( + 'name' => (string)$c->Key, + 'time' => strToTime((string)$c->LastModified), + 'size' => (int)$c->Size, + 'hash' => substr((string)$c->ETag, 1, -1) + ); + $lastMarker = (string)$c->Key; + } + } while ($response !== false && (string)$response->body->IsTruncated == 'true'); + + return $results; + } + + + /** + * Put a bucket + * + * @param string $bucket Bucket name + * @param constant $acl ACL flag + * @return boolean + */ + public function putBucket($bucket, $acl = self::ACL_PRIVATE) { + $rest = new S3Request('PUT', $bucket, ''); + $rest->setAmzHeader('x-amz-acl', $acl); + $rest = $rest->getResponse(); + if ($rest->error === false && $rest->code !== 200) + $rest->error = array('code' => $rest->code, 'message' => 'Unexpected HTTP status'); + if ($rest->error !== false) { + trigger_error(sprintf("S3::putBucket({$bucket}): [%s] %s", + $rest->error['code'], $rest->error['message']), E_USER_WARNING); + return false; + } + return true; + } + + + /** + * Delete an empty bucket + * + * @param string $bucket Bucket name + * @return boolean + */ + public function deleteBucket($bucket = '') { + $rest = new S3Request('DELETE', $bucket); + $rest = $rest->getResponse(); + if ($rest->error === false && $rest->code !== 204) + $rest->error = array('code' => $rest->code, 'message' => 'Unexpected HTTP status'); + if ($rest->error !== false) { + trigger_error(sprintf("S3::deleteBucket({$bucket}): [%s] %s", + $rest->error['code'], $rest->error['message']), E_USER_WARNING); + return false; + } + return true; + } + + + /** + * Create input info array for putObject() + * + * @param string $file Input file + * @param mixed $md5sum Use MD5 hash (supply a string if you want to use your own) + * @return array | false + */ + public static function inputFile($file, $md5sum = true) { + if (!file_exists($file) || !is_file($file) || !is_readable($file)) { + trigger_error('S3::inputFile(): Unable to open input file: '.$file, E_USER_WARNING); + return false; + } + return array('file' => $file, 'size' => filesize($file), + 'md5sum' => $md5sum !== false ? (is_string($md5sum) ? $md5sum : + base64_encode(md5_file($file, true))) : ''); + } + + + /** + * Use a resource for input + * + * @param string $file Input file + * @param integer $bufferSize Input byte size + * @param string $md5sum MD5 hash to send (optional) + * @return array | false + */ + public static function inputResource(&$resource, $bufferSize, $md5sum = '') { + if (!is_resource($resource) || $bufferSize <= 0) { + trigger_error('S3::inputResource(): Invalid resource or buffer size', E_USER_WARNING); + return false; + } + $input = array('size' => $bufferSize, 'md5sum' => $md5sum); + $input['fp'] =& $resource; + return $input; + } + + + /** + * Put an object + * + * @param mixed $input Input data + * @param string $bucket Bucket name + * @param string $uri Object URI + * @param constant $acl ACL constant + * @param array $metaHeaders Array of x-amz-meta-* headers + * @param string $contentType Content type + * @return boolean + */ + public static function putObject($input, $bucket, $uri, $acl = self::ACL_PRIVATE, $metaHeaders = array(), $contentType = null) { + if ($input == false) return false; + $rest = new S3Request('PUT', $bucket, $uri); + + if (is_string($input)) $input = array( + 'data' => $input, 'size' => strlen($input), + 'md5sum' => base64_encode(md5($input, true)) + ); + + // Data + if (isset($input['fp'])) + $rest->fp =& $input['fp']; + elseif (isset($input['file'])) + $rest->fp = @fopen($input['file'], 'rb'); + elseif (isset($input['data'])) + $rest->data = $input['data']; + + // Content-Length + if (isset($input['size']) && $input['size'] > 0) + $rest->size = $input['size']; + else { + if (isset($input['file'])) + $rest->size = filesize($input['file']); + elseif (isset($input['data'])) + $rest->size = strlen($input['data']); + } + + // Content-Type + if ($contentType !== null) + $input['type'] = $contentType; + elseif (!isset($input['type']) && isset($input['file'])) + $input['type'] = self::__getMimeType($input['file']); + else + $input['type'] = 'application/octet-stream'; + + // We need to post with the content-length and content-type, MD5 is optional + if ($rest->size > 0 && ($rest->fp !== false || $rest->data !== false)) { + $rest->setHeader('Content-Type', $input['type']); + if (isset($input['md5sum'])) $rest->setHeader('Content-MD5', $input['md5sum']); + + $rest->setAmzHeader('x-amz-acl', $acl); + foreach ($metaHeaders as $h => $v) $rest->setAmzHeader('x-amz-meta-'.$h, $v); + $rest->getResponse(); + } else + $rest->response->error = array('code' => 0, 'message' => 'Missing input parameters'); + + if ($rest->response->error === false && $rest->response->code !== 200) + $rest->response->error = array('code' => $rest->response->code, 'message' => 'Unexpected HTTP status'); + if ($rest->response->error !== false) { + trigger_error(sprintf("S3::putObject(): [%s] %s", $rest->response->error['code'], $rest->response->error['message']), E_USER_WARNING); + return false; + } + return true; + } + + + /** + * Puts an object from a file (legacy function) + * + * @param string $file Input file path + * @param string $bucket Bucket name + * @param string $uri Object URI + * @param constant $acl ACL constant + * @param array $metaHeaders Array of x-amz-meta-* headers + * @param string $contentType Content type + * @return boolean + */ + public static function putObjectFile($file, $bucket, $uri, $acl = self::ACL_PRIVATE, $metaHeaders = array(), $contentType = null) { + return self::putObject(S3::inputFile($file), $bucket, $uri, $acl, $metaHeaders, $contentType); + } + + + /** + * Put an object from a string (legacy function) + * + * @param string $string Input data + * @param string $bucket Bucket name + * @param string $uri Object URI + * @param constant $acl ACL constant + * @param array $metaHeaders Array of x-amz-meta-* headers + * @param string $contentType Content type + * @return boolean + */ + public function putObjectString($string, $bucket, $uri, $acl = self::ACL_PRIVATE, $metaHeaders = array(), $contentType = 'text/plain') { + return self::putObject($string, $bucket, $uri, $acl, $metaHeaders, $contentType); + } + + + /** + * Get an object + * + * @param string $bucket Bucket name + * @param string $uri Object URI + * @param mixed &$saveTo Filename or resource to write to + * @return mixed + */ + public static function getObject($bucket = '', $uri = '', $saveTo = false) { + $rest = new S3Request('GET', $bucket, $uri); + if ($saveTo !== false) { + if (is_resource($saveTo)) + $rest->fp =& $saveTo; + else + if (($rest->fp = @fopen($saveTo, 'wb')) == false) + $rest->response->error = array('code' => 0, 'message' => 'Unable to open save file for writing: '.$saveTo); + } + if ($rest->response->error === false) $rest->getResponse(); + + if ($rest->response->error === false && $rest->response->code !== 200) + $rest->response->error = array('code' => $rest->response->code, 'message' => 'Unexpected HTTP status'); + if ($rest->response->error !== false) { + trigger_error(sprintf("S3::getObject({$bucket}, {$uri}): [%s] %s", + $rest->response->error['code'], $rest->response->error['message']), E_USER_WARNING); + return false; + } + $rest->file = realpath($saveTo); + return $rest->response; + } + + + /** + * Get object information + * + * @param string $bucket Bucket name + * @param string $uri Object URI + * @param boolean $returnInfo Return response information + * @return mixed | false + */ + public static function getObjectInfo($bucket = '', $uri = '', $returnInfo = true) { + $rest = new S3Request('HEAD', $bucket, $uri); + $rest = $rest->getResponse(); + if ($rest->error === false && ($rest->code !== 200 && $rest->code !== 404)) + $rest->error = array('code' => $rest->code, 'message' => 'Unexpected HTTP status'); + if ($rest->error !== false) { + trigger_error(sprintf("S3::getObjectInfo({$bucket}, {$uri}): [%s] %s", + $rest->error['code'], $rest->error['message']), E_USER_WARNING); + return false; + } + return $rest->code == 200 ? $returnInfo ? $rest->headers : true : false; + } + + + /** + * Enable logging for buckets + * + * @param string $bucket Bucket name + * @param string $targetBucket Target bug (where logs are stored) + * @param string $targetPrefix Log prefix (e,g; domain.com-) + * @return boolean + */ + public static function enableBucketLogging($bucket, $targetBucket, $targetPrefix) { + $dom = new DOMDocument; + $bucketLoggingStatus = $dom->createElement('BucketLoggingStatus'); + $bucketLoggingStatus->setAttribute('xmlns', 'http://s3.amazonaws.com/doc/2006-03-01/'); + + $loggingEnabled = $dom->createElement('LoggingEnabled'); + + $loggingEnabled->appendChild($dom->createElement('TargetBucket', $targetBucket)); + $loggingEnabled->appendChild($dom->createElement('TargetPrefix', $targetPrefix)); + + // TODO: Add TargetGrants + + $bucketLoggingStatus->appendChild($loggingEnabled); + $dom->appendChild($bucketLoggingStatus); + + $rest = new S3Request('PUT', $bucket, ''); + $rest->setParameter('logging', null); + $rest->data = $dom->saveXML(); + $rest->size = strlen($rest->data); + $rest->setHeader('Content-Type', 'application/xml'); + $rest = $rest->getResponse(); + if ($rest->error === false && $rest->code !== 200) + $rest->error = array('code' => $rest->code, 'message' => 'Unexpected HTTP status'); + if ($rest->error !== false) { + trigger_error(sprintf("S3::enableBucketLogging({$bucket}, {$uri}): [%s] %s", + $rest->error['code'], $rest->error['message']), E_USER_WARNING); + return false; + } + return true; + } + + + /** + * Get logging status for a bucket + * + * This will return false if logging is not enabled. + * Note: To enable logging, you also need to grant write access to the log group + * + * @param string $bucket Bucket name + * @return array | false + */ + public static function getBucketLoggingStatus($bucket = '') { + $rest = new S3Request('GET', $bucket, ''); + $rest->setParameter('logging', null); + $rest = $rest->getResponse(); + if ($rest->error === false && $rest->code !== 200) + $rest->error = array('code' => $rest->code, 'message' => 'Unexpected HTTP status'); + if ($rest->error !== false) { + trigger_error(sprintf("S3::getBucketLoggingStatus({$bucket}): [%s] %s", + $rest->error['code'], $rest->error['message']), E_USER_WARNING); + return false; + } + if (!isset($rest->body->LoggingEnabled)) return false; // No logging + return array( + 'targetBucket' => (string)$rest->body->LoggingEnabled->TargetBucket, + 'targetPrefix' => (string)$rest->body->LoggingEnabled->TargetPrefix, + ); + } + + + /** + * Set object or bucket Access Control Policy + * + * @param string $bucket Bucket name + * @param string $uri Object URI + * @param array $acp Access Control Policy Data (same as the data returned from getAccessControlPolicy) + * @return boolean + */ + public static function setAccessControlPolicy($bucket, $uri = '', $acp = array()) { + $dom = new DOMDocument; + $dom->formatOutput = true; + $accessControlPolicy = $dom->createElement('AccessControlPolicy'); + $accessControlList = $dom->createElement('AccessControlList'); + + // It seems the owner has to be passed along too + $owner = $dom->createElement('Owner'); + $owner->appendChild($dom->createElement('ID', $acp['owner']['id'])); + $owner->appendChild($dom->createElement('DisplayName', $acp['owner']['name'])); + $accessControlPolicy->appendChild($owner); + + foreach ($acp['acl'] as $g) { + $grant = $dom->createElement('Grant'); + $grantee = $dom->createElement('Grantee'); + $grantee->setAttribute('xmlns:xsi', 'http://www.w3.org/2001/XMLSchema-instance'); + if (isset($g['id'])) { // CanonicalUser (DisplayName is omitted) + $grantee->setAttribute('xsi:type', 'CanonicalUser'); + $grantee->appendChild($dom->createElement('ID', $g['id'])); + } elseif (isset($g['email'])) { // AmazonCustomerByEmail + $grantee->setAttribute('xsi:type', 'AmazonCustomerByEmail'); + $grantee->appendChild($dom->createElement('EmailAddress', $g['email'])); + } elseif ($g['type'] == 'Group') { // Group + $grantee->setAttribute('xsi:type', 'Group'); + $grantee->appendChild($dom->createElement('URI', $g['uri'])); + } + $grant->appendChild($grantee); + $grant->appendChild($dom->createElement('Permission', $g['permission'])); + $accessControlList->appendChild($grant); + } + + $accessControlPolicy->appendChild($accessControlList); + $dom->appendChild($accessControlPolicy); + + $rest = new S3Request('PUT', $bucket, ''); + $rest->setParameter('acl', null); + $rest->data = $dom->saveXML(); + $rest->size = strlen($rest->data); + $rest->setHeader('Content-Type', 'application/xml'); + $rest = $rest->getResponse(); + if ($rest->error === false && $rest->code !== 200) + $rest->error = array('code' => $rest->code, 'message' => 'Unexpected HTTP status'); + if ($rest->error !== false) { + trigger_error(sprintf("S3::setAccessControlPolicy({$bucket}, {$uri}): [%s] %s", + $rest->error['code'], $rest->error['message']), E_USER_WARNING); + return false; + } + return true; + } + + + /** + * Get object or bucket Access Control Policy + * + * Currently this will trigger an error if there is no ACL on an object (will fix soon) + * + * @param string $bucket Bucket name + * @param string $uri Object URI + * @return mixed | false + */ + public static function getAccessControlPolicy($bucket, $uri = '') { + $rest = new S3Request('GET', $bucket, $uri); + $rest->setParameter('acl', null); + $rest = $rest->getResponse(); + if ($rest->error === false && $rest->code !== 200) + $rest->error = array('code' => $rest->code, 'message' => 'Unexpected HTTP status'); + if ($rest->error !== false) { + trigger_error(sprintf("S3::getAccessControlPolicy({$bucket}, {$uri}): [%s] %s", + $rest->error['code'], $rest->error['message']), E_USER_WARNING); + return false; + } + + $acp = array(); + if (isset($rest->body->Owner, $rest->body->Owner->ID, $rest->body->Owner->DisplayName)) { + $acp['owner'] = array( + 'id' => (string)$rest->body->Owner->ID, 'name' => (string)$rest->body->Owner->DisplayName + ); + } + if (isset($rest->body->AccessControlList)) { + $acp['acl'] = array(); + foreach ($rest->body->AccessControlList->Grant as $grant) { + foreach ($grant->Grantee as $grantee) { + if (isset($grantee->ID, $grantee->DisplayName)) // CanonicalUser + $acp['acl'][] = array( + 'type' => 'CanonicalUser', + 'id' => (string)$grantee->ID, + 'name' => (string)$grantee->DisplayName, + 'permission' => (string)$grant->Permission + ); + elseif (isset($grantee->EmailAddress)) // AmazonCustomerByEmail + $acp['acl'][] = array( + 'type' => 'AmazonCustomerByEmail', + 'email' => (string)$grantee->EmailAddress, + 'permission' => (string)$grant->Permission + ); + elseif (isset($grantee->URI)) // Group + $acp['acl'][] = array( + 'type' => 'Group', + 'uri' => (string)$grantee->URI, + 'permission' => (string)$grant->Permission + ); + else continue; + } + } + } + return $acp; + } + + + /** + * Delete an object + * + * @param string $bucket Bucket name + * @param string $uri Object URI + * @return mixed + */ + public static function deleteObject($bucket = '', $uri = '') { + $rest = new S3Request('DELETE', $bucket, $uri); + $rest = $rest->getResponse(); + if ($rest->error === false && $rest->code !== 204) + $rest->error = array('code' => $rest->code, 'message' => 'Unexpected HTTP status'); + if ($rest->error !== false) { + trigger_error(sprintf("S3::deleteObject(): [%s] %s", $rest->error['code'], $rest->error['message']), E_USER_WARNING); + return false; + } + return true; + } + + + /** + * Get MIME type for file + * + * @internal Used to get mime types + * @param string &$file File path + * @return string + */ + public static function __getMimeType(&$file) { + $type = false; + // Fileinfo documentation says fileinfo_open() will use the + // MAGIC env var for the magic file + if (extension_loaded('fileinfo') && isset($_ENV['MAGIC']) && + ($finfo = finfo_open(FILEINFO_MIME, $_ENV['MAGIC'])) !== false) { + if (($type = finfo_file($finfo, $file)) !== false) { + // Remove the charset and grab the last content-type + $type = explode(' ', str_replace('; charset=', ';charset=', $type)); + $type = array_pop($type); + $type = explode(';', $type); + $type = array_shift($type); + } + finfo_close($finfo); + + // If anyone is still using mime_content_type() + } elseif (function_exists('mime_content_type')) + $type = mime_content_type($file); + + if ($type !== false && strlen($type) > 0) return $type; + + // Otherwise do it the old fashioned way + static $exts = array( + 'jpg' => 'image/jpeg', 'gif' => 'image/gif', 'png' => 'image/png', + 'tif' => 'image/tiff', 'tiff' => 'image/tiff', 'ico' => 'image/x-icon', + 'swf' => 'application/x-shockwave-flash', 'pdf' => 'application/pdf', + 'zip' => 'application/zip', 'gz' => 'application/x-gzip', + 'tar' => 'application/x-tar', 'bz' => 'application/x-bzip', + 'bz2' => 'application/x-bzip2', 'txt' => 'text/plain', + 'asc' => 'text/plain', 'htm' => 'text/html', 'html' => 'text/html', + 'xml' => 'text/xml', 'xsl' => 'application/xsl+xml', + 'ogg' => 'application/ogg', 'mp3' => 'audio/mpeg', 'wav' => 'audio/x-wav', + 'avi' => 'video/x-msvideo', 'mpg' => 'video/mpeg', 'mpeg' => 'video/mpeg', + 'mov' => 'video/quicktime', 'flv' => 'video/x-flv', 'php' => 'text/x-php' + ); + $ext = strToLower(pathInfo($file, PATHINFO_EXTENSION)); + return isset($exts[$ext]) ? $exts[$ext] : 'application/octet-stream'; + } + + + /** + * Generate the auth string: "AWS AccessKey:Signature" + * + * This uses the hash extension if loaded + * + * @internal Signs the request + * @param string $string String to sign + * @return string + */ + public static function __getSignature($string) { + return 'AWS '.self::$__accessKey.':'.base64_encode(extension_loaded('hash') ? + hash_hmac('sha1', $string, self::$__secretKey, true) : pack('H*', sha1( + (str_pad(self::$__secretKey, 64, chr(0x00)) ^ (str_repeat(chr(0x5c), 64))) . + pack('H*', sha1((str_pad(self::$__secretKey, 64, chr(0x00)) ^ + (str_repeat(chr(0x36), 64))) . $string))))); + } + + +} + +final class S3Request { + private $verb, $bucket, $uri, $resource = '', $parameters = array(), + $amzHeaders = array(), $headers = array( + 'Host' => '', 'Date' => '', 'Content-MD5' => '', 'Content-Type' => '' + ); + public $fp = false, $size = 0, $data = false, $response; + + + /** + * Constructor + * + * @param string $verb Verb + * @param string $bucket Bucket name + * @param string $uri Object URI + * @return mixed + */ + function __construct($verb, $bucket = '', $uri = '') { + $this->verb = $verb; + $this->bucket = $bucket; + $this->uri = $uri !== '' ? '/'.$uri : '/'; + + if ($this->bucket !== '') { + $bucket = explode('/', $bucket); + $this->resource = '/'.$bucket[0].$this->uri; + $this->headers['Host'] = $bucket[0].'.s3.amazonaws.com'; + $this->bucket = implode('/', $bucket); + } else { + $this->headers['Host'] = 's3.amazonaws.com'; + if (strlen($this->uri) > 1) + $this->resource = '/'.$this->bucket.$this->uri; + else $this->resource = $this->uri; + } + $this->headers['Date'] = gmdate('D, d M Y H:i:s T'); + + $this->response = new STDClass; + $this->response->error = false; + } + + + /** + * Set request parameter + * + * @param string $key Key + * @param string $value Value + * @return void + */ + public function setParameter($key, $value) { + $this->parameters[$key] = $value; + } + + + /** + * Set request header + * + * @param string $key Key + * @param string $value Value + * @return void + */ + public function setHeader($key, $value) { + $this->headers[$key] = $value; + } + + + /** + * Set x-amz-meta-* header + * + * @param string $key Key + * @param string $value Value + * @return void + */ + public function setAmzHeader($key, $value) { + $this->amzHeaders[$key] = $value; + } + + + /** + * Get the S3 response + * + * @return object | false + */ + public function getResponse() { + $query = ''; + if (sizeof($this->parameters) > 0) { + $query = substr($this->uri, -1) !== '?' ? '?' : '&'; + foreach ($this->parameters as $var => $value) + if ($value == null || $value == '') $query .= $var.'&'; + else $query .= $var.'='.$value.'&'; + $query = substr($query, 0, -1); + $this->uri .= $query; + if (isset($this->parameters['acl']) || !isset($this->parameters['logging'])) + $this->resource .= $query; + } + $url = (extension_loaded('openssl')?'https://':'http://').$this->headers['Host'].$this->uri; + //var_dump($this->bucket, $this->uri, $this->resource, $url); + + // Basic setup + $curl = curl_init(); + curl_setopt($curl, CURLOPT_USERAGENT, 'S3/php'); + curl_setopt($curl, CURLOPT_SSL_VERIFYHOST, 0); + curl_setopt($curl, CURLOPT_SSL_VERIFYPEER, 0); + curl_setopt($curl, CURLOPT_URL, $url); + + // Headers + $headers = array(); $amz = array(); + foreach ($this->amzHeaders as $header => $value) + if (strlen($value) > 0) $headers[] = $header.': '.$value; + foreach ($this->headers as $header => $value) + if (strlen($value) > 0) $headers[] = $header.': '.$value; + foreach ($this->amzHeaders as $header => $value) + if (strlen($value) > 0) $amz[] = strToLower($header).':'.$value; + $amz = (sizeof($amz) > 0) ? "\n".implode("\n", $amz) : ''; + + // Authorization string + $headers[] = 'Authorization: ' . S3::__getSignature( + $this->verb."\n". + $this->headers['Content-MD5']."\n". + $this->headers['Content-Type']."\n". + $this->headers['Date'].$amz."\n".$this->resource + ); + + curl_setopt($curl, CURLOPT_HTTPHEADER, $headers); + curl_setopt($curl, CURLOPT_HEADER, false); + curl_setopt($curl, CURLOPT_RETURNTRANSFER, false); + curl_setopt($curl, CURLOPT_WRITEFUNCTION, array(&$this, '__responseWriteCallback')); + curl_setopt($curl, CURLOPT_HEADERFUNCTION, array(&$this, '__responseHeaderCallback')); + + // Request types + switch ($this->verb) { + case 'GET': break; + case 'PUT': + if ($this->fp !== false) { + curl_setopt($curl, CURLOPT_PUT, true); + curl_setopt($curl, CURLOPT_INFILE, $this->fp); + if ($this->size > 0) + curl_setopt($curl, CURLOPT_INFILESIZE, $this->size); + } elseif ($this->data !== false) { + curl_setopt($curl, CURLOPT_CUSTOMREQUEST, 'PUT'); + curl_setopt($curl, CURLOPT_POSTFIELDS, $this->data); + if ($this->size > 0) + curl_setopt($curl, CURLOPT_BUFFERSIZE, $this->size); + } else + curl_setopt($curl, CURLOPT_CUSTOMREQUEST, 'PUT'); + break; + case 'HEAD': + curl_setopt($curl, CURLOPT_CUSTOMREQUEST, 'HEAD'); + curl_setopt($curl, CURLOPT_NOBODY, true); + break; + case 'DELETE': + curl_setopt($curl, CURLOPT_CUSTOMREQUEST, 'DELETE'); + break; + default: break; + } + + // Exececute, grab errors + if (curl_exec($curl)) + $this->response->code = curl_getinfo($curl, CURLINFO_HTTP_CODE); + else + $this->response->error = array( + 'code' => curl_errno($curl), + 'message' => curl_error($curl), + 'resource' => $this->resource + ); + + @curl_close($curl); + + // Parse body into XML + if ($this->response->error === false && isset($this->response->headers['type']) && + $this->response->headers['type'] == 'application/xml' && isset($this->response->body)) { + $this->response->body = simplexml_load_string($this->response->body); + + // Grab S3 errors + if (!in_array($this->response->code, array(200, 204)) && + isset($this->response->body->Code, $this->response->body->Message)) { + $this->response->error = array( + 'code' => (string)$this->response->body->Code, + 'message' => (string)$this->response->body->Message + ); + if (isset($this->response->body->Resource)) + $this->response->error['resource'] = (string)$this->response->body->Resource; + unset($this->response->body); + } + } + + // Clean up file resources + if ($this->fp !== false && is_resource($this->fp)) fclose($this->fp); + + return $this->response; + } + + + /** + * CURL write callback + * + * @param resource &$curl CURL resource + * @param string &$data Data + * @return integer + */ + private function __responseWriteCallback(&$curl, &$data) { + if ($this->response->code == 200 && $this->fp !== false) + return fwrite($this->fp, $data); + else + $this->response->body .= $data; + return strlen($data); + } + + + /** + * CURL header callback + * + * @param resource &$curl CURL resource + * @param string &$data Data + * @return integer + */ + private function __responseHeaderCallback(&$curl, &$data) { + if (($strlen = strlen($data)) <= 2) return $strlen; + if (substr($data, 0, 4) == 'HTTP') + $this->response->code = (int)substr($data, 9, 3); + else { + list($header, $value) = explode(': ', trim($data)); + if ($header == 'Last-Modified') + $this->response->headers['time'] = strtotime($value); + elseif ($header == 'Content-Length') + $this->response->headers['size'] = (int)$value; + elseif ($header == 'Content-Type') + $this->response->headers['type'] = $value; + elseif ($header == 'ETag') + $this->response->headers['hash'] = substr($value, 1, -1); + elseif (preg_match('/^x-amz-meta-.*$/', $header)) + $this->response->headers[$header] = is_numeric($value) ? (int)$value : $value; + } + return $strlen; + } + +} diff --git a/example-wrapper.php b/example-wrapper.php new file mode 100755 index 00000000..f36daf26 --- /dev/null +++ b/example-wrapper.php @@ -0,0 +1,214 @@ +#!/usr/local/bin/php +url['host'], $this->url['path'])) !== false) ? + array('size' => $info['size'], 'mtime' => $info['time'], 'ctime' => $info['time']) : false; + } + + public function unlink($path) { + self::__getURL($path); + return self::deleteObject($this->url['host'], $this->url['path']); + } + + public function mkdir($path, $mode, $options) { + self::__getURL($path); + return self::putBucket($this->url['host'], self::__translateMode($mode)); + } + + public function rmdir($path) { + self::__getURL($path); + return self::deleteBucket($this->url['host']); + } + + public function dir_opendir($path, $options) { + self::__getURL($path); + if (($contents = self::getBucket($this->url['host'], $this->url['path'])) !== false) { + $pathlen = strlen($this->url['path']); + if (substr($this->url['path'], -1) == '/') $pathlen++; + $this->buffer = array(); + foreach ($contents as $file) { + if ($pathlen > 0) $file['name'] = substr($file['name'], $pathlen); + $this->buffer[] = $file; + } + return true; + } + return false; + } + + public function dir_readdir() { + return (isset($this->buffer[$this->position])) ? $this->buffer[$this->position++]['name'] : false; + } + + public function dir_rewinddir() { + $this->position = 0; + } + + public function dir_closedir() { + $this->position = 0; + unset($this->buffer); + } + + public function stream_close() { + if ($this->mode == 'w') { + self::putObject($this->buffer, $this->url['host'], $this->url['path']); + } + $this->position = 0; + unset($this->buffer); + } + + public function stream_stat() { + if (is_object($this->buffer) && isset($this->buffer->headers)) + return array( + 'size' => $this->buffer->headers['size'], + 'mtime' => $this->buffer->headers['time'], + 'ctime' => $this->buffer->headers['time'] + ); + elseif (($info = self::getObjectInfo($this->url['host'], $this->url['path'])) !== false) + return array('size' => $info['size'], 'mtime' => $info['time'], 'ctime' => $info['time']); + return false; + } + + public function stream_flush() { + $this->position = 0; + return true; + } + + public function stream_open($path, $mode, $options, &$opened_path) { + if (!in_array($mode, array('r', 'rb', 'w', 'wb'))) return false; // Mode not supported + $this->mode = substr($mode, 0, 1); + self::__getURL($path); + $this->position = 0; + if ($this->mode == 'r') { + if (($this->buffer = self::getObject($this->url['host'], $this->url['path'])) !== false) { + if (is_object($this->buffer->body)) $this->buffer->body = (string)$this->buffer->body; + } else return false; + } + return true; + } + + public function stream_read($count) { + if ($this->mode !== 'r' && $this->buffer !== false) return false; + $data = substr(is_object($this->buffer) ? $this->buffer->body : $this->buffer, $this->position, $count); + $this->position += strlen($data); + return $data; + } + + public function stream_write($data) { + if ($this->mode !== 'w') return 0; + $left = substr($this->buffer, 0, $this->position); + $right = substr($this->buffer, $this->position + strlen($data)); + $this->buffer = $left . $data . $right; + $this->position += strlen($data); + return strlen($data); + } + + public function stream_tell() { + return $this->position; + } + + public function stream_eof() { + return $this->position >= strlen(is_object($this->buffer) ? $this->buffer->body : $this->buffer); + } + + public function stream_seek($offset, $whence) { + switch ($whence) { + case SEEK_SET: + if ($offset < strlen($this->buffer->body) && $offset >= 0) { + $this->position = $offset; + return true; + } else return false; + break; + case SEEK_CUR: + if ($offset >= 0) { + $this->position += $offset; + return true; + } else return false; + break; + case SEEK_END: + $bytes = strlen($this->buffer->body); + if ($bytes + $offset >= 0) { + $this->position = $bytes + $offset; + return true; + } else return false; + break; + default: return false; + } + } + + private function __getURL($path) { + $this->url = parse_url($path); + if (!isset($this->url['scheme']) || $this->url['scheme'] !== 's3') return $this->url; + if (isset($this->url['user'], $this->url['pass'])) self::setAuth($this->url['user'], $this->url['pass']); + $this->url['path'] = isset($this->url['path']) ? substr($this->url['path'], 1) : ''; + } + + private function __translateMode($mode) { + $acl = self::ACL_PRIVATE; + if (($mode & 0x0020) || ($mode & 0x0004)) + $acl = self::ACL_PUBLIC_READ; + // You probably don't want to enable public write access + if (($mode & 0x0010) || ($mode & 0x0008) || ($mode & 0x0002) || ($mode & 0x0001)) + $acl = self::ACL_PUBLIC_READ; //$acl = self::ACL_PUBLIC_READ_WRITE; + return $acl; + } +} stream_wrapper_register('s3', 'S3Wrapper'); + + +################################################################################ + + +S3::setAuth(awsAccessKey, awsSecretKey); + + +$bucketName = uniqid('s3test'); + +echo "Creating bucket: {$bucketName}\n"; +var_dump(mkdir("s3://{$bucketName}")); + +echo "\nWriting file: {$bucketName}/test.txt\n"; +var_dump(file_put_contents("s3://{$bucketName}/test.txt", "Eureka!")); + +echo "\nReading file: {$bucketName}/test.txt\n"; +var_dump(file_get_contents("s3://{$bucketName}/test.txt")); + +echo "\nContents for bucket: {$bucketName}\n"; +foreach (new DirectoryIterator("s3://{$bucketName}") as $b) { + echo "\t".$b."\n"; +} + +echo "\nUnlinking: {$bucketName}/test.txt\n"; +var_dump(unlink("s3://{$bucketName}/test.txt")); + +echo "\nRemoving bucket: {$bucketName}\n"; +var_dump(rmdir("s3://{$bucketName}")); + + +#EOF \ No newline at end of file diff --git a/example.php b/example.php new file mode 100755 index 00000000..d03303aa --- /dev/null +++ b/example.php @@ -0,0 +1,86 @@ +#!/usr/local/bin/php +listBuckets(), 1)."\n"; + + +// Create a bucket with public read access +if ($s3->putBucket($bucketName, S3::ACL_PUBLIC_READ)) { + echo "Created bucket {$bucketName}".PHP_EOL; + + // Put our file (also with public read access) + if ($s3->putObjectFile($uploadFile, $bucketName, baseName($uploadFile), S3::ACL_PUBLIC_READ)) { + echo "S3::putObjectFile(): File copied to {$bucketName}/".baseName($uploadFile).PHP_EOL; + + // Get object info + $info = $s3->getObjectInfo($bucketName, baseName($uploadFile)); + echo "S3::getObjecInfo(): Info for {$bucketName}/".baseName($uploadFile).': '.print_r($info, 1); + + // You can also fetch the object into memory: + // var_dump("S3::getObject() to memory", $s3->getObject($bucketName, baseName($uploadFile))); + + // Or save it into a file (write stream) + // var_dump("S3::getObject() to savefile.txt", $s3->getObject($bucketName, baseName($uploadFile), 'savefile.txt')); + + // Or write it to a resource (write stream) + // var_dump("S3::getObject() to resource", $s3->getObject($bucketName, baseName($uploadFile), fopen('savefile.txt', 'wb'))); + + + // Get the contents of our bucket + $contents = $s3->getBucket($bucketName); + echo "S3::getBucket(): Files in bucket {$bucketName}: ".print_r($contents, 1); + + // Delete our file + if ($s3->deleteObject($bucketName, baseName($uploadFile))) { + echo "S3::deleteObject(): Deleted file\n"; + + // Delete the bucket we created (a bucket has to be empty to be deleted) + if ($s3->deleteBucket($bucketName)) { + echo "S3::deleteBucket(): Deleted bucket {$bucketName}\n"; + } else { + echo "S3::deleteBucket(): Failed to delete bucket (it probably isn't empty)\n"; + } + + } else { + echo "S3::deleteObject(): Failed to delete file\n"; + } + } else { + echo "S3::putObjectFile(): Failed to copy file\n"; + } +} else { + echo "S3::putBucket(): Unable to create bucket (it may already exist and/or be owned by someone else)\n"; +}