iCoders iCoders - 1 month ago 12
PHP Question

Laravel secure Amazon s3 bucket files

I am using Amazon s3 but here i am facing two problems

1.I cant directly upload files to amazon server when i submit form.I mean i have to upload images to upload folder and from there i have to retrieve and upload to

s3 server
.is there a way to upload images directly when we click on submit ?

2.if i pass
'public'
in
s3 put object
then only i can access or view files but if i make it public every one can view files but i need to protect all files and view only to the authenticated user .Can any one suggest me how to fix this issue ?

try {
$s3 = \Storage::disk('s3');
$s3->put($strFileName, file_get_contents($img_path.$strFileName), 'public');
} catch (Aws\Exception\S3Exception $e) {
echo "There was an error uploading the file.\n"+$e;
}


Before asking questions i have read so many answers from stackoverflow but it didnt helped me to fix my issue.Thanks

Answer

I recently tackled this problem. First off yes you can upload directly to s3 here is what I used for some info on this: http://docs.aws.amazon.com/AmazonS3/latest/dev/HTTPPOSTExamples.html

First off you need to create a policy and signature server side to add to your html form for uploading files.

$policy = base64_encode(json_encode([
            "expiration" => "2100-01-01T00:00:00Z",
            "conditions" => [
                ["bucket"=> "bucketname"],
                ["starts-with", '$key', "foldername"],
                ["acl" => "public-read"],
                ["starts-with", '$Content-Type', "image/"],
                ["success_action_status" => '201'],
            ]
        ]));
$signature = base64_encode(hash_hmac('sha1',$policy,getenv('S3_SECRET_KEY'),true));

Now on the frontend my form I don't use a submit button, you could use a submit button but you will need to catch the submit and prevent the form from actually submitting till after the upload finishes.

When we click save, it generates an md5 (use npm to install) filename so that file names can't really be guessed randomly, it then uses ajax to upload the file up to S3. After this is finished it puts the file data and returned aws data in a hidden input and submits the form. It should look something like this:

<form action="/post/url" method="POST" id="form">
    <input type="text" name="other_field" />
    <input type="file" class="form-control" id="image_uploader" name="file" accept="image/*" />
    <input type="hidden" id="hidden_medias" name="medias" value="[]" />
</form>
<input type="button" value="save" id="save" />
<script>
$(document).ready(function(){
    $('#save').click(function(){
            uploadImage(function () {
                $('#form').submit();
            });
    });
});
var uploadImage = function(callback) {
    var file = $('#image_uploader')[0].files[0];
    if(file !== undefined) {
        var data = new FormData();
        var filename = md5(file.name + Math.floor(Date.now() / 1000));
        var filenamePieces = file.name.split('.');
        var extension = filenamePieces[filenamePieces.length - 1];
        data.append('acl',"public-read");
        data.append('policy',"{!! $policy !!}");
        data.append('signature',"{!! $signature !!}");
        data.append('Content-type',"image/");
        data.append('success_action_status',"201");
        data.append('AWSAccessKeyId',"{!! getenv('S3_KEY_ID') !!}");
        data.append('key',filename + '.' + extension);
        data.append('file', file);

        var fileData = {
            type: file.type,
            name: file.name,
            size: file.size
        };

        $.ajax({
            url: 'https://{bucket_name}.s3.amazonaws.com/',
            type: 'POST',
            data: data,
            processData: false,
            contentType: false,

            success: function (awsData) {
                var xmlData = new XMLSerializer().serializeToString(awsData);
                var currentImages = JSON.parse($('#hidden_medias').val());
                currentImages.push({
                    awsData: xmlData,
                    fileData: fileData
                });
                $('#hidden_medias').val(JSON.stringify(currentImages));
                callback();
            },
            error: function (errorData) {
                console.log(errorData);
            }
        });
    }
};
</script>

The controller listening for the submit then parses the JSON from that input field and creates an instance of Media (a model I created) and it stores the awsData and fileData for each image.

Then instead of pointing html image tags to the s3 file like this:

<img src="https://{bucketname}.s3.amazonaws.com/filename.jpg" />

I do something like this:

<img src="/medias/{id}" />

Then the route can go through the normal auth middleware and all you need to do in Laravel. Finally, that route points to a controller that does this:

public function getResponse($id)
{
    $media = Media::find($id);
    return (new Response('',301,['Location' => $media->info['aws']['Location']]));
}

So what this does is simply uses a 301 redirect and sets the header location to the actual aws file. Since we generate an md5 filename when we upload the file to aws each filename is an md5 so people couldn't randomly search for aws files in the bucket.