├── .gitignore ├── .travis.yml ├── composer.json ├── LICENSE ├── README.md └── src └── S3.php /.gitignore: -------------------------------------------------------------------------------- 1 | /vendor/ 2 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: php 2 | php: 3 | - 5.4 4 | - 5.5 5 | install: composer install 6 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ericnorris/amazon-s3-php", 3 | "description": "A lightweight and fast S3 client for PHP.", 4 | "keywords": ["amazon", "aws", "s3"], 5 | "license": "MIT", 6 | "authors": [ 7 | { 8 | "name": "Eric Norris", 9 | "email": "erictnorris@gmail.com" 10 | } 11 | ], 12 | "support": { 13 | "source": "https://github.com/ericnorris/amazon-s3-php", 14 | "issues": "https://github.com/ericnorris/amazon-s3-php/issues" 15 | }, 16 | "autoload": { 17 | "files": ["src/S3.php"] 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Eric Norris 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | amazon-s3-php 2 | ============= 3 | 4 | Inspired by [tpyo/amazon-s3-php-class](https://github.com/tpyo/amazon-s3-php-class), this is a simple and configurable S3 PHP library. It was written to be as lightweight as possible, while still enabling access to all of the features of AWS (e.g. server-side encryption). 5 | 6 | Additionally, `curl_multi_exec` is used (rather than `curl_exec`) for better performance when doing bulk operations. 7 | 8 | ## Usage 9 | `$client = new S3($access_key, $secret_key [, $endpoint = null]);` 10 | 11 | ## Configuration 12 | ### Specify Custom Curl Options 13 | * `$client->useCurlOpts($curl_opts_array)` 14 | 15 | Provides the S3 class with any curl options to use in making requests. 16 | 17 | The following options are passed by default in order to prevent 'hung' requests: 18 | ```php 19 | curl_opts = array( 20 | CURLOPT_CONNECTTIMEOUT => 30, 21 | CURLOPT_LOW_SPEED_LIMIT => 1, 22 | CURLOPT_LOW_SPEED_TIME => 30 23 | ); 24 | ``` 25 | **Note:** *If you call this method, these defaults will not be used.* 26 | 27 | ### Send Additional AWS Headers 28 | All of the available S3 operations take an optional `$headers` array that will be passed along to S3. These can include `x-amz-meta-`, `x-amz-server-side-encryption`, `Content-Type`, etc. Any Amazon headers specified will be properly included in the AWS signature as per [AWS Signature v2](http://docs.aws.amazon.com/AmazonS3/latest/dev/RESTAuthentication.html). 29 | 30 | Request headers that are common to all requests are located [here](http://docs.aws.amazon.com/AmazonS3/latest/API/RESTCommonRequestHeaders.html). 31 | 32 | ## S3Response Class 33 | All methods in the S3 class will return an instance of the S3Response class. 34 | ```php 35 | class S3Response { 36 | public $error; // null if no error 37 | public $code; // response code from AWS 38 | public $headers; // response headers from AWS 39 | public $body; // response body from AWS 40 | } 41 | ``` 42 | 43 | If there is an error in curl or an error is returned from AWS, `$response->error` will be non-null and set to the following array: 44 | 45 | ``` 46 | $error = array( 47 | 'code' => xxx, // error code from either curl or AWS 48 | 'message' => xxx, // error string from either curl or AWS 49 | 'resource' => [optional] // the S3 resource frmo the request 50 | ) 51 | ``` 52 | 53 | ## Methods 54 | `putObject($bucket, $path, $file [, $headers = array()])` 55 | * Uploads a file to the specified path and bucket. `$file` can either be the raw representation of a file (e.g. the result of `file_get_contents()`) or a valid stream resource. 56 | * [AWS Documentation](http://docs.aws.amazon.com/AmazonS3/latest/API/RESTObjectPUT.html) 57 | 58 | `getObjectInfo($bucket, $path, [, $headers = array()])` 59 | * Retrieves metadata for the object. 60 | * [AWS Documentation](http://docs.aws.amazon.com/AmazonS3/latest/API/RESTObjectHEAD.html) 61 | 62 | `getObject($bucket, $path [, $resource = null [, $headers = array()]])` 63 | * Retrieves the contents of an object. If `$resource` is a valid stream resource, the contents will be written to the stream. Otherwise `$response->body` will contain the contents of the file. 64 | * [AWS Documentation](http://docs.aws.amazon.com/AmazonS3/latest/API/RESTObjectGET.html) 65 | 66 | `deleteObject($bucket, $path [, $headers = array()])` 67 | * Deletes an object from S3. 68 | * [AWS Documentation](http://docs.aws.amazon.com/AmazonS3/latest/API/RESTObjectDELETE.html) 69 | 70 | `getBucket($bucket [, $headers = array()])` 71 | * Returns a parsed response from S3 listing the contents of the specified bucket. 72 | * [AWS Documentation](http://docs.aws.amazon.com/AmazonS3/latest/API/RESTBucketGET.html) 73 | 74 | ## Examples 75 | Instantiating the S3 class: 76 | ```php 77 | $client = new S3(ACCESS_KEY, SECRET_KEY); 78 | 79 | // [OPTIONAL] Specify different curl options 80 | $client->useCurlOpts(array( 81 | CURLOPT_MAX_RECV_SPEED_LARGE => 1048576, 82 | CURLOPT_CONNECTTIMEOUT => 10 83 | )); 84 | ``` 85 | 86 | ### Upload an object 87 | ``` 88 | $response = $client->putObject( 89 | 'bucket', 90 | 'hello_world.txt', 91 | 'hello world!', 92 | array( 93 | 'Content-Type' => 'text/plain' 94 | ) 95 | ); 96 | 97 | print_r($response); 98 | ``` 99 | 100 | Output: 101 | ``` 102 | S3Response Object 103 | ( 104 | [error] => null 105 | [code] => 200 106 | [headers] => Array 107 | ( 108 | [x-amz-id-2] => ... 109 | [x-amz-request-id] => ... 110 | [ETag] => "..." 111 | [Content-Length] => ... 112 | [Server] => ... 113 | ) 114 | [body] => null 115 | ) 116 | ``` 117 | ### Download an object 118 | ```php 119 | $resource = tmpfile(); 120 | $client->getObject('bucket', 'hello_world.txt', $resource); 121 | 122 | print_r($response); 123 | echo stream_get_contents($resource) . "\n"; 124 | ``` 125 | Output: 126 | ``` 127 | S3Response Object 128 | ( 129 | [error] => 130 | [code] => 200 131 | [headers] => Array 132 | ( 133 | [x-amz-id-2] => ... 134 | [x-amz-request-id] => ... 135 | [ETag] => "..." 136 | [Accept-Ranges] => bytes 137 | [Content-Type] => text/plain 138 | [Content-Length] => 12 139 | [Server] => ... 140 | ) 141 | 142 | [body] => Resource id #17 143 | ) 144 | 145 | hello world! 146 | ``` 147 | -------------------------------------------------------------------------------- /src/S3.php: -------------------------------------------------------------------------------- 1 | access_key = $access_key; 18 | $this->secret_key = $secret_key; 19 | $this->endpoint = $endpoint ?: self::DEFAULT_ENDPOINT; 20 | 21 | $this->multi_curl = curl_multi_init(); 22 | 23 | $this->curl_opts = array( 24 | CURLOPT_CONNECTTIMEOUT => 30, 25 | CURLOPT_LOW_SPEED_LIMIT => 1, 26 | CURLOPT_LOW_SPEED_TIME => 30 27 | ); 28 | } 29 | 30 | public function __destruct() { 31 | curl_multi_close($this->multi_curl); 32 | } 33 | 34 | public function useCurlOpts($curl_opts) { 35 | $this->curl_opts = $curl_opts; 36 | return $this; 37 | } 38 | 39 | public function putObject($bucket, $path, $file, $headers = array()) { 40 | $uri = "$bucket/$path"; 41 | 42 | $request = (new S3Request('PUT', $this->endpoint, $uri)) 43 | ->setFileContents($file) 44 | ->setHeaders($headers) 45 | ->useMultiCurl($this->multi_curl) 46 | ->useCurlOpts($this->curl_opts) 47 | ->sign($this->access_key, $this->secret_key); 48 | 49 | return $request->getResponse(); 50 | } 51 | 52 | public function getObjectInfo($bucket, $path, $headers = array()) { 53 | $uri = "$bucket/$path"; 54 | 55 | $request = (new S3Request('HEAD', $this->endpoint, $uri)) 56 | ->setHeaders($headers) 57 | ->useMultiCurl($this->multi_curl) 58 | ->useCurlOpts($this->curl_opts) 59 | ->sign($this->access_key, $this->secret_key); 60 | 61 | return $request->getResponse(); 62 | } 63 | 64 | public function getObject($bucket, $path, $resource = null, 65 | $headers = array()) { 66 | $uri = "$bucket/$path"; 67 | 68 | $request = (new S3Request('GET', $this->endpoint, $uri)) 69 | ->setHeaders($headers) 70 | ->useMultiCurl($this->multi_curl) 71 | ->useCurlOpts($this->curl_opts) 72 | ->sign($this->access_key, $this->secret_key); 73 | 74 | if (is_resource($resource)) { 75 | $request->saveToResource($resource); 76 | } 77 | 78 | return $request->getResponse(); 79 | } 80 | 81 | public function deleteObject($bucket, $path, $headers = array()) { 82 | $uri = "$bucket/$path"; 83 | 84 | $request = (new S3Request('DELETE', $this->endpoint, $uri)) 85 | ->setHeaders($headers) 86 | ->useMultiCurl($this->multi_curl) 87 | ->useCurlOpts($this->curl_opts) 88 | ->sign($this->access_key, $this->secret_key); 89 | 90 | return $request->getResponse(); 91 | } 92 | 93 | public function getBucket($bucket, $headers = array()) { 94 | $request = (new S3Request('GET', $this->endpoint, $bucket)) 95 | ->setHeaders($headers) 96 | ->useMultiCurl($this->multi_curl) 97 | ->useCurlOpts($this->curl_opts) 98 | ->sign($this->access_key, $this->secret_key); 99 | 100 | $response = $request->getResponse(); 101 | 102 | if (!isset($response->error)) { 103 | $body = simplexml_load_string($response->body); 104 | 105 | if ($body) { 106 | $response->body = $body; 107 | } 108 | } 109 | 110 | return $response; 111 | } 112 | 113 | } 114 | 115 | class S3Request { 116 | 117 | private $action; 118 | private $endpoint; 119 | private $uri; 120 | private $headers; 121 | private $curl; 122 | private $response; 123 | 124 | private $multi_curl; 125 | 126 | public function __construct($action, $endpoint, $uri) { 127 | $this->action = $action; 128 | $this->endpoint = $endpoint; 129 | $this->uri = $uri; 130 | 131 | $this->headers = array( 132 | 'Content-MD5' => '', 133 | 'Content-Type' => '', 134 | 'Date' => gmdate('D, d M Y H:i:s T'), 135 | 'Host' => $this->endpoint 136 | ); 137 | 138 | $this->curl = curl_init(); 139 | $this->response = new S3Response(); 140 | 141 | $this->multi_curl = null; 142 | } 143 | 144 | public function saveToResource($resource) { 145 | $this->response->saveToResource($resource); 146 | } 147 | 148 | public function setFileContents($file) { 149 | if (is_resource($file)) { 150 | $hash_ctx = hash_init('md5'); 151 | $length = hash_update_stream($hash_ctx, $file); 152 | $md5 = hash_final($hash_ctx, true); 153 | 154 | rewind($file); 155 | 156 | curl_setopt($this->curl, CURLOPT_PUT, true); 157 | curl_setopt($this->curl, CURLOPT_INFILE, $file); 158 | curl_setopt($this->curl, CURLOPT_INFILESIZE, $length); 159 | } else { 160 | curl_setopt($this->curl, CURLOPT_POSTFIELDS, $file); 161 | $md5 = md5($file, true); 162 | } 163 | 164 | $this->headers['Content-MD5'] = base64_encode($md5); 165 | 166 | return $this; 167 | } 168 | 169 | public function setHeaders($custom_headers) { 170 | $this->headers = array_merge($this->headers, $custom_headers); 171 | return $this; 172 | } 173 | 174 | public function sign($access_key, $secret_key) { 175 | $canonical_amz_headers = $this->getCanonicalAmzHeaders(); 176 | 177 | $string_to_sign = ''; 178 | $string_to_sign .= "{$this->action}\n"; 179 | $string_to_sign .= "{$this->headers['Content-MD5']}\n"; 180 | $string_to_sign .= "{$this->headers['Content-Type']}\n"; 181 | $string_to_sign .= "{$this->headers['Date']}\n"; 182 | 183 | if (!empty($canonical_amz_headers)) { 184 | $string_to_sign .= implode($canonical_amz_headers, "\n") . "\n"; 185 | } 186 | 187 | $string_to_sign .= "/{$this->uri}"; 188 | 189 | $signature = base64_encode( 190 | hash_hmac('sha1', $string_to_sign, $secret_key, true) 191 | ); 192 | 193 | $this->headers['Authorization'] = "AWS $access_key:$signature"; 194 | 195 | return $this; 196 | } 197 | 198 | public function useMultiCurl($mh) { 199 | $this->multi_curl = $mh; 200 | return $this; 201 | } 202 | 203 | public function useCurlOpts($curl_opts) { 204 | curl_setopt_array($this->curl, $curl_opts); 205 | 206 | return $this; 207 | } 208 | 209 | public function getResponse() { 210 | $http_headers = array_map( 211 | function($header, $value) { 212 | return "$header: $value"; 213 | }, 214 | array_keys($this->headers), 215 | array_values($this->headers) 216 | ); 217 | 218 | curl_setopt_array($this->curl, array( 219 | CURLOPT_USERAGENT => 'ericnorris/amazon-s3-php', 220 | CURLOPT_URL => "https://{$this->endpoint}/{$this->uri}", 221 | CURLOPT_HTTPHEADER => $http_headers, 222 | CURLOPT_HEADER => false, 223 | CURLOPT_RETURNTRANSFER => false, 224 | CURLOPT_FOLLOWLOCATION => true, 225 | CURLOPT_WRITEFUNCTION => array( 226 | $this->response, '__curlWriteFunction' 227 | ), 228 | CURLOPT_HEADERFUNCTION => array( 229 | $this->response, '__curlHeaderFunction' 230 | ) 231 | )); 232 | 233 | switch ($this->action) { 234 | case 'DELETE': 235 | curl_setopt($this->curl, CURLOPT_CUSTOMREQUEST, 'DELETE'); 236 | break; 237 | case 'HEAD': 238 | curl_setopt($this->curl, CURLOPT_NOBODY, true); 239 | break; 240 | case 'POST': 241 | curl_setopt($this->curl, CURLOPT_CUSTOMREQUEST, 'POST'); 242 | break; 243 | case 'PUT': 244 | curl_setopt($this->curl, CURLOPT_CUSTOMREQUEST, 'PUT'); 245 | break; 246 | } 247 | 248 | if (isset($this->multi_curl)) { 249 | curl_multi_add_handle($this->multi_curl, $this->curl); 250 | 251 | $running = null; 252 | do { 253 | curl_multi_exec($this->multi_curl, $running); 254 | curl_multi_select($this->multi_curl); 255 | } while ($running > 0); 256 | 257 | curl_multi_remove_handle($this->multi_curl, $this->curl); 258 | } else { 259 | $success = curl_exec($this->curl); 260 | } 261 | 262 | $this->response->finalize($this->curl); 263 | 264 | curl_close($this->curl); 265 | 266 | return $this->response; 267 | } 268 | 269 | private function getCanonicalAmzHeaders() { 270 | $canonical_amz_headers = array(); 271 | 272 | foreach ($this->headers as $header => $value) { 273 | $header = trim(strtolower($header)); 274 | $value = trim($value); 275 | 276 | if (strpos($header, 'x-amz-') === 0) { 277 | $canonical_amz_headers[$header] = "$header:$value"; 278 | } 279 | } 280 | 281 | ksort($canonical_amz_headers); 282 | 283 | return $canonical_amz_headers; 284 | } 285 | 286 | } 287 | 288 | class S3Response { 289 | 290 | public $error; 291 | public $code; 292 | public $headers; 293 | public $body; 294 | 295 | public function __construct() { 296 | $this->error = null; 297 | $this->code = null; 298 | $this->headers = array(); 299 | $this->body = null; 300 | } 301 | 302 | public function saveToResource($resource) { 303 | $this->body = $resource; 304 | } 305 | 306 | public function __curlWriteFunction($ch, $data) { 307 | if (is_resource($this->body)) { 308 | return fwrite($this->body, $data); 309 | } else { 310 | $this->body .= $data; 311 | return strlen($data); 312 | } 313 | } 314 | 315 | public function __curlHeaderFunction($ch, $data) { 316 | $header = explode(':', $data); 317 | 318 | if (count($header) == 2) { 319 | list($key, $value) = $header; 320 | $this->headers[$key] = trim($value); 321 | } 322 | 323 | return strlen($data); 324 | } 325 | 326 | public function finalize($ch) { 327 | if (is_resource($this->body)) { 328 | rewind($this->body); 329 | } 330 | 331 | if (curl_errno($ch) || curl_error($ch)) { 332 | $this->error = array( 333 | 'code' => curl_errno($ch), 334 | 'message' => curl_error($ch), 335 | ); 336 | } else { 337 | $this->code = curl_getinfo($ch, CURLINFO_HTTP_CODE); 338 | $content_type = curl_getinfo($ch, CURLINFO_CONTENT_TYPE); 339 | 340 | if ($this->code > 300 && $content_type == 'application/xml') { 341 | if (is_resource($this->body)) { 342 | $response = simplexml_load_string( 343 | stream_get_contents($this->body) 344 | ); 345 | 346 | rewind($this->body); 347 | } else { 348 | $response = simplexml_load_string($this->body); 349 | } 350 | 351 | if ($response) { 352 | $error = array( 353 | 'code' => (string)$response->Code, 354 | 'message' => (string)$response->Message, 355 | ); 356 | 357 | if (isset($response->Resource)) { 358 | $error['resource'] = (string)$response->Resource; 359 | } 360 | 361 | $this->error = $error; 362 | } 363 | } 364 | } 365 | } 366 | } 367 | --------------------------------------------------------------------------------