October 29, 2019
Before you start playing around with rails, it is important to setup AWS to support presigned upload.
When you are creating you S3 bucket, make sure you configureyour CORS:
<?xml version="1.0" encoding="UTF-8"?>
<CORSConfiguration xmlns="http://s3.amazonaws.com/doc/2006-03-01/">
<CORSRule>
<AllowedOrigin>*</AllowedOrigin>
<AllowedMethod>POST</AllowedMethod>
<AllowedHeader>*</AllowedHeader>
</CORSRule>
</CORSConfiguration>
If you need the files in the bucket to be public, make sure you make sure under block access is disabled.
When you are granting access to the SDK, you do not want to use your primary account for security purposes. Incase it is compromised, you can restrict it to only have desired actions. Do not make it an admin!
Also when you are creating your new IAM make sure you select Programmatic access
. This will allow you have to API access through the sdk.
You will also need to set the permissions policies. I recomend creating a custom one like this:
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"s3:Post*"
],
"Resource": [
"arn:aws:s3:::example-avatars/*"
]
}
]
}
Make sure you change example-avatars
to match your s3 bucket name.
When you are create the new IAM copy they key
and secret
gem 'aws-sdk-s3'
gem 'dotenv-rails', groups: [:development, :test]
.env
AWS_KEY=XXXXXXXXXXX
AWS_SECRET=XXXXXXXX
config/initializers/aws.rb
require 'aws-sdk-s3'
Aws.config.update({
region: 'us-west-1',
credentials: Aws::Credentials.new(ENV['AWS_KEY'], ENV['AWS_SECRET'])
})
app/controllers/avatar_s3_presign_controller.rb
class AvatarS3PresignController < ApplicationController
skip_before_action :verify_authenticity_token, raise: false
def create
render json: {
'form-data' => s3_direct_post.fields,
key: key,
presigned_post: s3_direct_post,
url: s3_direct_post.url,
}
end
private
def key
@key ||= "user/#{client.id}/avatar-#{SecureRandom.uuid}"
end
def s3_direct_post
@s3_direct_post ||= begin
s3 = Aws::S3::Resource.new
bucket = s3.bucket('example_bucket')
bucket.presigned_post(key: key, success_action_status: '201', acl: 'public-read')
end
end
def client
Client.last # todo make sure you change this not to be the last client
end
end
app/controllers/avatar_controller.rb
class AvatarController < ApplicationController
skip_before_action :verify_authenticity_token, raise: false
before_action :fetch_client
def create
if @client.update(avatar_params)
render json: { message: 'Uploaded' }
else
render json: { message: @client.errors.full_messages.to_sentence }
end
end
def edit;end
private
def avatar_params
params.require(:client).permit(:avatar_url)
end
def fetch_client
@client = Client.last # todo make sure you change this not to be the last client
end
end
app/assets/javascripts/avatar.js
function readURL(input) {
if (input.files && input.files[0]) {
const file = input.files[0];
$.post('/avatar_s3_presign', {}, function (resp) {
var image = resp.url + '/' + resp.key;
var formData = new FormData();
for (let key in resp.form_data) {
formData.append(key, resp.form_data[key]);
}
formData.append('file', input.files[0]);
$.ajax({
type: "POST",
url: resp.url,
data: formData,
processData: false,
contentType: false,
success: function(uploadResp) {
$.post('/avatar', {
client: {
avatar_url: image,
}
}, function (resp) {
var reader = new FileReader();
reader.onload = function(e) {
$('#imagePreview').css('background-image', 'url('+e.target.result +')');
$('#imagePreview').hide();
$('#imagePreview').fadeIn(650);
}
reader.readAsDataURL(input.files[0]);
}).fail(function(response) {
var message = response.responseJSON.message || 'On no something went wrong';
alert(message);
});
}
})
});
}
}
$("#imageUpload").change(function() {
readURL(this);
});
app/views/avatar/edit.html.erb
<%= javascript_include_tag 'avatar' %>
<%= stylesheet_link_tag 'avatar' %>
<div class="avatar-upload">
<div class="avatar-edit">
<input type='file' id="imageUpload" accept=".png, .jpg, .jpeg" />
<label for="imageUpload"></label>
</div>
<div class="avatar-preview">
<div id="imagePreview" style="background-image: url(<%= @client.avatar_url || 'http://i.pravatar.cc/500?img=7' %>);">
</div>
</div>
</div>
app/stylesheets/edit.scss
.avatar-upload {
position: relative;
max-width: 205px;
margin: 50px auto;
.avatar-edit {
position: absolute;
right: 12px;
z-index: 1;
top: 10px;
input {
display: none;
+ label {
display: inline-block;
width: 34px;
height: 34px;
margin-bottom: 0;
border-radius: 100%;
background: #FFFFFF;
border: 1px solid transparent;
box-shadow: 0px 2px 4px 0px rgba(0, 0, 0, 0.12);
cursor: pointer;
font-weight: normal;
transition: all .2s ease-in-out;
&:hover {
background: #f1f1f1;
border-color: #d6d6d6;
}
&:after {
content: "\f040";
font-family: 'FontAwesome';
color: #757575;
position: absolute;
top: 10px;
left: 0;
right: 0;
text-align: center;
margin: auto;
}
}
}
}
.avatar-preview {
width: 192px;
height: 192px;
position: relative;
border-radius: 100%;
border: 6px solid #F8F8F8;
box-shadow: 0px 2px 4px 0px rgba(0, 0, 0, 0.1);
> div {
width: 100%;
height: 100%;
border-radius: 100%;
background-size: cover;
background-repeat: no-repeat;
background-position: center;
}
}
}
config/routes.rb
Rails.application.routes.draw do
resources :avatar, only: [:create]
resources :avatar_s3_presign, only: [:create]
end