In rails, I want to send email using Action Mailer with attachment that is obtained from form file field and want to delay it through sidekiq.
And I have written code as below.
In view:
<%= form_tag({ controller: 'my_controller', action: 'my_mail', method: 'post' }, { multipart: true }) do %>
<%= form_field_tag(:attachment) %>
<% end %>
In controller:
def my_mail
MyMailer.delay.my_mail(params)
end
In Mailer:
def my_mail(message)
attachments['attachment'] = File.read(message[:attachment].tempfile)
mail(from: ENV['MY_MAIL'], to: ENV['MAIL_RECIVER'], subject: 'this is subject')
end
But, IOError will be raised due to inaccessibility to the file.
And, I perform File read operation in controller as
def my_mail
MyMailer.delay.my_mail(File.read(params[:attachment].tempfile))
end
Now, I can make attachment in Mailer as
attachments['attachment'] = message
And Now, It does work as i want but It's very bad to read file in controller due to security reason.
So, Now I want to know the best way to attach the file obtained from form and send it through sidekiq.
In controller:
Its not good to send bulk objects like params and File object in redis-server via sidekiq. Lets make it simple
def my_mail
# get absolute path of temporary location uploaded file
attachment_tmp_path = File.absolute_path(params[:attachment].tempfile)
MyMailer.delay.my_mail(attachment_tmp_path)
end
In Mailer:
def my_mail(attachment_tmp_path)
attachments['attachment'] = File.read(attachment_tmp_path)
mail(from: ENV['MY_MAIL'], to: ENV['MAIL_RECIVER'], subject: 'this is subject')
end
Why security issue is warned?
It is not considered good to directly use params without using strong params to limit only permitted attributes.
Note:
Tmp uploaded files may not be available always as you have used sidekiq for background processing file uploaded by sidekiq-client may not be available when sidekiq-server (Background version) tries to access that tmp file when sidekiq-server being busy processed this task after very long long time.
Conditions your approach may not work:
When you goto to production and need to run multiple instance. Lets
say you need a separate utility instance to run sidekiq and
redis. Then your sidekiq-server cannot locate the tmp location
of application_master.
When you sidekiq-server is busy in
processing or is down for some time and resumes after long long time
Related
I have a message controller/model/view, where user can send message to each other. I have added an attachment column for user to upload files when creating their message. I would like the recipient of the message to be able to download the attached file from S3 to his/her default download folder.
I have read several post, but none of them seem to address the dynamic angle.
Below is where I am now, in my controller:
def download_url(attachment_file_name)
s3 = AWS::S3.new.buckets[Rails.application.secrets.s3_bucket_name] # This can be done elsewhere as well,
# e.g config/environments/development.rb
url_options = {
expires_in: 60.minutes,
use_ssl: true,
response_content_disposition: "attachment; filename=\"#{attachment_file_name}\""
}
s3.objects[ self.path ].url_for( :read, url_options ).to_s
end
and in the view:
<%= link_to 'Download attachment', download_url(#message.attachment) %>
But this returns the error:
ActionView::Template::Error (undefined method `download_url' for #<#<Class:0x00007fee04a12648>:0x00007fedf5c1d4d8>):
Which kind of makes sense. Does anyone knows how to do this please, i am surprise something that simple gets complex to me. help please - 4 hours in :(
I am trying to send an sms to a number entered in an input field using Nexmo gem
This is what I have so far and it doesn't seem to work
pages/test.html.erb
<%= form_tag "/pages/send_sms" do -%>
<%= text_field_tag "number" %>
<%= submit_tag "Send" %>
<% end -%>
routes.rb
Rails.application.routes.draw do
get 'pages/home'
post '/pages/send_sms', as: 'send_sms'
get 'test', to: 'pages#test'
root 'pages#home'
# For details on the DSL available within this file, see http://guides.rubyonrails.org/routing.html
end
pages_controller.rb
def send_sms
#number = params[:number]
nexmo = Nexmo::Client.new(
key: ENV['NEXMO_API_KEY'],
secret: ENV['NEXMO_API_SECRET']
)
notification = "Download the app through this link"
response = nexmo.send_message(
from: "GLAM360",
to: params['number'],
text: notification
)
if response['messages'].first['status'] == '0'
redirect_to root_path
end
end
This is what I see in the terminal
Started POST "/pages/send_sms" for 127.0.0.1 at 2017-10-08 00:35:45 +0400
Processing by PagesController#send_sms as HTML
Parameters: {"utf8"=>"✓", "authenticity_token"=>"zPj5PcZrD+uNYxvvfDio8B5uNWitg0vMw+3Vm8KbvQumbNWzsgN4sBJDKsi2srx0rSatiOISegWHQFE860
JxcA==", "number"=>"+971585959698", "commit"=>"Send"}
No template found for PagesController#send_sms, rendering head :no_content
Completed 204 No Content in 803ms
Any help will be highly appreciated here
The error itself tells you what to do. You have few options.
First for your case, just add another redirect if the last check
condition fails. In your case, its failing and so asking for default template. If you specify an else clause, in which you
describe where should it go to (say to the form again with an alert message), rails will itself take care of that.
if response['messages'].first['status'] == '0'
redirect_to root_path
else
redirect_to test_path
end
I used your script and added a send_sms.html.erb with a status
variable being passed from the controller according to the response
of the nexmo.send_message function. Like "Success" or failure and
everything works fine, on the webpage it informs me about the status and I received a text message as well. This is what I will prefer for a better UX.
.
If I have to do it, there would be a lot of changes I would do to the script. A suggestion would be to never rely on an external API, always wrap the interactions you do to an external API in an interface. I will wrap the Nextio scripts, take it out of the controller and place them in an interface (Have a class in either lib or a wrapper function in helper and use interface to interact with it) and will interact with it through my controller. Try not to put logic in your controller. I would also use begin rescue block for interactions with external API and will read about all the errors and think about how to handle them. Lastly for the flow, I would give users more information and not keep them hanging. I would redirect them to another page like send_sms and give them status or use alert messages to tell them whats the status.
Lastly, I would use background tasks for these jobs (which can take sometime depending upon external servers). Use something like sidekiq.
I want to call redirect_to function in the Delayed job.
I have file print_job.rb
class PrintJob < Struct.new(:device,:survey,:filter_tags,:day1,:day2,:lastsurvey, :scale)
def perform
pc = PagesController.new
pc.redirect_to "http://google.com"
#pc.redirect_to :action => "print", :format => "pdf", :device => device, :survey => survey, :filter_tags => filter_tags,
#:day1 => day1, :day2 => day2, :lastsurvey => lastsurvey, :scale => scale
end
def success(job)
end
end
but it does not work. If I just put redirect_to "htpp://google.com". It said that no method redirect_to
The current situation now is I am using pdf kit with delayed_job but not exporting a pdf file directly. I have link locahost/print If i want to open it in format pdf, just put localhost/print.pdf or using link_to :action => "print", :format => "pdf" But if the pdf file is big, it does not have well performance so I want to open localhost/print.pdf in delayed_job. So how can I solve this problem
So how can I call redirect_to in delayed job now? Thank you!
delayed_job, are designed run long processes in background with out interrupting the http request of the rails controller. So it works like
1) controller gets a request
2) controller calls the delayed job and pass the 'long running'
process
3) controller move on without waiting for reply from delayed job
So in that context, what you asked is kind of 'strange', however , if your requirement is to notify the user, etc.. once the process is completed,
a) update a database column by the delayed_job and read it from your view
b) use some kind of a server-push method, its built-in to rails4, or using a gem like Faye
HTH
As mentioned above you could just use the database (one status column) and proceed that way (worker updates column, page some how auto refresh and get column values constantly) but right now at this time, i would highly recommend the use of active cable (which right now is part of rails 5+)
With active cable you can easily create a communication channel between your views or controllers and any worker, all you have to do is to setup a channel for your user and then send the messages from the worker while you already have a the listener (view with js code) that will get broadcast messages from the channel common channel (common between the view and worker using the same name/id etc this is really just a string representing the channel name, the js code (front end) will have it as well as the rails (back end) config)
Hope this helps!
I need to have a button to save the current web site (just like clicking on "Save as"), I created a method in the controller which works great for any external site (like http://www.google.com) but doesn't work for the sites inside my application, I get a timeout error!. This has no explanation to me :(
Any clue what is the issue?
#CONTROLLER FILE
def save_current_page
# => Using MECHANIZE
agent = Mechanize.new
page = agent.get request.referer
send_data(page.content, :filename => "filename.txt")
end
I tried also Open URI, same problem!
#CONTROLLER FILE
def save_current_page
# => USANDO OPEN URI
send_data(open(request.referer).read, :filename => "filename.txt")
end
I'm using rails 3.2 and ruby 1.9, any help is appreciated, I already spent like 10 hours trying to make it work!!
Rails can only handle one request at a time. It's a never-ending standoff between the two requests - the first request is waiting for the second request, but the second request is waiting for the first request, and therefore you get a Timeout error. Even if you're running multiple instances of the app with Passenger or something, it's a bad idea.
The only way I can think to get around it would be to use conditional statements like so:
referer = URI.parse(request.referer)
if Rails.application.config.default_url_options[:host] == referer.host
content = "via yoursite.com"
else
agent = Mechanize.new
page = agent.get request.referer
content = page.content
end
send_data content, filename: "filename.txt"
A little dirty but it should get around the Timeout problem. As far a getting the actual content of a page from your own site - that's up to you. You could either render the template, grab something from cache, or just ignore it.
A much better solution would be to enqueue this code into something like Resque or Delayed Job. Then the queue could make the request and wait in line to request the page like normal. It would also mean that the user wouldn't have to wait while your application make a remote request, which is dangerous because who knows how long the page will take to respond.
After several hours and lots of other posts I got to a final solution:
Bricker is right in that it is not possible for rails to render more than once in a call, as taken from http://guides.rubyonrails.org/layouts_and_rendering.html "Can only render or redirect once per action"
The site also states "The rule is that if you do not explicitly render something at the end of a controller action, Rails will automatically look for the action_name.html.erb template in the controller’s view path and render it."
Then, the solution that worked great for me was to tell the controller to render to a string if a download flag (download=true) was set in :params (I also use request.url to have it working from any view in my application)
View:
= link_to 'Download', request.url+"&downloadexcel=true", :class => 'btn btn-primary btn-block'
Controller:
def acontrolleraction
#some controller code here
if params[:downloadexcel]
save_page_xls
else
# render normally
end
end
def save_page_xls
#TRESCLOUD - we create a proper name for the file
path = URI(request.referer).path.gsub(/[^0-9a-z]/i, '')
query = URI(request.referer).query.gsub(/[^0-9a-z]/i, '')
filename = #project_data['NOMBRE']+"_"+path+"_"+query+".xls"
#TRESCLOUD - we render the page into a variable and process it
page = render_to_string
#TRESCLOUD - we send the file for download!
send_data(page, :filename => filename, :type => "application/xls")
end
Thanks for your tips!
For the life of my I can't understand how the basic paperclip example works. There's only one line included in the controller, and that's
#user = User.create( params[:user] )
I simply don't understand how that's all that is needed to upload an image to s3. I've changed the example quite a bit because i wanted to use jquery file uploader rather than the default rails form helper, so I'm at the point where an image is being POSTed to my controller, but I can't figure out how I'm supposed to take the image from the params and assign it as an attachment. Here's what I'm seeing the logs:
Parameters: {"files"=>[#<ActionDispatch::Http::UploadedFile:0x132263b98 #tempfile=#<File:/var/folders/5d/6r3qnvmx0754lr5t13_y1vd80000gn/T/RackMultipart20120329-71039-1b1ewde-0>, #headers="Content-Disposition: form-data; name=\"files[]\"; filename=\"background.png\"\r\nContent-Type: image/png\r\n", #content_type="image/png", #original_filename="background.png">], "id"=>"385"}
My JS is very simple:
` $('#fileupload').fileupload({
dataType: 'json',
url: '/my_url',
done: function (e, data) {
console.log('done');
}
});`
What would be helpful for me to know is how I can strip the file data from the POSTed parameters given above and pass it to paperclip. I'm sure that I'll have to assign the attachment attribute a value of File.open(...), but I dont know what source of my file is.
I've spent a ridiculous amount of time trying to figure this out and I can't seem to get it. I've tried uploading directly to s3, but the chain of events was terribly confusing, so I want to get this simple pass-through example completed first. Thanks so much for any help you cna give!
You need a few more pieces and it will help if you can show the exact code you're using.
Paperclip can post to S3 by using:
http://rubydoc.info/gems/paperclip/Paperclip/Storage/S3
When your controller creates a User model, it is sending along all the params. This is called "mass assignment" (be sure to read about attr_accessible).
When your model receives the params, it uses the Paperclip AWS processor, which uploads it.
You need the AWS gem, a valid bucket on S3, and a config file.
Try this blog post and let us know if it helps you:
http://blog.trydionel.com/2009/11/08/using-paperclip-with-amazon-s3/
UPDATE 2013-04-03: Pleases see Chloe's comment below-- you may need an additional parameter, and the blog post may be outdated.
If you want to do it manually, approach it like this:
# In order to get contents of the POST request with the photo,
# you need to read contents of request
upload = params[:file].is_a(String)
file_name = upload ? params[:file] : params[:file].original_filename
extension = file_name.split('.').last
# We have to create a temp file which is going to be used by Paperclip for
# its upload
tmp_file = "#{Rails.root}/tmp/file.#{extension}"
file_id = 0
# Check if file with the name exists and generate unique path
while File.exists?(tmp_file) do
tmp_file_path = "#{Rails.root}/tmp/file#{file_id}.#{extension}"
id += 1
end
# Let's write the file from post request to unique location
File.open(tmp_file_path, 'wb') do |f|
if upload
f.write request.body.read
else
f.write params[:file].read
end
end
# Now that file is saved in temp location, we can use Paperclip to mimic one file
# upload
#photo = Photo.new :photo => File.open(tmp_file_path)
# We'll return javascript to say that the file is uploaded and put its thumbnail in
# HTML or whatever else you wanted to do with it
respond_to do |format|
if #photo.save
render :text => "Success"
else
render :text => #photo.errors
end
end
You can rewrite your create or whatever you use as the url to which you are POSTing the form.
This bit:
"files"=>[#<ActionDispatch::Http::UploadedFile:0x132263b98 #tempfile=# <File:/var/folders/5d/6r3qnvmx0754lr5t13_y1vd80000gn/T/RackMultipart20120329-71039-1b1ewde-0>
is the part (I think) that holds the file contents that are posted in the form.
In Rails, the User model will have a helper: has_attached_file
Passing the [:params] to the User.create method allows that helper to pick up the file contents, do any processing on them (eg resizing etc based on attributes supplied to the helper) and then push the image(s) to your storage (eg S3 or whatever - S3 credentials are passed to the helper).
Hopefully that explains the 'how does it do it?' question
re the jQuery bit.. not sure what the code should be there, but why not use the Rails form with :remote => true and handle the response in jquery?