How can I parse a local CSV file with Rails? - ruby-on-rails

I'm trying to use smarter_csv to parse csv files with my Rails app. But the documentation only explains how to parse a file that already belongs to the app.
I want to parse a file that's stored locally on my computer. So I think I have to upload the file, parse it, and then delete it.
This is how far I got:
<%= form_tag({action: :upload}, multipart: true) do %>
<%= file_field :csv %>
<%= submit_tag 'Submit' %>
<% end %>
So then how can I reference and use the uploaded file in my controller action?
def upload
#save file temporarily to app
filename = #filename
#parse file with smarter_csv
#File.delete(filename)
end

To get the file path as a string you need to do the following:
filename = params[:csv].path
as params[:csv] is a UploadedFile object. You don't need handle the temp file yourself, i.e. storing and deleting it. Rails would do that for you. As per documentation:
Uploaded files are temporary files whose lifespan is one request. When the object is finalized Ruby unlinks the file, so there is no need to clean them with a separate maintenance task.

Related

Rails Active Storage without model

Can Rails Active Storage be used without a model backing it? I have a form where I need a file to be uploaded but I don't wish it to be attached to a model. I just need the file uploaded so I can process it with a background job and then delete it.
Yes.
When ActiveStorage is backed by a model, there are two parts involved:
An ActiveStorage::Blob record that holds the file info in the active_storage_blobs table
An ActiveStorage::Attachment record that connects the blob to the model using the active_storage_attachments table
You can skip creating the ActiveStorage::Attachment record if you call create_and_upload! directly. This will create a unique key, determine the content type, compute a checksum, store an entry in active_storage_blobs, and upload the file:
filename = 'local_file.txt'
file = File.open(filename)
blob = ActiveStorage::Blob.create_and_upload!(io: file, filename: filename)
You can download later with:
blob.download
And delete with:
blob.purge
If you want to skip the storage of the ActiveStorage::Blob entirely, you would need to go go directly to the storage service that manages uploading and downloading files. For example, if you are using Disk storage you'd see something like this:
ActiveStorage::Blob.service
=> #<ActiveStorage::Service::DiskService ...
Then you'd have to generate your own key and do something like:
service = ActiveStorage::Blob.service
key = 'some_unique_key'
service.upload(key, file)
service.download(key)
service.delete(key)
To upload a file without a model you can use form_with and specify a url like:
<%= form_with url: "/uploads", multipart: true do |form| %>
<%= form.file_field :picture %>
<%= form.submit %>
<% end %>
Then on the server you can get the file with:
file = params[:picture]
blob = ActiveStorage::Blob.create_and_upload!(io: file, filename: file.original_filename)
...

Render a Markdown file stored in S3 uploaded with shrine - Rails

