I'm using a modified version of the Jquery Multi-file uploader from Railscast #383 (http://railscasts.com/episodes/383-uploading-to-amazon-s3) in a Rails 3 app, and I need to tweak it so that it checks if a file already exists on S3, and skips re-uploading it if so.
Some background: my users need to update large chunks of data. For instance, one might select 500 4MB files to upload. Inevitably, their internet connection breaks, and rather than expecting the user to figure out which files uploaded and which didn't, I want them to be able to just select those same 500 files and the app be smart enough to not start again at the very beginning.
The most preferable solution would be to include an option in the S3 POST that says not to overwrite an existing file. Next most preferable would be to fire off a GET to S3 to see if the file exists and skip it if so.
Least preferably, I've implemented a solution that non-asynchronously fires off a GET to my Rails app (because I create a database entry upon completion of each upload), but I seem to be having problems with throttling those requests, and my user says her browser keeps crashing (it does all 500 at once I guess).
Relevant application.js
//= require jquery
//= require jquery_ujs
//= require jquery.ui.all
//= require jquery-fileupload/basic
//= require jquery-fileupload/vendor/tmpl
My form:
<%= s3_uploader_form post: uploaded_photos_path, as: "uploaded_photo[image_url]", photo_shoot_id: #photo_shoot.id do %>
<%= file_field_tag :file, multiple: true %>
<%= button_tag 'Upload Photos', id: 'upload_photo_button', type: 'button' %>
<% end %>
My javascript:
$(function() {
$('#s3_uploader').fileupload({
limitConcurrentUploads: 5,
add: function(e, data) {
var file, record_exists, photo_check_url;
file = data.files[0];
photo_check_url = "/my_route/has_photo_been_uploaded/" + encodeURIComponent(file.name)
// THIS IS MY NON-THROTTLING HACK THAT NEEDS REPLACEMENT/IMPROVEMENT
// THE CONTROLLER THAT HANDLES THE REQUEST JUST RENDERS AN INLINE STRING OF 'true' OR 'false'
$.ajax( {
url: photo_check_url,
async: false,
success: function (result) {
record_exists = result;
}
});
if (record_exists == 'false') {
data.context = $(tmpl("template-upload", file));
$('#s3_uploader').append(data.context);
data.submit();
}
},
progress: function(e, data) { // irrelevant },
done: function(e, data) { // irrelevant. It posts the object to my database }
},
fail: function(e, data) { // irrelevant }
});
});
My Helper:
module S3UploaderHelper
def s3_uploader_form(options = {}, &block)
uploader = S3Uploader.new(options)
form_tag(uploader.url, uploader.form_options) do
uploader.fields.map do |name, value|
hidden_field_tag(name, value)
end.join.html_safe + capture(&block)
end
end
class S3Uploader
def initialize(options)
#options = options.reverse_merge(
id: "s3_uploader",
aws_access_key_id: ENV["S3_ACCESS_KEY"],
aws_secret_access_key: ENV["S3_SECRET_ACCESS_KEY"],
bucket: S3_BUCKET_NAME,
acl: "private",
expiration: 10.hours.from_now.utc,
max_file_size: 20.megabytes,
as: "file"
)
end
def form_options
{
id: #options[:id],
method: "post",
authenticity_token: false,
multipart: true,
data: {
post: #options[:post],
as: #options[:as]
}
}
end
def fields
{
:key => key,
:acl => #options[:acl],
:policy => policy,
:signature => signature,
"AWSAccessKeyId" => #options[:aws_access_key_id],
}
end
def key
#key ||= "uploaded_photos/${filename}"
end
def url
"https://#{#options[:bucket]}.s3.amazonaws.com/"
end
def policy
Base64.encode64(policy_data.to_json).gsub("\n", "")
end
def policy_data
{
expiration: #options[:expiration],
conditions: [
["starts-with", "$utf8", ""],
["starts-with", "$key", ""],
["content-length-range", 0, #options[:max_file_size]],
{bucket: #options[:bucket]},
{acl: #options[:acl]}
]
}
end
def signature
Base64.encode64(
OpenSSL::HMAC.digest(
OpenSSL::Digest::Digest.new('sha1'),
#options[:aws_secret_access_key], policy
)
).gsub("\n", "")
end
end
end
After learning more about AJAX (after it occurred to me in my second comment), it looks like an acceptable solution was indeed to make the AJAX call asynchronous and place the S3 POST code inside its success callback. That solved my browser non-responsiveness issues.
$.ajax( {
url: my_route_to_ask_if_photo_was_already_uploaded,
success: function (result) {
if (result == 'false') {
// ...other code
data.submit();
}
});
Related
I have created a new controller, that in future will be responsible only for translating strings for different objects.
Here is the code of the controller:
# frozen_string_literal: true
class Api::TranslationsController < AuthenticatedController
def translate_string
#translated_string = GoogleTranslator.translate('hello', 'de')
render json: {translated_string: #translated_string.to_json}
end
helper_method :translate_string
private
def translations_params
params.permit(
:id,
:translated_string
)
end
end
It is placed in directory controllers/api/translations_controller. Here is the part of routes:
namespace :api do
resource :translations do
member do
get 'translate_string'
end
end
end
And here is part of my html.erb to call JS function:
<%= image_tag "google-icon.png", id: "google_icon", onclick: "test_transl()",
remote: true%>
And here is my JS code, currently only with Ajax:
<script>
function test_transl(){
$.ajax({
type: "GET",
url: "/api/translations/translate_string",
dataType: "json",
success:function (result){
console.log(result)
}
})
}
</script>
I expect this ajax code to translate the world 'hello' on German and get value #translated_string - 'hallo' in console but nothing happens except the fact, that the error I got is 'statusText: "parsererror"'. What may be wrong?
I finally found what was wrong with my code. The problem was with fact that I didn't pass sessions token in ajax. So now my ajax code will look like this:
$.ajax({
type: "GET",
headers: {
"Authorization": "Bearer " + window.sessionToken
},
url: "/api/translations/translate_string",
dataType: "json",
success: function(result){
console.log(result);
},
error: function (result){
console.log(result, this.error)
}
})
And def from controller like this:
def translate_string
#translated_string = GoogleTranslator.translate('hello', 'de')
render json: #translated_string.to_json
end
The output in console is: "hallo".
I have the smart buttons "working" in sandbox but I can't think of any way to attach the smart buttons success to the order form which creates the order. With Stripe Elements, it's pretty plug and play because it's on the page and a part of the form itself, but with PayPal with the redirects, I can't seem to think of a way.
Does this require javascript or can I do this without it, aside from what's already there?
Form:
<%= form_for(#order, url: listing_orders_path([#listing, #listing_video]), html: {id: "payment_form-4"} ) do |form| %>
<%= form.label :name, "Your Name", class: "form-label" %>
<%= form.text_field :name, class: "form-control", required: true, placeholder: "John" %>
#stripe code here (not important)
<%= form.submit %>
<div id="paypal-button-container"></div>
<!-- Include the PayPal JavaScript SDK -->
<script src="https://www.paypal.com/sdk/js?client-id=sb¤cy=USD"></script>
<script>
// Render the PayPal button into #paypal-button-container
paypal.Buttons({
// Set up the transaction
createOrder: function(data, actions) {
return actions.order.create({
purchase_units: [{
amount: {
value: <%= #listing.listing_video.price %>
}
}]
});
},
// Finalize the transaction
onApprove: function(data, actions) {
return actions.order.capture().then(function(details) {
// Show a success message to the buyer
alert('Transaction completed by ' + details.payer.name.given_name + '!');
});
}
}).render('#paypal-button-container');
</script>
Create Method in Controller:
require 'paypal-checkout-sdk'
client_id = Rails.application.credentials[Rails.env.to_sym].dig(:paypal, :client_id)
client_secret = Rails.application.credentials[Rails.env.to_sym].dig(:paypal, :client_secret)
# Creating an environment
environment = PayPal::SandboxEnvironment.new(client_id, client_secret)
client = PayPal::PayPalHttpClient.new(environment)
#amount_paypal = (#listing.listing_video.price || #listing.listing_tweet.price)
request = PayPalCheckoutSdk::Orders::OrdersCreateRequest::new
request.request_body(
{
intent: 'AUTHORIZE',
purchase_units: [
{
amount: {
currency_code: 'USD',
value: "#{#amount_paypal}"
}
}
]
}
)
begin
# Call API with your client and get a response for your call
response = client.execute(request)
# If call returns body in response, you can get the deserialized version from the result attribute of the response
order = response.result
puts order
#order.paypal_authorization_token = response.id
rescue BraintreeHttp::HttpError => ioe
# Something went wrong server-side
puts ioe.status_code
puts ioe.headers['debug_id']
end
How can I tie in the PayPal smart buttons with the form so once the payment is completed, it creates an order if successful?
UPDATE:::::::
Created a PaypalPayments controller and model:
controller:
def create
#paypal_payment = PaypalPayment.new
#listing = Listing.find_by(params[:listing_id])
require 'paypal-checkout-sdk'
client_id = "#{Rails.application.credentials[Rails.env.to_sym].dig(:paypal, :client_id)}"
client_secret = "#{Rails.application.credentials[Rails.env.to_sym].dig(:paypal, :client_secret)}"
# Creating an environment
environment = PayPal::SandboxEnvironment.new(client_id, client_secret)
client = PayPal::PayPalHttpClient.new(environment)
#amount_paypal = #listing.listing_video.price
request = PayPalCheckoutSdk::Orders::OrdersCreateRequest::new
#paypal_payment = request.request_body({
intent: "AUTHORIZE",
purchase_units: [
{
amount: {
currency_code: "USD",
value: "#{#amount_paypal}"
}
}
]
})
begin
# Call API with your client and get a response for your call
response = client.execute(request)
# If call returns body in response, you can get the deserialized version from the result attribute of the response
order = response.result
puts order
# #order.paypal_authorization_token = response.id
rescue BraintreeHttp::HttpError => ioe
# Something went wrong server-side
puts ioe.status_code
puts ioe.headers["debug_id"]
end
# if #paypal_payment.create
# render json: {success: true}
# else
# render json: {success: false}
# end
end
Javascript in view:
paypal.Buttons({
createOrder: function() {
return fetch('/paypal_payments', {
method: 'post',
headers: {
'content-type': 'application/json'
}
}).then(function(res) {
return res.json();
}).then(function(data) {
return data.orderID;
});
},
onApprove: function(data) {
return fetch('/orders', {
method: 'post',
headers: {
'content-type': 'application/json'
},
body: JSON.stringify({
orderID: data.orderID
})
}).then(function(res) {
return res.json();
}).then(function(details) {
alert('Authorization created for ' + details.payer_given_name);
});
},
}).render('#paypal-button-container');
With this, the paypal box appears but then goes away right after it loads with this in the CMD:
#<OpenStruct id="1Pxxxxxxx394U", links=[#<OpenStruct href="https://api.sandbox.paypal.com/v2/checkout/orders/1P0xxxxxxx394U", rel="self", method="GET">, #<OpenStruct href="https://www.sandbox.paypal.com/checkoutnow?token=1P07xxxxxxx94U", rel="approve", method="GET">, #<OpenStruct href="https://api.sandbox.paypal.com/v2/checkout/orders/1Pxxxxxxx4U", rel="update", method="PATCH">, #<OpenStruct href="https://api.sandbox.paypal.com/v2/checkout/orders/1P07xxxxxxx394U/authorize", rel="authorize", method="POST">], status="CREATED">
No template found for PaypalPaymentsController#create, rendering head :no_content
Completed 204 No Content in 2335ms (ActiveRecord: 15.8ms)
I have not used smart buttons. However, you should not have "a ton more code" in a create action. If you are following MVC and rails conventions. It would seem that you need a seperate controller action to handle the payment authorization separately from the create action. But if you can get to this point in your javascript, here is example of how you would send the data from paypal javascript back to your controller, this will need some work but hopefully it points you in the right direction:
// Finalize the transaction
onApprove: function(data, actions) {
return actions.order.capture().then(function(details) {
// Show a success message to the buyer
alert('Transaction completed by ' + details.payer.name.given_name + '!');
// here is where you should send info to your controller action via ajax.
$.ajax({
type: "POST",
url: "/orders",
data: data,
success: function(data) {
alert(data); // or whatever you wanna do here
return false;
},
error: function(data) {
alert(data); // or something else
return false;
}
});
});
}
This is most likely far too late, but ill add what worked for me.
You need to return the response ID to the PayPal script as a json object. All you need to do is update your create function like so :
...
begin
# Call API with your client and get a response for your call
response = client.execute(request)
# If call returns body in response, you can get the deserialized version from the result attribute of the response
order = response.result
render json: { orderID: order.id }
# #order.paypal_authorization_token = response.id
rescue BraintreeHttp::HttpError => ioe
# Something went wrong server-side
puts ioe.status_code
puts ioe.headers["debug_id"]
end
...
I have spent days now trying to make this work. I am getting this error
OPTIONS https://bucketname.s3.oregon.amazonaws.com/ net::ERR_NAME_RESOLUTION_FAILED
I am using Version 43.0.2357.130 Ubuntu 14.04 (64-bit)
Gemfile:
gem "jquery-fileupload-rails"
gem 'aws-sdk'
application.js (after jquery):
//= require jquery-fileupload/basic
application.css:
*= require jquery.fileupload
*= require jquery.fileupload-ui
I have a model called uploads that I have generated scaffolds for like this:
rails generate scaffold Upload upload_url:string
uploads_controller.rb:
def new
#s3_direct_post = Aws::S3::PresignedPost.new(Aws::Credentials.new(ENV['AWS_S3_ACCESS_KEY_ID'], ENV['AWS_S3_SECRET_ACCESS_KEY']),
"Oregon", ENV['AWS_S3_BUCKET'], {
key: '/uploads/object/test.test',
content_length_range: 0..999999999,
acl: 'public-read',
success_action_status: "201",
})
#upload = Upload.new
end
_form.html.erb (for uploads):
<%= form_for(#upload, html: { class: "directUpload" }) do |f| %>
......
<div class="field">
<%= f.label :upload_url %><br>
<%= f.file_field :upload_url %>
</div>
......
<%= content_tag "div", id: "upload_data", data: {url: #s3_direct_post.url, form_data: #s3_direct_post.fields } do %>
<% end %>
application.js (in the end):
$( document ).ready(function() {
$(function() {
$('.directUpload').find("input:file").each(function(i, elem) {
var fileInput = $(elem);
var form = $(fileInput.parents('form:first'));
var submitButton = form.find('input[type="submit"]');
var progressBar = $("<div class='bar'></div>");
var barContainer = $("<div class='progress'></div>").append(progressBar);
fileInput.after(barContainer);
fileInput.fileupload({
fileInput: fileInput,
url: $('#upload_data').data('url'),
type: 'POST',
autoUpload: true,
formData: $('#upload_data').data('form-data'),
paramName: 'file', // S3 does not like nested name fields i.e. name="user[avatar_url]"
dataType: 'XML', // S3 returns XML if success_action_status is set to 201
replaceFileInput: false,
progressall: function (e, data) {
var progress = parseInt(data.loaded / data.total * 100, 10);
progressBar.css('width', progress + '%')
},
start: function (e) {
submitButton.prop('disabled', true);
progressBar.
css('background', 'green').
css('display', 'block').
css('width', '0%').
text("Loading...");
},
done: function(e, data) {
submitButton.prop('disabled', false);
progressBar.text("Uploading done");
// extract key and generate URL from response
var key = $(data.jqXHR.responseXML).find("Key").text();
// create hidden field
var input = $("<input />", { type:'hidden', name: fileInput.attr('name'), value: url })
form.append(input);
},
fail: function(e, data) {
submitButton.prop('disabled', false);
progressBar.
css("background", "red").
text("Failed");
}
});
});
});
});
Seriously What can I do to fix this?
My guess is that you have misconfigured your bucket name / route. The error comes from Amazon, warning you there is no DNS route to https://bucketname.s3.oregon.amazonaws.com/.
It seems to me you need to set the actual bucketname to your bucket name, and also drop oregon from the url. Given that your bucket is named aymansalah, the url will be: https://aymansalah.s3.amazonaws.com/
Review Aws::Credentials documentation and check your environment variables to achieve that URL.
I found the problem. Thanks a lot to felixbuenemann a collaborator in jquery-fileupload-rails
Although that is what I see in the properties (it says Region: Oregon), I have to use "us-west-2" according to this Amazon region documentation
uploads_controller.rb is now:
def new
#s3_direct_post = Aws::S3::PresignedPost.new(Aws::Credentials.new(ENV['AWS_S3_ACCESS_KEY_ID'], ENV['AWS_S3_SECRET_ACCESS_KEY']),
"us-west-2", ENV['AWS_S3_BUCKET'], {
key: '/uploads/object/test.test',
content_length_range: 0..999999999,
acl: 'public-read',
success_action_status: "201",
})
#upload = Upload.new
end
I need help providing a content-type to amazon via a client side jQuery upload form. I need to add the content type because I'm uploading audio files that will not play in jPlayer for ie10 unless the content type is properly set. I used the blog post by pjambet - http://pjambet.github.io/blog/direct-upload-to-s3/ to get up and running (excellent post btw). It seems though that the order of the fields is extremely important. I've been trying to insert a hidden input tag either contaning the relevant content type (audio/mpeg3 I think) or blank to be populated by my upload script. No luck. The upload hangs when the extra fields are added.
direct-upload-form.html.erb
<form accept-charset="UTF-8" action="http://my_bucket.s3.amazonaws.com" class="direct-upload" enctype="multipart/form-data" method="post"><div style="margin:0;padding:0;display:inline"></div>
<%= hidden_field_tag :key, "${filename}" %>
<%= hidden_field_tag "AWSAccessKeyId", ENV['AWS_ACCESS_KEY_ID'] %>
<%= hidden_field_tag :acl, 'public-read' %>
<%= hidden_field_tag :policy %>
<%= hidden_field_tag :signature %>
<%= hidden_field_tag :success_action_status, "201" %>
<%= file_field_tag :file %>
<div class="row-fluid">
<div class="progress hide span8">
<div class="bar"></div>
</div>
</div>
</form>
audio-upload.js
$(function() {
$('input[type="submit"]').attr("disabled","true");
$('input[type="submit"]').val("Please upload audio first");
if($('#demo_audio').val() != ''){
var filename = $('#demo_audio').val().split('/').pop().split('%2F').pop();
$('#file_status').removeClass('label-info').addClass('label-success').html(filename + ' upload complete');
}
$('.direct-upload').each(function() {
var form = $(this)
$(this).fileupload({
url: form.attr('action'),
type: 'POST',
autoUpload: true,
dataType: 'xml', // This is really important as s3 gives us back the url of the file in a XML document
add: function (event, data) {
$.ajax({
url: "/signed_urls",
type: 'GET',
dataType: 'json',
data: {doc: {title: data.files[0].name}}, // send the file name to the server so it can generate the key param
async: false,
success: function(data) {
// Now that we have our data, we update the form so it contains all
// the needed data to sign the request
form.find('input[name=key]').val(data.key)
form.find('input[name=policy]').val(data.policy)
form.find('input[name=signature]').val(data.signature)
}
})
data.form.find('#content-type').val(file.type)
data.submit();
},
send: function(e, data) {
var filename = data.files[0].name;
$('input[type="submit"]').val("Please wait until audio uploaded is complete...");
$('#file_status').addClass('label-info').html('Uploading ' + filename);
$('.progress').fadeIn();
},
progress: function(e, data){
// This is what makes everything really cool, thanks to that callback
// you can now update the progress bar based on the upload progress
var percent = Math.round((e.loaded / e.total) * 100)
$('.bar').css('width', percent + '%')
},
fail: function(e, data) {
console.log('fail')
},
success: function(data) {
// Here we get the file url on s3 in an xml doc
var url = $(data).find('Location').text()
$('#demo_audio').val(url) // Update the real input in the other form
},
done: function (event, data) {
$('input[type="submit"]').val("Create Demo");
$('input[type="submit"]').removeAttr("disabled");
$('.progress').fadeOut(300, function() {
$('.bar').css('width', 0);
var filename = data.files[0].name;
$('span.filename').html(filename);
$('#file_status').removeClass('label-info').addClass('label-success').html(filename + ' upload complete');
$('#file').hide();
})
},
})
})
})
signed_urls_controller.rb
class SignedUrlsController < ApplicationController
def index
render json: {
policy: s3_upload_policy_document,
signature: s3_upload_signature,
key: "uploads/#{SecureRandom.uuid}/#{params[:doc][:title]}",
success_action_redirect: "/"
}
end
private
# generate the policy document that amazon is expecting.
def s3_upload_policy_document
Base64.encode64(
{
expiration: 30.minutes.from_now.utc.strftime('%Y-%m-%dT%H:%M:%S.000Z'),
conditions: [
{ bucket: ENV['AWS_S3_BUCKET'] },
{ acl: 'public-read' },
["starts-with", "$key", "uploads/"],
{ success_action_status: '201' }
]
}.to_json
).gsub(/\n|\r/, '')
end
# sign our request by Base64 encoding the policy document.
def s3_upload_signature
Base64.encode64(
OpenSSL::HMAC.digest(
OpenSSL::Digest::Digest.new('sha1'),
ENV['AWS_SECRET_ACCESS_KEY'],
s3_upload_policy_document
)
).gsub(/\n/, '')
end
end
As mentioned in the comments section for the above question, two changes are required to set the Content-Type for the uploaded content to audio/mpeg3.
The policy for the S3 POST API call must be changed to accept an additional "Content-Type" value. In the sample code, this can be achieved by adding the following condition to the conditions array in the s3_upload_policy_document method: ["eq", "$Content-Type", "audio/mpeg3"]
The "Content-Type" variable must be included with the POST request to S3. In the jQuery file uploader plugin this can be achieved by adding a hidden field to the form that is sent to S3, with the name "Content-Type" and the value "audio/mpeg3".
I'm looking to add functionality to my Rails app to upload files directly to Amazon S3. From my research the general consensus seems to be to use the s3-swf-upload-plugin. I've setup a sample app using that gem but I can't get it to play nice with only allowing the selection of a single file. I'd also like to create a record post upload and use paperclip to create a thumbnail for which I can find little guidance.
So my questions are:
(1) am I on the right track using that gem or should I be taking another appraoch?
(2) are there any samples out there that I could use for reference?
Any assistance would be much appreciated.
Chris
Try a new Gem called CarrierWaveDirect it allows you to upload files direct to S3 using a html form and easily move the image processing into a background process
Not sure about whether you can modify it easily to only upload one file at a time, but this gem works very well for me. It is based on one of Ryan Bates' Railscast:
https://github.com/waynehoover/s3_direct_upload
Try looking into carrierwave https://github.com/jnicklas/carrierwave (supports s3)
Multi file uploads with carrierwave and uploadify http://blog.assimov.net/post/4306595758/multi-file-upload-with-uploadify-and-carrierwave-on
If you are using Rails 3, please check out my sample projects:
Sample project using Rails 3, Flash and MooTools-based FancyUploader to upload directly to S3: https://github.com/iwasrobbed/Rails3-S3-Uploader-FancyUploader
Sample project using Rails 3, Flash/Silverlight/GoogleGears/BrowserPlus and jQuery-based Plupload to upload directly to S3: https://github.com/iwasrobbed/Rails3-S3-Uploader-Plupload
By the way, you can do post-processing with Paperclip using something like this blog post describes:
http://www.railstoolkit.com/posts/fancyupload-amazon-s3-uploader-with-paperclip
I have adapted Heroku's direct to S3 upload solution in Rails (which uses jQuery-File-Upload and the aws-sdk gem) so uploads to S3 can be made remotely using ajax. I hope this is useful:
posts_controller.rb
before_action :set_s3_direct_post, only: [:index, :create]
before_action :delete_picture_from_s3, only: [:destroy]
class PostsController < ApplicationController
def index
.
.
end
def create
#post = #user.posts.build(post_params)
if #post.save
format.html
format.js
end
end
def destroy
Post.find(params[:id]).destroy
end
private
def set_s3_direct_post
return S3_BUCKET.presigned_post(key: "uploads/#{SecureRandom.uuid}/${filename}", success_action_status: '201', acl: 'public-read')
end
def delete_picture_from_s3
key = params[:picture_url].split('amazonaws.com/')[1]
S3_BUCKET.object(key).delete
return true
rescue => e
# If anyone knows a good way to deal with a defunct file sitting in the bucket, please speak up.
return true
end
def post_params
params.require(:post).permit(:content, :picture_url)
end
end
posts.html.erb
<div class="info" data-url="<%= #s3_direct_post.url %>"
data-formdata="<%= (#s3_direct_post.fields.to_json) %>"
data-host="<%= URI.parse(#s3_direct_post.url).host %>">
</div>
The form
<%= form_for(:post, url: :posts, method: :post,
html: { class: "post_form", id: "post_form-#{post.id}" }
) do |f| %>
<%= f.text_area :content, id: "postfield-#{post.id}", class: "postText" %>
<%= f.button( :submit, name: "Post", title: "Post" ) do %>
<span class="glyphicon glyphicon-ok" aria-hidden="true"></span>
<% end %>
<span class="postuploadbutton" id="postUp-<%= post.id %>" title="Add file" >
<span class="glyphicon glyphicon-upload" aria-hidden="true"></span>
</span>
<span title="Cancel file" class="noticecancelupload" id="postCancel-<%= post.id %>" >
<span class="glyphicon glyphicon-remove-circle" aria-hidden="true"></span>
</span>
<%= f.file_field :picture_url, accept: 'image/jpeg,image/gif,image/png',
class: "notice_file_field", id: "postFile-#{post.id}" %>
<% end %>
_post.html.erb
<%= button_to post_path(
params: {
id: post.id,
picture_url: post.picture_url
}
),
class: 'btn btn-default btn-xs blurme',
data: { confirm: "Delete post: are you sure?" },
method: :delete do %>
<span class="glyphicon glyphicon-remove" aria-hidden="true"></span>
<% end %>
Javascript in each _post.html.erb
$(document).off('click',"#postUp-<%= post.id %>");
$(document).on('click', '#postUp-<%= post.id %>', function(e) {
prepareUpload("#post_form-<%= post.id %>");
$('#postFile-<%= post.id %>').trigger("click");
});
$(document).off('click',"#postCancel-<%= post.id %>");
$(document).on('click', '#postCancel-<%= post.id %>', function(e) {
$(".appendedInput").remove(); // $('#postFile-<% post.id %>').val(""); doesn't work for me
$('.progBar').css('background','white').text("");
});
$(document).off('submit',"#post_form-<%= post.id %>"); // without this the form submitted multiple times in production
$(document).on('submit', '#post_form-<%= post.id %>', function(e) { // don't use $('#post_form-<%= post.id %>').submit(function() { so it doesn't bind to the #post_form (so it still works after ajax loading)
e.preventDefault(); // prevent normal form submission
if ( validatePostForm('<%= post.id %>') ) {
$.ajax({
type: 'POST',
url: $(this).attr('action'),
data: $(this).serialize(),
dataType: 'script'
});
$('#postCancel-<%= post.id %>').trigger("click");
}
});
function validatePostForm(postid) {
if ( jQuery.isBlank($('#postfield-' + postid).val()) && jQuery.isBlank($('#postFile-' + postid).val()) ) {
alert("Write something fascinating or add a picture.");
return false;
} else {
return true;
}
}
Javascript in application.js
function prepareUpload(feckid) {
$(feckid).find("input:file").each(function(i, elem) {
var fileInput = $(elem);
var progressBar = $("<div class='progBar'></div>");
var barContainer = $("<div class='progress'></div>").append(progressBar);
fileInput.after(barContainer);
var maxFS = 10 * 1024 * 1024;
var info = $(".info");
var urlnumbnuts = info.attr("data-url");
var formdatanumbnuts = jQuery.parseJSON(info.attr("data-formdata"));
var hostnumbnuts = info.attr("data-host");
var form = $(fileInput.parents('form:first'));
fileInput.fileupload({
fileInput: fileInput,
maxFileSize: maxFS,
url: urlnumbnuts,
type: 'POST',
autoUpload: true,
formData: formdatanumbnuts,
paramName: 'file',
dataType: 'XML',
replaceFileInput: false,
add: function (e, data) {
$.each(data.files, function (index, file) {
if (file.size > maxFS) {
alert('Alas, the file exceeds the maximum file size of 10MB.');
form[0].reset();
return false;
} else {
data.submit();
return true;
}
});
},
progressall: function (e, data) {
var progress = parseInt(data.loaded / data.total * 100, 10);
progressBar.css('width', progress + '%')
},
start: function (e) {
progressBar.
css('background', 'orange').
css('display', 'block').
css('width', '0%').
text("Preparing...");
},
done: function(e, data) {
var key = $(data.jqXHR.responseXML).find("Key").text();
var url = '//' + hostnumbnuts + '/' + key;
var input = $('<input />', { type:'hidden', class:'appendedInput',
name: fileInput.attr('name'), value: url });
form.append(input);
progressBar.
css('background', 'green').
text("Ready");
},
fail: function(e, data) {
progressBar.
css("background", "red").
css("color", "black").
text("Failed");
}
});
});
} // function prepareUpload()
create.js.erb
$(".info").attr("data-formdata", '<%=raw #s3_direct_post.fields.to_json %>'); // don't use .data() to set attributes
$(".info").attr("data-url", "<%= #s3_direct_post.url %>");
$(".info").attr("data-host", "<%= URI.parse(#s3_direct_post.url).host %>");
$('.post_form')[0].reset();
$('.postText').val('');
application.js
//= require jquery-fileupload/basic
config/initializers/aws.rb
Aws.config.update({
region: 'us-east-1',
credentials: Aws::Credentials.new(ENV['AWS_ACCESS_KEY_ID'], ENV['AWS_SECRET_ACCESS_KEY']),
})
S3_BUCKET = Aws::S3::Resource.new.bucket(ENV['S3_BUCKET'])
Notes:
This solution is designed for multiple post forms on the index.html.erb page. This is why the #s3_direct_post information is placed inside a div of class info inside index.html.erb, rather than in each post form. This means there is only one #s3_direct_post presented on the page at any one time, irrespective of the number of forms on the page. The data inside the #s3_direct_post is only grabbed (with a call to prepareUpload()) upon clicking the file upload button. Upon submission a fresh #s3_direct_post is generated in the posts controller, and the information inside .info is updated by create.js.erb. Storing the #s3_direct_post data inside the form means many different instances of #s3_direct_post can exist at once, leading to errors with the file name generation.
You need to :set_s3_direct_post in both the posts controller index action (ready for the first upload) and the create action (ready for the second and subsequent uploads).
Normal form submission is prevented by e.preventDefault(); so it can be done 'manually' with $.ajax({. Why not just use remote: true in the form? Because in Rails, file upload is done with an HTML request and page refresh even when you try to do it remotely.
Use info.attr() rather than info.data() to set and retrieve the #s3_direct_post attributes because info.data doesn't get updated
(for example see this question). This means you also have to manually parse the attribute into an object using jQuery.parseJSON() (which .data() actually does automatically).
Don't use //= require jquery-fileupload in application.js. This bug was a real ballache to identify (see here). The original Heroku solution didn't work until I changed this.
You can use Paperclip to upload to S3 (see documentation) and to create thumbnails, although it uploads to temporary folder first, after that image processing can be applied before uploading file to S3.
As for the examples of such configuration, there are plenty of them throughout the blogosphere and on StackOverflow, e.g. this.