I'm receiving files (up to 4 GB): the file-content is streamed to me in the body of a POST request.
I want to upload this stream directly to a s3 bucket, without saving it locally first.
Already tried different approaches which failed for different reasons.
My current approach:
use GuzzleHttp\Psr7\Stream;
use Aws\S3\S3Client;
$s3 = new \Aws\S3\S3Client([
'version' => 'latest',
'region' => 'eu-west-1',
'credentials' => [
'key' => 'abc',
'secret' => '123'
]
]);
$stream = new \GuzzleHttp\Psr7\Stream(fopen('php://input', 'r'));
$result = $s3->putObject(array(
'Bucket' => $bucket,
'Key' => $keyname,
'ContentLength' => (int)$_SERVER['CONTENT_LENGTH'],
'Body' => $stream->getContents(),
'ACL' => 'private',
'StorageClass' => 'STANDARD_IA',
));
The following error occurs while trying to stream a 80 MB file:
PHP message: PHP Fatal error: Allowed memory size of 134217728 bytes exhausted (tried to allocate 78847383 bytes) in /var/www/slimapi/vendor/slim/slim/Slim/Http/Stream.php on line 403
Line 403 of Stream.php is:
if (!$this->isReadable() || ($contents = stream_get_contents($this->stream)) === false) {
So the error is probably caused by triying to load the whole content of the stream into a string, which exceeds the memory limit.
(It's irritating why the error is occuring within Slim/Stream, as I'm trying to use guzzle\Stream.)
So my questions is:
How can I stream the incoming POST data directly to a s3 bucket without buffering issues leading to memory problems?
I already tried:
$stream = Psr7\stream_for(fopen('php://input', 'r'));
$stream
= fopen('php://input', 'r');
within putObject(): 'Body' => Stream::factory(fopen('php://input', 'r')),
I know this is old topic but it's not marked as solved, so...
PHP SDK does support stream source as you can see in the SDK specs (https://docs.aws.amazon.com/aws-sdk-php/v3/api/api-s3-2006-03-01.html#putobject) - see Parameter syntax:
$result = $client->putObject([
// ...
'Body' => <string || resource || Psr\Http\Message\StreamInterface>,
// ...
]);
It means your code is almost OK, the only thing is that you should pass $stream instead of $stream->getContents():
$stream = new \GuzzleHttp\Psr7\Stream(fopen('php://input', 'r'));
$result = $s3->putObject(array(
'Bucket' => $bucket,
'Key' => $keyname,
'ContentLength' => (int)$_SERVER['CONTENT_LENGTH'],
'Body' => $stream,
'ACL' => 'private',
'StorageClass' => 'STANDARD_IA',
));
As simple as that.
That PHP SDK call does not support directly reading a stream. So what appears to be happening to me is that PHP is exhausting memory as it loads the entire object from that stream into a variable, before it actually calls the SDK to PUT that string of data to the object.
You'll want to consider using the S3 Stream Wrapper.
This example seems most appropriate, but you'll need to pass the data between both streams. While the S3 Stream Wrapper appears to support creating a stream say from a local file, I didn't see a direct example of passing an existing stream to it.
In this example, we read 4096 bytes from the source if available (or less if 4096 isn't available, and if the returned value is non-empty then we write that to the S3 object. We continue this until the source reached EOF (in this example source must support and EOF).
$client = new Aws\S3\S3Client([/** options **/]);
// Register the stream wrapper from an S3Client object
$client->registerStreamWrapper();
$stream = fopen('s3://bucket/key', 'w');
while (!$stream_source->stream_eof()) {
$string = $stream_source->stream_read (4096)
if (!empty($string)) {
fwrite($stream, $string);
}
}
fclose($stream);
Related
latestAll S3 bucket file is displayed, but when I upload file then error is generate.
I have ARN and instance profile.
use Aws\Credentials\CredentialProvider;
use Aws\Credentials\InstanceProfileProvider;
use Aws\Credentials\AssumeRoleCredentialProvider;
use Aws\S3\S3Client;
use Aws\Sts\StsClient;
$profile = new InstanceProfileProvider();
$ARN = ""; // MY ARN
$sessionName = "s3-access-example";
$assumeRoleCredentials = new AssumeRoleCredentialProvider([
'client' => new StsClient([
'region' => "ap-east-1",
'version' => "latest",
'credentials' => $profile
]),
'assume_role_params' => [
'RoleArn' => $ARN,
'RoleSessionName' => $sessionName,
],
]);
$provider = CredentialProvider::memoize($assumeRoleCredentials);
$this->s3hd = S3Client::factory([
'credentials' => $provider,
'version' => "latest",
'region' => "ap-east-1"
]);
public function upload($name, $file, $type, $Bucket = false)
{
if (! $Bucket) {
$Bucket = $this->bucket;
}
$result = $this->s3hd->putObject([
'Bucket' => $Bucket,
'Key' => $name,
'SourceFile' => $file,
'ContentType' => $type,
'ACL' => 'public-read'
]);
$this->s3hd->waitUntil('ObjectExists', [
'Bucket' => $Bucket,
'Key' => $name
]);
return $result;
}
Message: Error executing "PutObject" on error file url here; AWS HTTP error: Client error: PUT error file url here resulted in a 400 Bad Request` response: InvalidTokenThe provided token is malformed or other (truncated...) InvalidToken (client): The provided token is malformed or otherwise invalid. - InvalidTokenThe provided token is malformed or otherwise invalid.
In my case. I was trying through CLI.
I configured AWS cli by running
aws configure
Inside ~/.aws/credentials, look for session_token. If you have the session_token. Then update it.
Otherwise remove session_token and try again.
And Voila It worked!
You're getting this error because Hong Kong (ap-east-1) is not enabled by default. You need to enable it in the AWS console if you have the correct permissions, then try again.
See the AWS Docs for instructions on how to do that.
I am using s3cmd and based on the above answer I was able to solve by deleting the .s3cmd file and reconfiguring (s3cmd --configure), which cleared the access_token:
diff .s3cfg previous_s3cmd
< access_token =
---
> access_token = vkp1Jf\etc...
Also increased the chunk sizes:
< recv_chunk = 65536
---
> recv_chunk = 4096
< send_chunk = 65536
---
> send_chunk = 4096
And I guess I had previously opted for https requests, as opposed to the (current) default:
< use_https = True
---
> use_https = False
Success
Success. Your access key and secret key worked fine :-)
Now verifying that encryption works...
Success. Encryption and decryption worked fine :-)
Here is my code, which works for forms upload (via $_FILES) (I'm omitting that part of the code because it is irrelevant):
$file = "http://i.imgur.com/QLQjDpT.jpg";
$s3 = S3Client::factory(array(
'region' => $region,
'version' => $version
));
try {
$content_type = "image/" . $ext;
$to_send = array();
$to_send["SourceFile"] = $file;
$to_send["Bucket"] = $bucket;
$to_send["Key"] = $file_path;
$to_send["ACL"] = 'public-read';
$to_send["ContentType"] = $content_type;
// Upload a file.
$result = $s3->putObject($to_send);
As I said, this works if file is a $_FILES["files"]["tmp_name"] but fails if $file is a valid image url with Uncaught exception 'Aws\Exception\CouldNotCreateChecksumException' with message 'A sha256 checksum could not be calculated for the provided upload body, because it was not seekable. To prevent this error you can either 1) include the ContentMD5 or ContentSHA256 parameters with your request, 2) use a seekable stream for the body, or 3) wrap the non-seekable stream in a GuzzleHttp\Psr7\CachingStream object. You should be careful though and remember that the CachingStream utilizes PHP temp streams. This means that the stream will be temporarily stored on the local disk.'. Does anyone know why this happens? What might be off? Tyvm for your help!
For anyone looking for option #3 (CachingStream), you can pass the PutObject command a Body stream instead of a source file.
use GuzzleHttp\Psr7\Stream;
use GuzzleHttp\Psr7\CachingStream;
...
$s3->putObject([
'Bucket' => $bucket,
'Key' => $file_path,
'Body' => new CachingStream(
new Stream(fopen($file, 'r'))
),
'ACL' => 'public-read',
'ContentType' => $content_type,
]);
Alternatively, you can just request the file using guzzle.
$client = new GuzzleHttp\Client();
$response = $client->get($file);
$s3->putObject([
'Bucket' => $bucket,
'Key' => $file_path,
'Body' => $response->getBody(),
'ACL' => 'public-read',
'ContentType' => $content_type,
]);
You have to download the file to the server where PHP is running first. S3 uploads are only for local files - which is why $_FILES["files"]["tmp_name"] works - its a file that's local to the PHP server.
I'm trying to transfer files from Shopify to S3, and I'm getting this error:
"You must specify a non-null value for the Body or SourceFile parameters." I believe it's because I'm transferring files from a remote location but I don't know how to get around this problem. I have validated that the remote file does exist, and that my code works fine if I'm uploading using a form.
My code:
require dirname(__FILE__).'/../../vendor/autoload.php';
use Aws\S3\S3Client;
$s3Client = null;
$bucket="mybucketname";
$myfile="myfilename.jpg";
$mypath="https://shopifyfilepath/".$myfile;
function SendFileToS3($filename, $filepath, $bucket)
{
global $s3Client;
if (!$s3Client)
{
$s3Client = S3Client::factory(array(
'key' => $_SERVER["AWS_ACCESS_KEY_ID"],
'secret' => $_SERVER["AWS_SECRET_KEY"]
));
}
$result = $s3Client->putObject(array(
'Bucket' => $bucket,
'Key' => $filename,
'SourceFile' => $filepath,
'ACL' => 'public-read'
));
return $result;
}
SendFileToS3($myfile, $mypath, $bucket);
You can't transfer a file directly from a HTTP path to S3. You'll need to download it to a local file (or variable) first, then transfer that.
I'm having problems setting the "Metadata" option when uploading files to Amazon S3 using the AWS SDK PHP v2. The documentation that I'm reading for the upload() method states that the the 5th parameter is an array of options...
*$options Custom options used when executing commands: - params: Custom
parameters to use with the upload. The parameters must map to a
PutObject or InitiateMultipartUpload operation parameters. -
min_part_size: Minimum size to allow for each uploaded part when
performing a multipart upload. - concurrency: Maximum number of
concurrent multipart uploads. - before_upload: Callback to invoke
before each multipart upload. The callback will receive a
Guzzle\Common\Event object with context.*
My upload() code looks like this..
$upload = $client->upload(
'<BUCKETNAME>',
'metadatatest.upload.jpg',
fopen('metadatatest.jpg','r'),
'public-read',
array('Metadata' => array(
'SomeKeyString' => 'SomeValueString'
))
);
...and no meta data is set after upload.
If however I use putObject() as documented here, which I assume is a "lower level" method compared to upload()...
$putObject = $client->putObject(
array(
'Bucket' => '<BUCKETNAME>',
'Key' => 'metadatatest.putobject.jpg',
'Body' => file_get_contents('metadatatest.jpg'),
'ACL' => 'public-read',
'Metadata' => array(
'SomeKeyString' => 'SomeValueString'
)
)
);
The meta data is successfully returned when I call getObject() or view the file directly in my browser when uploaded using putObject()
$getObject = $client->getObject(
array(
'Bucket' => '<BUCKETNAME>',
'Key' => 'metadatatest.putobject.jpg'
)
);
I would prefer to use the $client->upload() method as the documentation states
Upload a file, stream, or string to a bucket. If the upload size exceeds the specified threshold, the upload will be performed using
parallel multipart uploads.
I'm not sure what I've missed?
There's really no difference in using upload() or putObject() if you don't do multipart uploads. You can have a look at the AWS PHP SDK source code but basically the upload method just calls putObject like this:
// Perform a simple PutObject operation
return $this->putObject(array(
'Bucket' => $bucket,
'Key' => $key,
'Body' => $body,
'ACL' => $acl
) + $options['params']);
This isn't very clear in the SDK documentation, but you need to send the last parameter as an array with the key params and its value being a second array with the Metadata key and value like this:
$upload = $client->upload(
'<BUCKETNAME>',
'metadatatest.upload.jpg',
fopen('metadatatest.jpg','r'),
'public-read',
array('params' => array(
'Metadata' => array(
'SomeKeyString' => 'SomeValueString'
)))
);
However, I you could just use the putObject call to achieve the same thing.
I am to newbie to amazon s3. I downloaded zip into my project and unzipped it. Bucket is already created 'bucketToUpload'. My code for upload a file is
require 'aws/aws-autoloader.php';
use Aws\S3\S3Client;
use Aws\S3\Exception\S3Exception;
use Aws\S3\Enum\CannedAcl;
$bucket = 'bucketToUpload';
$pathToFile = getcwd().'/s3_upload_file.doc';
if(file_exists($pathToFile)) {
try {
$client = S3Client::factory(array(
'key' => 'MYKEY',
'secret' => 'MYSECRETKEY',
'region' => 'us-west-1'
));
$client->waitUntilBucketExists(array('Bucket' => $bucket));
$result = $client->putObject(array(
'Bucket' => $bucket,
'Key' => 's3_upload_file.doc',
'SourceFile' => getcwd(),
'ACL' => CannedAcl::PRIVATE_ACCESS,
'Metadata' => array(
'Foo' => 'abc',
'Baz' => '123'
)
));
} catch (S3Exception $e) {
echo "The file was not uploaded: " . $e->getMessage();
}
var_dump($result);
}
I am getting Fatel error: Maximum execution time of 15 seconds exceeded.
Really don't know what am I doing wrong. Any help could be really appreciable.
Thanks
I see a few things you may need to do.
First, you are specifying
'SourceFile' => getcwd(),
When I think you probably meant to do
'SourceFile' => $pathToFile,
Second, you are doing var_dump($result); which will probably not show you what you are expecting to see. Try var_dump($result->toArray()); instead, but make sure to checkout the user guide page about response models for more information about working with results.
Third, the error you are seeing Fatal error: Maximum execution time of 15 seconds exceeded is related to PHP's max_execution_time INI setting. You should increase that limit if needed. You can also use the set_time_limit function within a single process.
I hope that helps!