I'm using shrine to upload files from my rails application to S3. All is working fine, but I dont know how to display that file using redcarpet gem.
For example I can do this:
<div>
<%= markdown("##title
* ") %>
</div>
And works fine.
But if I do this:
<%= markdown(#rfile.rfile.url) %>
Is showing me a download link from S3.
How I can get the file content and not the file link?
Call to #rfile.rfile returns a Shrine::UploadedFile object, which has many more convenient methods other than just #url. On such method is #read, which retrieves content of the file:
<%= markdown(#rfile.rfile.read) %>
However, in this case the file would be opened and read, but not closed. So it's better to call #open with a block, and call #read on the yielded IO object, which can be neatly written as
<%= markdown(#rfile.rfile.open(&:read)) %>

is there a rails method to loop through each line of uploaded file? "each_line" is an IO method but it's not working

I'm attempting to upload a csv file, parse it, and spit out a file for S3 or just pass to view. I use a file_field_tag to upload the csv. I thought file_field_tag passes an object that is a subclass of IO and would have all ruby IO methods such as "each_line". I can call "read" on the object (method of IO class) but not "each_line"... so how can I iterate over each line of a file_field_tag upload?
create method of my controller as:
#csv_file = params[:csv_file]
My show view which throws a no "each_line" method error:
<% #csv_file.each_line do |line| %>
<%= line %>
<% end %>
Yet I can use
<%= #csv_file.read(100) %>
I'm really confused what methods a file_field_tag upload params[] has... each_line, gets don't work... I can't seem to find a list of what I can use.
EDIT
I worked around this by doing:
#csv_file = params[:csv_file].read.to_s
then iterated through with:
<% #sp_file.each_line do |line| %>
<%= line %>
<% end %>
EDIT 2
The file being uploaded has repeats the header after lines which don't contain a comma (don't ask)... So I find lines without a comma and call .gets (in my rb script independent of rails). Unfortunately I get an error about gets being a private method I can't call. Which goes back to my initial issue being. Aren't files a sub class of IO with IO methods like read_lines & gets?
#file_out = []
#file_in.each_line do |line|
case line
when /^[^,]+$/
#comp = line.to_s.strip
comp_header = #file_in.gets.strip.split('')
#file_out.push(#comp)
end
end
When you post a 'file_field' , the param returned to the controller has some special magic hooked in.
I.e. in your case you could this
<%= "The following file was uploaded #{params[:csv_file].original_filename}" %>
<%= "It's content type was #{params[:csv_file].content_type}" %>
<%= "The content of the file is as follows: #{params[:csv_file].read}" %>
So those are the three special methods you can call on params[:csv_file], or any params posted as the result of a successful 'file_field_tag' or 'f.file_field' in a view
Just remember that those are the three extra special things you can to to a param posted as a result of a file_field:
original_filename
content_type
read
You've clearly figured out how to do the read, the original_filename and content_type may help you out in the future.
EDIT
OK, so all you have is the read method, which will read the contents of the file uploaded.
contents = params[:csv_file].read
So now contents is a string containing the contents of the file, but nothing else is known about that file EXCEPT that it's a csv file. We know that csvs are delimited with '\r' (I think, I've done a lot of work with parsing csv's, but I'm too lazy to go check)
so you could do this:
contents = params[:csv_file].read
contents.split("\r").each do |csvline|
???
end
EDIT 2
so here is the take away from this post
When you post a file_field to a controller, the most common thing to do with the contents of the uploaded file is 'read' it into a ruby String. Any additional processing of the uploaded contents must be done on that ruby String returned from the 'read'.
In this particular case, if the uploaded file is ALWAYS a CSV, you can just assume the CSV and start parsing it accordingly. If you expect multiple upload formats, you have to deal with that, for example:
contents = params[:csv_file].read
case params[:csv_file].content_type
when 'txt/csv'
contents.split("\r").each do |csvline|
???
end
when 'application/pdf'
???
end

Carrierwave Temporary File Without Model

I need to be able to attach a file to a mail (using Mailer) for a recently uploaded file which is not linked to any Model.
In the code that goes for the upload form:
<%= form_for(:mail, :url => {:action => 'send_mail'}, :html => {:multipart => true}) do |f| %>
<table summary="send_table">
<tr>
<th>Attachment</th>
<td><%= f.file_field(:attachment) %><a id="attachment"></a></td>
</tr>
</table>
<%= submit_tag "Send!" %>
Now, what I am looking into doing in the send_mail action is something like:
MyMailer.send_mail(params[:mail][:attachment]).deliver
with params[:mail][:attachment] being the path to the temp file uploaded with the form. How can one do that?
This also implies another question: Should I manually delete the file from the temp once the mail is sent? If yes, how?
Copying the answer from the comments in order to remove this question from the "Unanswered" filter:
Finally nailed it:
unless (params[:mail][:attachment]).nil?
uploader = AttachmentUploader.new
uploader.cache!(params[:mail][:attachment])
#file_name = uploader.filename
#file_data = uploader.file.read()
end
and then
MyMailer.send_mail(#file_name,#file_data)
~ answer per user1563325
I'd like to add that for the scenario described here, maybe you don't need CarrierWave for such a temporary upload situation. When uploading using file_field, Rails saves it into a Tempfile, whose path you can access like this:
params[:mail][:attachment].path
These files are deleted automatically so you shouldn't need to worry about them, as the Rails docs explain:
Uploaded files are temporary files whose lifespan is one request. When the object is finalized Ruby unlinks the file, so there is no need to clean them with a separate maintenance task.

file upload without activerecord

How do you handle file upload in rail without attaching them to active record ?
I just want to write the files to the disk.
Thanks,
If I understand correctly what you need then the most simple example would be this:
The controller:
class UploadController < ApplicationController
def new
end
def create
name = params[:upload][:file].original_filename
path = File.join("public", "images", "upload", name)
File.open(path, "wb") { |f| f.write(params[:upload][:file].read) }
flash[:notice] = "File uploaded"
redirect_to "/upload/new"
end
end
The view:
<% flash.each do |key, msg| %>
<%= content_tag :div, msg, :class => [key, " message"], :id => "notice_#{key}" %>
<% end %>
<% form_tag '/upload/create', { :multipart => true } do %>
<p>
<%= file_field_tag 'upload[file]' %>
</p>
<p>
<%= submit_tag "Upload" %>
</p>
<% end %>
This would let you upload any file without any checks or validations which in my opinion isn't that usefull.
If I would do it myself then I would use something like validatable gem or tableless gem just tableless is not supported anymore. These gems would allow you to validate what you're uploading to make it more sane.
You can just move the temporary file to destiny path using FileUtils
tmp = params[:my_file_field].tempfile
destiny_file = File.join('public', 'uploads', params[:my_file_field].original_filename)
FileUtils.move tmp.path, destiny_file
The Tempfile documentation shows an example that's equivalent to Rytis's code, which is fine most of the time. But when you call tempfile.read, Ruby is reading the whole file as a single chunk into memory, which is sub-optimal.
However, FileUtils provides a copy_stream method, and IO, at least in Ruby 2.0, provides a copy_stream implementation that handles writing directly to a filepath (FileUtils.copy_stream requires File-like objects on both sides, or so say the docs).
In my case, I was initiating a large multi-file upload via AJAX, and wanted to avoid reading the whole file(s) into Ruby's memory before writing to disk.
In the example below, params[:files] is an Array of ActionDispatch::Http::UploadedFile instances, and local_filepath is a string pointing to a non-existing file in an existing directory. For brevity, I'll assume I'm only uploading one file:
IO.copy_stream(params[:files][0].tempfile, local_filepath)
The ActionDispatch::Http::UploadedFile instance has a .tempfile field that's just a regular Tempfile instance.
I'm not actually sure that Ruby still isn't reading the whole file into memory—I didn't benchmark anything—but it's a lot more possible than it is with the localfile.write(tempfile.read) syntax.
tl;dr: IO.copy_stream(your_tempfile, your_disk_filepath) is more concise, if not faster.
You could try using the Rails plugin Attachment_fu to handle file uploads. It allows you to save uploads to the file system instead of the database.

Resources