Upload a Tempfile to Carrierwave in Rails - ruby-on-rails

I am trying to create a Tempfile using some user-inputted information in one of my models:
after_create :form_to_csv
def form_to_csv
temp_file = Tempfile.new('test_temp.csv', encoding: 'utf-8')
begin
self.input_data.split('\n').each do |line|
temp_file.write line
end
temp_file.rewind
self.build_input_data_file(file: Rails.root.join("temp_file.path}").open)
ensure
temp_file.close
temp_file.unlink
end
end
The form data is stored in the input_data field as text. I get to write my data to the Tempfile successfully but attaching it to the model is cumbersome - I keep trying to attach nil to my model. My main problem is that the file I generate has a path like:
/var/folders/jw/7pjlw_212qd3s4ddfj1zvnpr0000gn/T/test_temp.csv20170502-78762-1eh23ml
and it won't work with Rails.root.join and with the attachment method propose in CarrierWave docs here (Upload from a local file).

My mistake was that I tried to assign a .csv extension within the filename parameter of the Tempfile object. Instead I needed to pass an array like this:
def form_to_csv
temp_file = Tempfile.new(['test_temp', '.csv'], encoding: 'utf-8')
begin
self.input_data.split('\n').each do |line|
temp_file.write line
end
temp_file.rewind
self.build_input_data_file(file: Rails.root.join(temp_file.path).open)
ensure
temp_file.close
temp_file.unlink
end
end

Related

ActiveStorage download File / IO from attachment object on S3 service?

After the file is uploaded, I want to analyze and immediately process.
I'm currently attaching then processing each:
current_account.archives.attach(archive_params)
current_account.archives.each do |archive|
Job.enqueue(AccountArchiveImportJob.new(current_account.id, archive.id))
end
In the job i'm opening the CSV and parsing junk
attachment = Account.find(account_id).archives.where(id: archive_id).first
CSV.parse(attachment.download) do |row|
do_stuff_with_the_row(row)
end
I would like to do something like:
CSV.foreach(attachment.open) do |row|
do_stuff_with_the_row(row)
end
I cannot find documentation that allows converting the attachment back into a FILE
At least from Rails 6.0 rc1:
model.attachment_changes['attachment_name'].attachable
will give you IO of the original TmpFile BEFORE it is uploaded.
Rails-6 we will get a download method that will yield a file but you can get this very easily!
Add this downloader.rb file as an initializer
Then given this model
class Business < ApplicationRecord
has_one_attached :csvfile
end
you can do
ActiveStorage::Downloader.new(csvfile).download_blob_to_tempfile do |file|
CSV.foreach(file.path, {headers: true}) do |row|
do_something_with_each_row(row.to_h)
end
end
EDIT: not sure why this took me to so long to find service_url. Way more simple, but has been noted that service_url should not be shown to users
open(csvfile.service_url)
From Rails 5.2 official guide
class VirusScanner
include ActiveStorage::Downloading
attr_reader :blob
def initialize(blob)
#blob = blob
end
def scan
download_blob_to_tempfile do |file|
system 'scan_virus', file.path
end
end
end
So you can do
include ActiveStorage::Downloading
attr_reader :blob
def initialize(blob)
#blob = blob
end
def perform
download_blob_to_tempfile do |file|
CSV.foreach(file.path, {headers: true}) do |row|
do_something_with_each_row(row.to_h)
end
end
end
You can get the file path from the attachment, and then open the file.
path = ActiveStorage::Blob.service.send(:path_for, attachment.key)
File.open(path) do |file|
#...
end

Carrierwave unique filename not being set

I looked at the carrierwave wiki on github, and used the method they describe to generate unique file names:
def filename
#filename ||= "#{secure_token}.#{file.extension}" if original_filename.present?
end
private
def secure_token
var = :"##{mounted_as}_secure_token"
random_token = Digest::SHA2.hexadigest("#{Time.now.utc}--#{model.id.to_s}").first(20)
model.instance_variable_get(var) or model.instance_variable_set(var, random_token)
end
(Using hashes though)
The problem I am having is that the filename is still being set to the original file's name. It's as if the filename mehod is being ignored. Not really sure what is going on. I reset the server and everything, still getting the original filename on the uploaded file and thumbnail version.
I'm not sure why the docs used ||= operator in filename method, but this way the unique file name will not be set unless #filename is nil, which doesn't seem to be the usual case. Using = instead of ||= (or not using assignment at all) seems to solve the issue.
def filename
#filename = "#{secure_token}.#{file.extension}" if original_filename.present?
# The following line works too
#"#{secure_token}.#{file.extension}" if original_filename.present?
end

How to set path where to save CSV file?

I'm saving records into CSV using after_save_filer. I don't receive any error and also I can't find file, what was generated(if it exists).
Here is code what I'm using:
after_save :to_csv
def to_csv(options = {})
require 'csv'
CSV.open("C:/project/myfile.csv", "w") do |csv|
csv << self.class.column_names
csv << self.attributes.values_at(*column_names)
end
end
How can I check is after save filter has been run ? How I can set the path ?
To check if the after_save was run you can either put a debugger statement inside the to_csv method and see if it stops there when you save the model, or you could simply put a puts "I am in to_csv method inside the method and look for it in the console.

