├── .gitignore ├── .github └── FUNDING.yml ├── examples ├── .env.example ├── delete.php ├── read.php ├── write.php ├── list.php ├── read-stream.php ├── download.php ├── multi-upload.php ├── upload.php └── write-stream.php ├── LICENSE └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | /composer.lock 2 | /examples/.env 3 | /vendor/ 4 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: clue 2 | custom: https://clue.engineering/support 3 | -------------------------------------------------------------------------------- /examples/.env.example: -------------------------------------------------------------------------------- 1 | # copy this file to ".env" and adjust S3 credentials for the examples 2 | 3 | S3_KEY="XXXXXXXXXXXXXXXXXXXX" 4 | S3_SECRET="XXXXxxxXXXXxxXXXXXxXXXXXxxxXXXXXXXxxXXXXXXX" 5 | S3_BUCKET="foobar" 6 | S3_REGION="aac1" 7 | S3_ENDPOINT="https://{bucket}.{region}.example.com" 8 | -------------------------------------------------------------------------------- /examples/delete.php: -------------------------------------------------------------------------------- 1 | load(); 8 | 9 | $s3 = new Clue\React\S3\S3Client(getenv('S3_KEY'), getenv('S3_SECRET'), getenv('S3_BUCKET'), getenv('S3_REGION'), getenv('S3_ENDPOINT')); 10 | 11 | $s3->delete($argv[1] ?? 'demo.txt')->then(function () { 12 | echo 'deleted' . PHP_EOL; 13 | }, function (Exception $e) { 14 | echo 'Error: ' . $e->getMessage() . PHP_EOL; 15 | }); 16 | -------------------------------------------------------------------------------- /examples/read.php: -------------------------------------------------------------------------------- 1 | load(); 8 | 9 | $s3 = new Clue\React\S3\S3Client(getenv('S3_KEY'), getenv('S3_SECRET'), getenv('S3_BUCKET'), getenv('S3_REGION'), getenv('S3_ENDPOINT')); 10 | 11 | $s3->read($argv[1] ?? 'demo.txt')->then(function (string $contents) { 12 | echo $contents; 13 | }, function (Exception $e) { 14 | fwrite(STDERR, 'Error: ' . $e->getMessage() . PHP_EOL); 15 | }); 16 | -------------------------------------------------------------------------------- /examples/write.php: -------------------------------------------------------------------------------- 1 | load(); 8 | 9 | $s3 = new Clue\React\S3\S3Client(getenv('S3_KEY'), getenv('S3_SECRET'), getenv('S3_BUCKET'), getenv('S3_REGION'), getenv('S3_ENDPOINT')); 10 | 11 | $s3->write($argv[1] ?? 'demo.txt', $argv[2] ?? 'hello wörld')->then(function (int $bytes) { 12 | echo $bytes . ' bytes written' . PHP_EOL; 13 | }, function ($e) { 14 | echo $e->getMessage() . PHP_EOL; 15 | }); 16 | -------------------------------------------------------------------------------- /examples/list.php: -------------------------------------------------------------------------------- 1 | load(); 9 | 10 | $s3 = new Clue\React\S3\S3Client(getenv('S3_KEY'), getenv('S3_SECRET'), getenv('S3_BUCKET'), getenv('S3_REGION'), getenv('S3_ENDPOINT')); 11 | 12 | $s3->ls($argv[1] ?? '')->then(function (array $files) { 13 | foreach ($files as $file) { 14 | echo $file . PHP_EOL; 15 | } 16 | }, function (Exception $e) { 17 | echo 'Error: ' . $e->getMessage() . PHP_EOL; 18 | }); 19 | -------------------------------------------------------------------------------- /examples/read-stream.php: -------------------------------------------------------------------------------- 1 | load(); 8 | 9 | $s3 = new Clue\React\S3\S3Client(getenv('S3_KEY'), getenv('S3_SECRET'), getenv('S3_BUCKET'), getenv('S3_REGION'), getenv('S3_ENDPOINT')); 10 | 11 | $stream = $s3->readStream($argv[1] ?? 'demo.txt'); 12 | 13 | $stream->on('data', function ($chunk) { echo $chunk; }); 14 | $stream->on('error', function (Exception $e) { fwrite(STDERR, 'Error: ' . $e->getMessage() . PHP_EOL); }); 15 | $stream->on('close', function () { fwrite(STDERR, '[CLOSED]' . PHP_EOL); }); 16 | -------------------------------------------------------------------------------- /examples/download.php: -------------------------------------------------------------------------------- 1 | load(); 14 | 15 | $s3 = new Clue\React\S3\S3Client(getenv('S3_KEY'), getenv('S3_SECRET'), getenv('S3_BUCKET'), getenv('S3_REGION'), getenv('S3_ENDPOINT')); 16 | 17 | $stream = $s3->readStream($argv[1] ?? 'demo.txt'); 18 | 19 | $target = new React\Stream\WritableResourceStream(fopen($argv[2] ?? basename($argv[1] ?? 'demo.txt'), 'w')); 20 | $stream->pipe($target); 21 | 22 | $stream->on('error', function (Exception $e) { echo $e->getMessage() . PHP_EOL; }); 23 | $stream->on('close', function () { echo '[CLOSED input]' . PHP_EOL; }); 24 | 25 | $target->on('error', function (Exception $e) { echo $e->getMessage() . PHP_EOL; }); 26 | $target->on('close', function () { echo '[CLOSED output]' . PHP_EOL; }); 27 | -------------------------------------------------------------------------------- /examples/multi-upload.php: -------------------------------------------------------------------------------- 1 | load(); 6 | 7 | $browser = new React\Http\Browser(); 8 | $s3 = new Clue\React\S3\S3Client(getenv('S3_KEY'), getenv('S3_SECRET'), getenv('S3_BUCKET'), getenv('S3_REGION'), getenv('S3_ENDPOINT'), $browser); 9 | 10 | function loga($msg) { 11 | // prepend message with date/time with millisecond precision 12 | $time = microtime(true); 13 | echo date('Y-m-d H:i:s', (int)$time) . sprintf('.%03d ', ($time - (int)$time) * 1000) . $msg . PHP_EOL; 14 | } 15 | 16 | for ($i = 1; isset($argv[$i]); ++$i) { 17 | $url = $argv[$i]; 18 | loga('Downloading ' . $url); 19 | $browser->get($url)->then(function (Psr\Http\Message\ResponseInterface $response) use ($url, $s3) { 20 | loga('Downloaded ' . $url . ' (' . $response->getBody()->getSize() . ' bytes)'); 21 | $s3->write(basename($url), (string)$response->getBody())->then(function () use ($url) { 22 | loga('Uploaded ' . $url); 23 | }); 24 | }); 25 | } 26 | -------------------------------------------------------------------------------- /examples/upload.php: -------------------------------------------------------------------------------- 1 | load(); 14 | 15 | $s3 = new Clue\React\S3\S3Client(getenv('S3_KEY'), getenv('S3_SECRET'), getenv('S3_BUCKET'), getenv('S3_REGION'), getenv('S3_ENDPOINT')); 16 | 17 | $source = new React\Stream\ReadableResourceStream($resource = fopen($argv[1] ?? 'demo.txt', 'r')); 18 | 19 | $stream = $s3->writeStream($argv[2] ?? basename($argv[1] ?? 'demo.txt'), fstat($resource)['size']); 20 | $source->pipe($stream); 21 | 22 | $stream->on('error', function (Exception $e) { echo $e->getMessage() . PHP_EOL; }); 23 | $stream->on('close', function () { echo '[CLOSED output]' . PHP_EOL; }); 24 | 25 | $source->on('error', function (Exception $e) { echo $e->getMessage() . PHP_EOL; }); 26 | $source->on('close', function () { echo '[CLOSED input]' . PHP_EOL; }); 27 | -------------------------------------------------------------------------------- /examples/write-stream.php: -------------------------------------------------------------------------------- 1 | load(); 17 | 18 | $s3 = new Clue\React\S3\S3Client(getenv('S3_KEY'), getenv('S3_SECRET'), getenv('S3_BUCKET'), getenv('S3_REGION'), getenv('S3_ENDPOINT')); 19 | 20 | $stream = $s3->writeStream($argv[1] ?? 'demo.txt', $argv[2] ?? null); 21 | 22 | $stream->on('error', function (Exception $e) { echo fwrite(STDERR, 'Error: ' . $e->getMessage() . PHP_EOL); }); 23 | $stream->on('close', function () { fwrite(STDERR, '[CLOSED]' . PHP_EOL); }); 24 | 25 | $stdin = new React\Stream\ReadableResourceStream(STDIN); 26 | $stdin->pipe($stream); 27 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2020 Christian Lück 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 furnished 10 | to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | 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 | # clue/reactphp-s3 2 | 3 | Async S3 filesystem API (supporting Amazon S3, Ceph, MiniIO, DigitalOcean Spaces and others), 4 | built on top of [ReactPHP](https://reactphp.org/). 5 | 6 | **Table of contents** 7 | 8 | * [Support us](#support-us) 9 | * [Quickstart example](#quickstart-example) 10 | * [Usage](#usage) 11 | * [Methods](#methods) 12 | * [Promises](#promises) 13 | * [Cancellation](#cancellation) 14 | * [Timeouts](#timeouts) 15 | * [Blocking](#blocking) 16 | * [Streaming](#streaming) 17 | * [API](#api) 18 | * [S3Client](#s3client) 19 | * [ls()](#ls) 20 | * [read()](#read) 21 | * [readStream()](#readstream) 22 | * [write()](#write) 23 | * [writeStream()](#writestream) 24 | * [delete()](#delete) 25 | * [Install](#install) 26 | * [Tests](#tests) 27 | * [License](#license) 28 | 29 | ## Support us 30 | 31 | [![A clue·access project](https://raw.githubusercontent.com/clue-access/clue-access/main/clue-access.png)](https://github.com/clue-access/clue-access) 32 | 33 | *This project is currently under active development, 34 | you're looking at a temporary placeholder repository.* 35 | 36 | The code is available in early access to my sponsors here: https://github.com/clue-access/reactphp-s3 37 | 38 | Do you sponsor me on GitHub? Thank you for supporting sustainable open-source, you're awesome! ❤️ Have fun with the code! 🎉 39 | 40 | Seeing a 404 (Not Found)? Sounds like you're not in the early access group. Consider becoming a [sponsor on GitHub](https://github.com/sponsors/clue) for early access. Check out [clue·access](https://github.com/clue-access/clue-access) for more details. 41 | 42 | This way, more people get a chance to take a look at the code before the public release. 43 | 44 | ## Quickstart example 45 | 46 | Once [installed](#install), you can use the following code to read a file from 47 | your S3 file storage: 48 | 49 | ```php 50 | read('example.txt')->then(function (string $contents) { 57 | echo $contents; 58 | }, function (Exception $e) { 59 | echo 'Error: ' . $e->getMessage() . PHP_EOL; 60 | }); 61 | ``` 62 | 63 | See also the [examples](examples). 64 | 65 | ## Usage 66 | 67 | ### Methods 68 | 69 | Most importantly, this project provides a [`S3Client`](#s3client) object that offers 70 | several methods that resemble a filesystem-like API to access your files and 71 | directories: 72 | 73 | ```php 74 | $s3 = new Clue\React\S3\S3Client($key, $secret, $bucket, $region, $endpoint); 75 | 76 | $s3->ls($path); 77 | $s3->read($path); 78 | $s3->write($path, $contents); 79 | $s3->delete($path); 80 | ``` 81 | 82 | Each of the above methods supports async operation and either *fulfills* with 83 | its result or *rejects* with an `Exception`. 84 | Please see the following chapter about [promises](#promises) for more details. 85 | 86 | ### Promises 87 | 88 | Sending requests is async (non-blocking), so you can actually send multiple requests in parallel. 89 | S3 will respond to each request with a response message, the order is not guaranteed. 90 | Sending requests uses a [Promise](https://github.com/reactphp/promise)-based interface 91 | that makes it easy to react to when a request is completed (i.e. either successfully fulfilled or rejected with an error). 92 | 93 | ```php 94 | $s3->read($path)->then( 95 | function (string $contents) { 96 | // file contents received 97 | }, 98 | function (Exception $e) { 99 | // an error occurred while executing the request 100 | } 101 | }); 102 | ``` 103 | 104 | If this looks strange to you, you can also use the more traditional [blocking API](#blocking). 105 | 106 | ### Cancellation 107 | 108 | The returned Promise is implemented in such a way that it can be cancelled 109 | when it is still pending. 110 | Cancelling a pending promise will reject its value with an Exception and 111 | clean up any underlying resources. 112 | 113 | ```php 114 | $promise = $s3->read('example.txt'); 115 | 116 | Loop::addTimer(2.0, function () use ($promise) { 117 | $promise->cancel(); 118 | }); 119 | ``` 120 | 121 | ### Timeouts 122 | 123 | This library uses a very efficient HTTP implementation, so most S3 requests 124 | should usually be completed in mere milliseconds. However, when sending S3 125 | requests over an unreliable network (the internet), there are a number of things 126 | that can go wrong and may cause the request to fail after a time. As such, 127 | timeouts are handled by the underlying HTTP library and this library respects 128 | PHP's `default_socket_timeout` setting (default 60s) as a timeout for sending the 129 | outgoing S3 request and waiting for a successful response and will otherwise 130 | cancel the pending request and reject its value with an `Exception`. 131 | 132 | Note that this timeout value covers creating the underlying transport connection, 133 | sending the request, waiting for the remote service to process the request 134 | and receiving the full response. To use a custom timeout value, you can 135 | pass the timeout to the [underlying `Browser`](https://github.com/reactphp/http#timeouts) 136 | like this: 137 | 138 | ```php 139 | $browser = new React\Http\Browser(); 140 | $browser = $browser->withTimeout(10.0); 141 | 142 | $s3 = new Clue\React\S3\S3Client($key, $secret, $bucket, $region, $endpoint, $browser); 143 | 144 | $s3->read('example.txt')->then(function (string $contents) { 145 | // contents received within 10 seconds maximum 146 | var_dump($contents); 147 | }); 148 | ``` 149 | 150 | Similarly, you can use a negative timeout value to not apply a timeout at all 151 | or use a `null` value to restore the default handling. Note that the underlying 152 | connection may still impose a different timeout value. See also the underlying 153 | [timeouts documentation](https://github.com/reactphp/http#timeouts) for more details. 154 | 155 | ### Blocking 156 | 157 | As stated above, this library provides you a powerful, async API by default. 158 | 159 | If, however, you want to integrate this into your traditional, blocking environment, 160 | you should look into also using [clue/reactphp-block](https://github.com/clue/reactphp-block). 161 | 162 | The resulting blocking code could look something like this: 163 | 164 | ```php 165 | use Clue\React\Block; 166 | 167 | $s3 = new Clue\React\S3\S3Client($key, $secret, $bucket, $region, $endpoint); 168 | 169 | $promise = $s3->read($path); 170 | 171 | try { 172 | $contents = Block\await($promise, Loop::get()); 173 | // file contents received 174 | } catch (Exception $e) { 175 | // an error occurred while executing the request 176 | } 177 | ``` 178 | 179 | Similarly, you can also process multiple requests concurrently and await an array of results: 180 | 181 | ```php 182 | $promises = [ 183 | $s3->read('example.txt'), 184 | $s3->read('folder/demo.txt') 185 | ]; 186 | 187 | $results = Block\awaitAll($promises, Loop::get()); 188 | ``` 189 | 190 | Please refer to [clue/reactphp-block](https://github.com/clue/reactphp-block#readme) for more details. 191 | 192 | ### Streaming 193 | 194 | The following API endpoints expose the file contents as a string: 195 | 196 | ```php 197 | $s3->read($path); 198 | $s3->write($path, $contents); 199 | ```` 200 | 201 | Keep in mind that this means the whole string has to be kept in memory. 202 | This is easy to get started and works reasonably well for smaller files. 203 | 204 | For bigger files it's usually a better idea to use a streaming approach, 205 | where only small chunks have to be kept in memory. 206 | This works for (any number of) files of arbitrary sizes. 207 | 208 | The [`S3Client::readStream()`](#readstream) method complements the default 209 | Promise-based [`S3Client::read()`](#read) API and returns an instance implementing 210 | [`ReadableStreamInterface`](https://github.com/reactphp/stream#readablestreaminterface) instead: 211 | 212 | ```php 213 | $stream = $s3->readStream($path); 214 | 215 | $stream->on('data', function (string $chunk) { 216 | echo $chunk; 217 | }); 218 | 219 | $stream->on('error', function (Exception $error) { 220 | echo 'Error: ' . $error->getMessage() . PHP_EOL; 221 | }); 222 | 223 | $stream->on('close', function () { 224 | echo '[DONE]' . PHP_EOL; 225 | }); 226 | ``` 227 | 228 | The [`S3Client::writeStream()`](#writestream) method complements the default 229 | Promise-based [`S3Client::write()`](#write) API and returns an instance implementing 230 | [`WritableStreamInterface`](https://github.com/reactphp/stream#writablestreaminterface) instead: 231 | 232 | ```php 233 | $stream = $s3->writeStream('folder/image.jpg', 10); 234 | 235 | $stream->write('hello'); 236 | $stream->end('world'); 237 | 238 | $stream->on('error', function (Exception $error) { 239 | echo 'Error: ' . $error->getMessage() . PHP_EOL; 240 | }); 241 | 242 | $stream->on('close', function () { 243 | echo '[CLOSED]' . PHP_EOL; 244 | }); 245 | ``` 246 | 247 | ## API 248 | 249 | ### S3Client 250 | 251 | The `S3Client` class is responsible for communication with your S3 file storage 252 | and assembling and sending HTTP requests. It requires your S3 credentials in order to 253 | authenticate your requests: 254 | 255 | ```php 256 | $s3 = new Clue\React\S3\S3Client($key, $secret, $bucket, $region, $endpoint); 257 | ``` 258 | 259 | This class takes an optional `Browser|null $browser` parameter that can be used to 260 | pass the browser instance to use for this object. 261 | If you need custom connector settings (DNS resolution, TLS parameters, timeouts, 262 | proxy servers etc.), you can explicitly pass a custom instance of the 263 | [`ConnectorInterface`](https://github.com/reactphp/socket#connectorinterface) 264 | to the [`Browser`](https://github.com/reactphp/http#browser) instance 265 | and pass it as an additional argument to the `S3Client` like this: 266 | 267 | ```php 268 | $connector = new React\Socket\Connector([ 269 | 'dns' => '127.0.0.1', 270 | 'tcp' => [ 271 | 'bindto' => '192.168.10.1:0' 272 | ], 273 | 'tls' => [ 274 | 'verify_peer' => false, 275 | 'verify_peer_name' => false 276 | ] 277 | ]); 278 | 279 | $browser = new React\Http\Browser($connector); 280 | $s3 = new Clue\React\S3\S3Client($key, $secret, $bucket, $region, $endpoint, $browser); 281 | ``` 282 | 283 | #### ls() 284 | 285 | The `ls(string $path): PromiseInterface` method can be used to 286 | list all objects (files and directories) in the given directory `$path`. 287 | 288 | ```php 289 | $s3->ls('folder/')->then(function (array $files) { 290 | foreach ($files as $file) { 291 | echo $file . PHP_EOL; 292 | } 293 | }); 294 | ``` 295 | 296 | Similarly, you can use an empty `$path` to list all objects in the root 297 | path (the bucket itself). 298 | 299 | Note that S3 doesn't have a concept of "files" and "directories", but 300 | this API aims to work more filesystem-like. All objects sharing a common 301 | prefix delimited by a slash (e.g. "folder/") are considered a "directory" 302 | entry. This method will only report objects directly under `$path` prefix 303 | and will not recurse into deeper directory paths. All file objects will 304 | always be returned as the file name component relative to the given 305 | `$path`, all directory objects will always be returned with a trailing 306 | slash (e.g. "folder/"). 307 | 308 | #### read() 309 | 310 | The `read(string $path): PromiseInterface` method can be used to 311 | read (download) the given object located at `$path`. 312 | 313 | ```php 314 | $s3->read('folder/image.jpg')->then(function (string $contents) { 315 | echo 'file is ' . strlen($contents) . ' bytes' . PHP_EOL; 316 | }); 317 | ``` 318 | 319 | Keep in mind that due to resolving with the file contents as a `string` 320 | variable, this API has to keep the complete file contents in memory when 321 | the Promise resolves. This is easy to get started and works reasonably 322 | well for smaller files. If you're dealing with bigger files (or files 323 | with unknown sizes), it's usually a better idea to use a streaming 324 | approach, where only small chunks have to kept in memory. See also 325 | `readStream()` for more details. 326 | 327 | #### readStream() 328 | 329 | The `readStream(string $path): ReadableStreamInterface` method can be used to 330 | read (download) the given object located at `$path` as a readable stream. 331 | 332 | ```php 333 | $stream = $s3->readStream('folder/image.jpg'); 334 | 335 | $stream->on('data', function (string $chunk) { 336 | echo $chunk; 337 | }); 338 | 339 | $stream->on('error', function (Exception $error) { 340 | echo 'Error: ' . $error->getMessage() . PHP_EOL; 341 | }); 342 | 343 | $stream->on('close', function () { 344 | echo '[CLOSED]' . PHP_EOL; 345 | }); 346 | ``` 347 | 348 | This works for files of arbitrary sizes as only small chunks have to 349 | be kept in memory. The resulting stream is a well-behaving readable stream 350 | that will emit the normal stream events, see also 351 | [`ReadableStreamInterface`](https://github.com/reactphp/stream#readablestreaminterface). 352 | 353 | #### write() 354 | 355 | The `write(string $path, string $contents): PromiseInterface` method can be used to 356 | write (upload) the given `$contents` to the object located at `$path`. 357 | 358 | ```php 359 | $s3->write('folder/image.jpg', $contents)->then(function (int $bytes) { 360 | echo $bytes . ' bytes written' . PHP_EOL; 361 | }); 362 | ``` 363 | 364 | Keep in mind that due to accepting the file contents as a `string` 365 | variable, this API has to keep the complete file contents in memory when 366 | starting the upload. This is easy to get started and works reasonably 367 | well for smaller files. If you're dealing with bigger files (or files 368 | with unknown sizes), it's usually a better idea to use a streaming 369 | approach, where only small chunks have to kept in memory. See also 370 | `writeStream()` for more details. 371 | 372 | Note that S3 will always overwrite anything that already exists under the 373 | given `$path`. If this information is useful to you, you may want to 374 | check `ls()` before writing. 375 | 376 | #### writeStream() 377 | 378 | The `writeStream(string $path, int $contentLength): WritableStreamInterface` method can be used to 379 | write (upload) contents to the object located at `$path` as a writable stream. 380 | 381 | ```php 382 | $stream = $s3->writeStream('folder/image.jpg', 10); 383 | 384 | $stream->write('hello'); 385 | $stream->end('world'); 386 | 387 | $stream->on('error', function (Exception $error) { 388 | echo 'Error: ' . $error->getMessage() . PHP_EOL; 389 | }); 390 | 391 | $stream->on('close', function () { 392 | echo '[CLOSED]' . PHP_EOL; 393 | }); 394 | ``` 395 | 396 | This works for files of arbitrary sizes as only small chunks have to 397 | be kept in memory. The resulting stream is a well-behaving writable stream 398 | that will emit the normal stream events, see also 399 | [`WritableStreamInterface`](https://github.com/reactphp/stream#writablestreaminterface). 400 | 401 | Note that S3 requires a `$contentLength` argument to be known upfront and 402 | match the complete stream contents size in total number of bytes that 403 | will be written to this stream. 404 | 405 | Note that S3 will always overwrite anything that already exists under the 406 | given `$path`. If this information is useful to you, you may want to 407 | check `ls()` before writing. 408 | 409 | #### delete() 410 | 411 | The `delete(string $path): PromiseInterface` method can be used to 412 | delete the given object located at `$path`. 413 | 414 | ```php 415 | $s3->delete('folder/image.jpg')->then(function () { 416 | echo 'deleted' . PHP_EOL; 417 | }); 418 | ``` 419 | 420 | This method will resolve (with no value) when deleting completes, whether 421 | the given `$path` existed or not. If this information is useful to you, 422 | you may want to check `ls()` before deleting. 423 | 424 | Note that S3 doesn't have a concept of "directories", so this method will 425 | not recurse into deeper path components. If you want to delete multiple 426 | objects with the same prefix (e.g. "folder/"), you may want to use `ls()` 427 | and individually delete all objects. 428 | 429 | ## Install 430 | 431 | The recommended way to install this library is [through Composer](https://getcomposer.org/). 432 | [New to Composer?](https://getcomposer.org/doc/00-intro.md) 433 | 434 | This project does not yet follow [SemVer](https://semver.org/). 435 | This will install the latest supported version: 436 | 437 | While in [early access](#support-us), you first have to manually change your 438 | `composer.json` to include these lines to access the supporters-only repository: 439 | 440 | ```json 441 | { 442 | "repositories": [ 443 | { 444 | "type": "vcs", 445 | "url": "https://github.com/clue-access/reactphp-s3" 446 | } 447 | ] 448 | } 449 | ``` 450 | 451 | Then install this package as usual: 452 | 453 | ```bash 454 | $ composer require clue/reactphp-s3:dev-main 455 | ``` 456 | 457 | This project aims to run on any platform and thus does not require any PHP 458 | extensions and supports running on PHP 7.0 through current PHP 8+. 459 | 460 | ## Tests 461 | 462 | To run the test suite, you first need to clone this repo and then install all 463 | dependencies [through Composer](https://getcomposer.org/): 464 | 465 | ```bash 466 | $ composer install 467 | ``` 468 | 469 | To run the test suite, go to the project root and run: 470 | 471 | ```bash 472 | $ vendor/bin/phpunit 473 | ``` 474 | 475 | ## License 476 | 477 | This project is released under the permissive [MIT license](LICENSE). 478 | 479 | > Did you know that I offer custom development services and issuing invoices for 480 | sponsorships of releases and for contributions? Contact me (@clue) for details. 481 | --------------------------------------------------------------------------------