Uploading a file in Rails - ruby-on-rails

I'm new to rails, and I'm writing a RESTful website using the CRUD technique. So far I have created three pages, all of which allow the user to create, edit, and delete a row from the database. However, my fourth page will need to include an upload file form, but a) I don't know how the filesystem works with Rails thus I don't know where files should be stored. The file would be around 100kb and couldn't be stored in temporary storage because it will be constantly downloaded. And b) I don't know how to write to a file.
It would be great if you could tell me how to do what I mentioned above - create an upload input on an input form, and to then write the file to a filepath in a separate directory.

Update 2018
While everything written below still holds true, Rails 5.2 now includes active_storage, which allows stuff like uploading directly to S3 (or other cloud storage services), image transformations, etc. You should check out the rails guide and decide for yourself what fits your needs.
While there are plenty of gems that solve file uploading pretty nicely (see https://www.ruby-toolbox.com/categories/rails_file_uploads for a list), rails has built-in helpers which make it easy to roll your own solution.
Use the file_field-form helper in your form, and rails handles the uploading for you:
<%= form_for #person do |f| %>
<%= f.file_field :picture %>
<% end %>
You will have access in the controller to the uploaded file as follows:
uploaded_io = params[:person][:picture]
File.open(Rails.root.join('public', 'uploads', uploaded_io.original_filename), 'wb') do |file|
file.write(uploaded_io.read)
end
It depends on the complexity of what you want to achieve, but this is totally sufficient for easy file uploading/downloading tasks. This example is taken from the rails guides, you can go there for further information: http://guides.rubyonrails.org/form_helpers.html#uploading-files

Sept 2018
For anyone checking this question recently, Rails 5.2+ now has ActiveStorage by default & I highly recommend checking it out.
Since it is part of the core Rails 5.2+ now, it is very well integrated & has excellent capabilities out of the box (still all other well-known gems like Carrierwave, Shrine, paperclip,... are great but this one offers very good features that we can consider for any new Rails project)
Paperclip team deprecated the gem in favor of the Rails ActiveStorage.
Here is the github page for the ActiveStorage & plenty of resources are available everywhere
Also I found this video to be very helpful to understand the features of Activestorage

There is a nice gem especially for uploading files : carrierwave. If the wiki does not help , there is a nice RailsCast about the best way to use it . Summarizing , there is a field type file in Rails forms , which invokes the file upload dialog. You can use it , but the 'magic' is done by carrierwave gem .
I don't know what do you mean with "how to write to a file" , but I hope this is a nice start.

Okay. If you do not want to store the file in database and store in the application, like assets (custom folder), you can define non-db instance variable defined by attr_accessor: document and use form_for - f.file_field to get the file,
In controller,
#person = Person.new(person_params)
Here person_params return whitelisted params[:person] (define yourself)
Save file as,
dir = "#{Rails.root}/app/assets/custom_path"
FileUtils.mkdir(dir) unless File.directory? dir
document = #person.document.document_file_name # check document uploaded params
File.copy_stream(#font.document, "#{dir}/#{document}")
Note, Add this path in .gitignore & if you want to use this file again add this path asset_pathan of application by application.rb
Whenever form read file field, it get store in tmp folder, later you can store at your place, I gave example to store at assets
note: Storing files like this will increase the size of the application, better to store in the database using paperclip.

In your intiallizer/carrierwave.rb
if Rails.env.development? || Rails.env.test?
config.storage = :file
config.root = "#{Rails.root}/public"
if Rails.env.test?
CarrierWave.configure do |config|
config.storage = :file
config.enable_processing = false
end
end
end
use this to store in a file while running on local

Related

Rails Active Storage set folder to store files

I'm using Active Storage to store files in a Rails 5.2 project. I've got files saving to S3, but they save with random string filenames and directly to the root of the bucket. I don't mind the random filenames (I actually prefer it for my use case) but would like to keep different attachments organized into folders in the bucket.
My model uses has_one_attached :file. I would like to specify to store all these files within a /downloads folder within S3 for example. I can't find any documentation regarding how to set these paths.
Something like has_one_attached :file, folder: '/downloads' would be great if that's possible...
The ultimate solution is to add an initializer. You can add a prefix based on an environment variable or your Rails.env :
# config/initializer/active_storage.rb
Rails.configuration.to_prepare do
ActiveStorage::Blob.class_eval do
before_create :generate_key_with_prefix
def generate_key_with_prefix
self.key = if prefix
File.join prefix, self.class.generate_unique_secure_token
else
self.class.generate_unique_secure_token
end
end
def prefix
ENV["SPACES_ROOT_FOLDER"]
end
end
end
It works perfectly with this. Other people suggest using Shrine.
Credit to for this great workaround : https://dev.to/drnic/how-to-isolate-your-rails-blobs-in-subfolders-1n0c
As of now ActiveStorage doesn't support that kind of functionality. Refer to this link. has_one_attached just accepts name and dependent.
Also in one of the GitHub issues, the maintainer clearly mentioned that they have clearly no idea of implementing something like this.
The workaround that I can imagine is, uploading the file from the front-end and then write a service that updates key field in active_storage_blob_statement
There is no official way to change the path which is determined by ActiveStorage::Blob#key and the source code is:
def key
self[:key] ||= self.class.generate_unique_secure_token
end
And ActieStorage::Blog.generate_unique_secure_token is
def generate_unique_secure_token
SecureRandom.base36(28)
end
So a workaround is to override the key method like the following:
# config/initializers/active_storage.rb
ActiveSupport.on_load(:active_storage_blob) do
def key
self[:key] ||= "my_folder/#{self.class.generate_unique_secure_token}"
end
end
Don't worry, this will not affect existing files. But you must be careful ActiveStorage is very new stuff, its source code is variant. When upgrading Rails version, remind yourself to take look whether this patch causes something wrong.
You can read ActiveStorage source code from here: https://github.com/rails/rails/tree/master/activestorage
Solution using Cloudinary service
If you're using Cloudinary you can set the folder on storage.yml:
cloudinary:
service: Cloudinary
folder: <%= Rails.env %>
With that, Cloudinary will automatically create folders based on your Rails env:
This is a long due issue with Active Storage that seems to have been worked around by the Cloudinary team. Thanks for the amazing work ❤️
# config/initializers/active_storage.rb
ActiveSupport.on_load(:active_storage_blob) do
def key
sql_find_order_id = "select * from active_storage_attachments where blob_id = #{self.id}"
active_storage_attachment = ActiveRecord::Base.connection.select_one(sql_find_order_id)
# this variable record_id contains the id of object association in has_one_attached
record_id = active_storage_attachment['record_id']
self[:key] = "my_folder/#{self.class.generate_unique_secure_token}"
self.save
self[:key]
end
end
Active Storage by default doesn't contain a path/folder feature but you can override the function by
model.file.attach(key: "downloads/filename", io: File.open(file), content_type: file.content_type, filename: "#{file.original_filename}")
Doing this will store the key with the path where you want to store the file in the s3 subdirectory and upload it at the exact place where you want.

How to implement a user creating a file via form and then downloading it in rails?

I am building an online translation platform. When a job is done being translated and saved, I want the user to be able to download the translated version as a text file. It is currently saved in a string in the model called "target_text".
I know in ruby I can use this method:
File.open("translation.txt", 'w') {|f| f.write("my translated string") }
I am assuming I could tack the location for the file to be saved in front of the "translation.txt", but I am not sure what folder within my app I should specify?
Furthermore I want this file to be attached to the "job" object in the same way that paperclip can attach files, the difference being it's initiated server side. How should I go about this?
I have googled all over looking for an answer to this, and I want to make sure I do it in the cleanest way possible. I would really appreciate even directions to a good place to look to understand this concept.
I don't quite understand the question, but I hope this could help...
Instead of using
File.open("translation.txt", 'w') {|f| f.write("my translated string") }
try using the following
Tempfile.open(['translation', '.txt'], Rails.root.join('tmp')) do |file|
# this will create a temp file in RAILS_ROOT/tmp/ folder
# you can replace the 'translation' text part to any auto generated text for example
# Tempfile.open([#user.id.to_s, '_translation.txt'] will create
# RAILS_ROOT/tmp/1_translation.1fe2ed.txt
# the 1fe2ed is generated by Tempfile to avoid conflicting
begin
file << "my translated string"
# this creates the file
# add all the processing you need here... cause the next ensure block
# will close and delete this temp file... so that the tmp dir doesn't get big.
# you can for example add the file to paperclip attachment
#user.translation = file
# assuming that user has paperclip attachment called translation
ensure
# close and delete file
file.close
file.unlink
end
end
also check the Tempfile docs... this is the practice i've been using... not sure if it's the best or not.. but it didn't create any issues so far
(even with paperclip s3 storage)

Parsing rails uploaded temporary file more then once

I'm new to rails and I'm currently trying to parse an uploaded file to rails. However, after I "read" the file once I cannot read it again. From what I've read online it appears that rails immediately deletes the uploaded file. Is there a way to make the file persistent? My code is as follows
file_param = params[:sequence]
file_param.read.each do |l|
# do stuff
end
file_param.read.each do |l|
# do stuff again. this is not being called.
end
I've thought of using paperclip or some other storage gem, but I don't need to store the files, simply read their contents. Thanks!
Read it into an array, if you really need to go over it multiple times, or just save it.

How to make carrierwave delete the file when destroying a record?

I'm using the carrierwave gem to upload files.
I have built a system for users to flag images as inappropriate and for admins to remove the images. From what I can tell, calling destroy on the image will only remove the path name from the table.
Is there a way to have carrierwave actually remove the file itself? Or should rails automatically remove the file when I destroy the image path?
Like #mu_is_too_short said, you can use File#delete.
Here's a code snippet you could use as a helper with a little tweaking in your rails app.
def remove_file(file)
File.delete(file)
end
or if you just have the filename stored in file
def remove_file(file)
File.delete("./path/to/#{file}")
end
Not sure what CarrierWave offers for this, but you could use FileUtils in the Ruby standard library with an ActiveRecord callback.
For instance,
require 'FileUtils'
before_destroy :remove_hard_image
def remove_hard_image
FileUtils.rm(path_to_image)
end
Sidenote: This code is from memory.
If one wants to delete a file but does not want to specify the full filename you can use the below.
Can also be used to delete many files or all files in a directory with a specific extension...
file = Rails.root.join("tmp", "foo*")
or
file = Rails.root.join("tmp", ".pdf")
files = Dir.glob(file) #will build an array of the full filepath & filename(s)
files.each do |f|
File.delete(f)
end

How do you access the raw content of a file uploaded with Paperclip / Ruby on Rails?

I'm using Paperclip / S3 for file uploading. I upload text-like files (not .txt, but they are essentially a .txt). In a show controller, I want to be able to get the contents of the uploaded file, but don't see contents as one of its attributes. What can I do here?
attachment_file_name: "test.md", attachment_content_type: "application/octet-stream", attachment_file_size: 58, attachment_updated_at: "2011-06-22 01:01:40"
PS - Seems like all the Paperclip tutorials are about images, not text files.
In Paperclip 3.0.1 you could just use the io_adapter which doesn't require writing an extra file to (and removing from) the local file system.
Paperclip.io_adapters.for(attachment.file).read
#jon-m answer needs to be updated to reflect the latest changes to paperclip, in order for this to work needs to change to something like:
class Document
has_attached_file :revision
def revision_contents(path = 'tmp/tmp.any')
revision.copy_to_local_file :original, path
File.open(path).read
end
end
A bit convoluted as #jwadsack mentioned using Paperclip.io_adapters.for method accomplishes the same and seems like a better, cleaner way to do this IMHO.
To access the file you can use the path method:
csv_file.path
http://rdoc.info/gems/paperclip/Paperclip/Attachment#path-instance_method
This can be used along with for example the CSV reader.
Here's how I access the raw contents of my attachment:
class Document
has_attached_file :revision
def revision_contents
revision.copy_to_local_file.read
end
end
Please note, I've omitted my paperclip configuration options and any sort of error handling.
You would need to load the contents of the file (using Rubys File.open) into a variable before you show it. This may be an expensive operation if your app gets lots of use, so it may be worthwhile reading the contents of the file and putting it into a text column in your database after uploading it.
Attachment already inherits from IOStream. http://rdoc.info/github/thoughtbot/paperclip/master/Paperclip/Attachment
So it should just be "#{attachment}" or <% RDiscount.new(attachment).to_html %> or send_data(attachment). However you wanted to display the data.
This is a method I used for upload from paperclip to active storage and should provide some guidance on temporarily working with a file in memory. Note: This should only be used for relatively small files.
Written for gem paperclip 6.1.0
Where I have a simple model
class Post
has_attached_file :image
end
Working with a temp file in ruby so we do not have to worry about closing the file
Tempfile.create do |tmp_file|
post.image.copy_to_local_file(nil, tmp_file.path)
post.image_temp.attach(
io: tmp_file,
filename: post.image_file_name,
content_type: post.image_content_type
)
end

Resources