Renaming uploaded files with Carrierwave

I'm using Carrierwave to upload files, and I have it working.
My issue is attempting to change the name of the uploaded file.
In the generated uploader.rb there is a method I think I should be using
def filename
"something.jpg" if original_filename
basename = "what"+orginal_filename if original_filename, works
basename = (0...8).map{65.+(rand(25)).chr}.join if original_filename # will create a random name for each version, e.g. the orginal, the thumb, and the filename in the db, useless
end
I can't seem to access items like 'extension' or 'content_type' in sanitized_file.rb, so this is a bit beyond my current skill level right now.
Any suggestions or exercises for doing this, i.e. generate filename for an uploaded file that works as well as the carrierwave default (do nothing, but does carry on to each version)? Seems like it should be simple enough but I've stumbled over this.
Well, another problem with your random filename generator is that it's possible to have collisions isn't it? You could possibly generate a filename that was already generated.
One way to go about it would be to somehow generate a hash based on unique properties of the image, like file path. An example, from the carrierwave group:
def filename
if original_filename
#name ||= Digest::MD5.hexdigest(File.dirname(current_path))
"#{#name}.#{file.extension}"
end
end
This will create an MD5 hash based on the current path and then append the original file's extension to it.
Edit: The carrierwave wiki added an entry with a few methods on how to create random and unique filenames for all versioned files.
To have a real unique filename (not almost unique) I recommend to use the uuid gem.
in Gemfile add:
gem 'uuid'
in file_uploader.rb:
def filename
if original_filename
if model && model.read_attribute(mounted_as).present?
model.read_attribute(mounted_as)
else
#name ||= "#{mounted_as}-#{uuid}.#{file.extension}"
end
end
end
protected
def uuid
UUID.state_file = false
uuid = UUID.new
uuid.generate
end
From the Google Group:
def filename
#name ||= "#{secure_token}.#{file.extension}" if original_filename
end
private
def secure_token
ivar = "##{mounted_as}_secure_token"
token = model.instance_variable_get(ivar)
token ||= model.instance_variable_set(ivar, ActiveSupport::SecureRandom.hex(4))
end
To just make the record.id prefix the filename you can do the following:
class MyUploader < CarrierWave::Uploader::Base
storage :file
def store_dir
model.class.to_s.underscore.pluralize
end
def filename
model.id ? "#{model.id}-#{original_filename}" : original_filename
end
def url
"/#{store_dir}/#{model.id}-#{model.file_before_type_cast}"
end
end
The other solution looks good, but how I did it then was to have a hook that created a random string for a new name on instance creation, then:
def filename
"#{model.randomstring}.#{model.image.file.extension}"
end
in the uploader.
That worked, putting the random name generation as part of the model, then having carrierwave use that.
I am curious which is faster, more effective, reasonable, sound, etc.
Here is the solution, how to change the name of the file, if store_dir already contains the file with the exact name:
if File.exists?(Rails.root.join("documents/" + "#{file.filename}")) && !path.to_s.eql?(Rails.root.join("documents/" + original_filename).to_s)
#name ||= File.basename(original_filename, '.*') + Digest::MD5.hexdigest(File.dirname(current_path)).from(25)
"#{#name}.#{file.extension}"
else
"#{original_filename}"
end
Note: Rails.root.join("documents/") is defined as my store_dir.
Hope it helps someone.

Paperclip save attachment

Is there a better way to save some string as an attachment via Paperlip as making a tmp file, putting the string into it, opening it again and saving it as an attachment ?
Like this :
def save_string data
tmp_file = "/some/path"
File.open(tmp_file,'w') do |f|
f.write(data)
end
File.open(tmp_file,'r') do |f|
ceneo_xml = f
save!
end
end
There is actually a better way - you can wrap it to StringIO which Paperclip enhances and you will get a pseudo uploaded file in no time. You can customize it by defining instance methods or directly create a subclass of StringIO like this
class InvoiceAttachment < StringIO
def initialize(invoice, content)
#invoice = invoice
super(content)
end
def original_filename
from = #invoice.from
to = #invoice.to
date = #invoice.created_at.strftime('%B-%Y').downcase
"invoice_#{date}_from_#{from}_to_#{to}.pdf"
end
def content_type
'application/pdf'
end
end
Enjoy!
Paperclip stores files alongside your models -- this is what it has been written to do, so I think the short answer is "no".
If you look in attachment.rb in the Paperclip source you'll see a method called def assign uploaded_file. If you look at the implementation of this method you can see that it expects the uploaded file object to have a certain methods defined on it.
You could create your own class which followed the same interface as Paperclip expects, but to be honest your solution of saving a file and assigning that to Paperclip is probably the easiest approach.

Resources