ajck's avatar
Level 1

Upload multiple files to AWS S3 from Laravel?

I'm currently using the standard Laravel file storage to upload a single file to S3 but now have a need to upload a whole load of separate files at once (possibly up to 200). I want to do this as efficiently as possible, rather than using separate requests. Is this possible somehow, either using Laravel storage, the Flysystem library it's based on or the official Amazon S3 PHP SDK? Separate files are very small, about 1Kb each.

Thanks!

0 likes
5 replies
Mick79's avatar

I struggled with this for a long time. I finally caved in and now use a service - filestack.

Honestly... it costs me $19 a month but I would pay more. They make uploading and asset storage a dream.

PS. I absolutely do not work or am affiliated in anyway with that company. I just struggled for so long with trying to create a nice file upload experience and they have changed my life (and my app) hugely for the better.

MarkLL's avatar
MarkLL
Best Answer
Level 7

@ajck I wrote an Artisan command to upload a directory of files to S3. It involves using MultipartUploader() and Promises. It handled 25 concurrent uploads and several of these were > 1GB. I was very impressed in the end. roughly it went like this...

$s3Client = AWS::createClient('s3');
$bucket = config('aws.bucket');
$promises = [];
foreach( $this->fetchFilenamesToUpload() as $keyToFile => $pathToFile) {
    $uploader = new MultipartUploader($s3Client, $pathToFile, [
                'bucket'          => $bucket,
                'key'             => $keyToFile,
                'concurrency'     => 25,
                'before_complete' => function (AWSCommand $command) use ($keyToFile) {echo 'before_complete... (' .$keyToFile . ')'. PHP_EOL; },
                'before_initiate' => function (AWSCommand $command) use ($keyToFile) {echo 'before_initiate... (' .$keyToFile . ')'. PHP_EOL; },
                'before_upload'   => function (AWSCommand $command) use ($keyToFile) {echo 'before_upload...   (' .$keyToFile . ')' . PHP_EOL; }
    ]);
    $promises[] = $uploader->promise();
}
$aggregate = \GuzzleHttp\Promise\all($promises);
$result = $aggregate->wait();

You don't really need all the output I added, as this was a dev tool, I just wanted to see what was happening.

2 likes
ajck's avatar
Level 1

@markll Thanks very much. Looks like what I need :) I've adapted the code slightly, listed below. Does it look right do you think? I presume your line

foreach($this->fetchFilenamesToUpload() as $keyToFile => $pathToFile) {

is just iterating through a standard array of keys and paths as key => value?

Also I was wondering how to handle errors effectively. I've just used the SDK docs examples for Promise error handling. Ideally I would like to have used the code here: https://docs.aws.amazon.com/sdk-for-php/v3/developer-guide/s3-multipart-upload.html#recovering-from-errors to resume from failed uploads but that's probably over engineering in my case.

My code:

//Create a S3Client
$s3Client = new S3Client([
    'profile' => 'default',
    'region' => env('AWS_REGION'),
    'version' => 'latest'
]);
$promises = [];
// Compose promises:
foreach($filenames_array as $keyToFile => $pathToFile) {
    $uploader = new MultipartUploader($s3Client, $pathToFile, [
        'bucket'          => env('AWS_REGION'),
        'key'             => $keyToFile,
        'concurrency'     => 25,
    ]);
    $promises[] = $uploader->promise();
}
// Execute upload:
$aggregate = \GuzzleHttp\Promise\all($promises);

try {
    $result = $aggregate->wait();
} catch (S3Exception $e) {
    // Handle the error
    // echo $e->getMessage();
}

Thanks again

1 like
MarkLL's avatar

@AJCK - Hi @ajck What $this->fetchFilenamesToUpload() did was to iterate a directory, and yield the $keyToFile => $pathToFile so I could handle any size and it fitted the promise model. However, yes it's basically a filename => path key pair.

Personally I only ever use env() in config files but shouldn't make any difference. Better to move the env() call outside the loop though and assign a variable imho.

Because it was only a devtool, I did not go too deep into error recovery. I did try it, but it didn't recover very well LOL.

1 like

Please or to participate in this conversation.