How in Ruby on Rails 4.2.6 make a statement which has to check for file extension and upload to right public folder - ruby-on-rails

I'm working on an application which has an upload functionality for documents. I can download various kind of documents like pdf, docx and etc. However, all is uploaded in one folder like ../uploads/documents.
What I have to reach is when the upload began, a statement will check the file extension and upload it to the right folder named as the extension of the file. As an example, I can have a PDF in upload and the app check if the PDF directory exists and if not create one, then upload to that directory. So far I have done what below but I'm new in RoR so I would like to have some suggestions how to make what mentioned above:
This comes from my CTRL:
module UploaderWidget
class Engine < ::Rails::Engine
end
def initialize(params = {})
#file = params.delete(:file)
super
if #file
self.filename = sanitize_filename(#file.original_filename)
self.content_type = #file.content_type
self.file_contents = #file.read
end
end
def upload_local
path = "#{Rails.root}/public/uploads/document"
FileUtils.mkdir_p(path) unless File.exists?(path)
FileUtils.copy(#file.tempfile, path)
end
private
def sanitize_filename(filename)
return File.basename(filename)
end
def document_file_format
unless ["application/pdf","application/vnd.ms-excel",
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
"application/msword",
"application/vnd.openxmlformats-officedocument.wordprocessingml.document",
"text/plain", "text/csv", "application/octet-stream"].include? self.content_type
errors.add(:file, 'Invalid file format.')
end
end
NUM_BYTES_IN_MEGABYTE = 1048576
def file_size_under_one_mb
if (#file.size.to_f / NUM_BYTES_IN_MEGABYTE) > 1
errors.add(:file, 'File size cannot be over one megabyte.')
end
end
end

You can use the File.extname() method:
File.extname("test.rb") #=> ".rb"
File.extname("a/b/d/test.rb") #=> ".rb"
File.extname("foo.") #=> ""
File.extname("test") #=> ""
File.extname(".profile") #=> ""
File.extname(".profile.sh") #=> ".sh"
In your particular case, you could do something like this:
sanitize the file name and save it to an instance variable
extract the extension name and save it to an instance variable
eventually manipulate your extension name, e.g.
remove the '.' char
create an hash having every extension as a key and the sub-folder name as a value
define a method to manipulate the extension and return the folder name
build your path like path = Rails.root.join('public', 'uploads', 'document' sub_folder_name)
The rest of your code should work as it is

Related

Rails 5.2 Import XLSX with ActiveStorage and Creek

I have a model called ImportTemp which is used for store imported XLSX file to database. I'm using ActiveStorage for storing the files.
this is the model code:
class ImportTemp < ApplicationRecord
belongs_to :user
has_one_attached :file
has_one_attached :log_result
end
this is my import controller code:
def import
# Check filetype
case File.extname(params[:file].original_filename)
when ".xlsx"
# Add File to ImportFile model
import = ImportTemp.new(import_type: 'UnitsUpload', user: current_user)
import.file.attach(params[:file])
import.save
# Import unit via sidekiq with background jobs
ImportUnitWorker.perform_async(import.id)
# Notice
flash.now[:notice] = "We are processing your xlsx, we will inform you after it's done via notifications."
# Unit.import_file(xlsx)
else flash.now[:error] = t('shared.info.unknown')+": #{params[:file].original_filename}"
end
end
after upload the xlsx file, then the import will be processed in sidekiq. This is the worker code (still doesn't do the import actually) :
class ImportUnitWorker
include Sidekiq::Worker
sidekiq_options retry: false
def perform(file_id)
import_unit = ImportTemp.find(file_id)
# Open the uploaded xlsx to Creek
creek = Creek::Book.new(Rails.application.routes.url_helpers.rails_blob_path(import_unit.file, only_path: true))
sheet = creek.sheets[0]
puts "Opening Sheet #{sheet.name}"
sheet.rows.each do |row|
puts row
end
units = []
# Unit.import(units)
end
but after i tried it, it gives me error:
Zip::Error (File /rails/active_storage/blobs/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaHBCZz09IiwiZXhwIjpudWxsLCJwdXIiOiJibG9iX2lkIn19--3960b6ba5b55f7004e09967d16dfabe63f09f0a9/2018-08-10_10_39_audit_gt.xlsx not found)
but if i tried to open it with my browser, which is the link looks like this:
http://localhost:3000/rails/active_storage/blobs/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaHBCZz09IiwiZXhwIjpudWxsLCJwdXIiOiJibG9iX2lkIn19--3960b6ba5b55f7004e09967d16dfabe63f09f0a9/2018-08-10_10_39_audit_gt.xlsx
it's working and the xlsx is downloaded. My question is what's wrong with it? why the file is not found in the sidekiq?
I ended up using Tempfile as suggested by George Claghorn. I don't know if this is the best solution or best practices, but it works for me now. I'm going to use this solution while waiting Rails 6 stable to come out with ActiveStorage::Blob#open feature.
def perform(file_id)
import = ImportTemp.find(file_id)
temp_unit = Tempfile.new([ 'unit_import_temp', '.xlsx' ], :encoding => 'ascii-8bit')
units = []
begin
# Write xlsx from ImportTemp to Tempfile
temp_unit.write(import.file.download)
# Open the temp xlsx to Creek
book = Creek::Book.new(temp_unit.path)
sheet = book.sheets[0]
sheet.rows.each do |row|
# Skip the header
next if row.values[0] == 'Name' || row.values[1] == 'Abbreviation'
cells = row.values
# Add cells to new Unit
unit = Unit.new(name: cells[0], abbrev: cells[1], desc: cells[2])
units << unit
end
# Import the unit
Unit.import(units)
ensure
temp_unit.close
temp_unit.unlink # deletes the temp file
end
end
Rails.application.routes.url_helpers.rails_blob_path doesn’t return the path to the file on disk. Rather, it returns a path that can be combined with a hostname to produce an URL for downloading the file, for use in links.
You have two options:
If you’d prefer to keep ImportUnitWorker indifferent to the storage service in use, “download” the file to a tempfile on disk. Switch to Rails master and use ActiveStorage::Blob#open:
def perform(import_id)
import = ImportTemp.find(import_id)
units = []
import.file.open do |file|
book = Creek::Book.new(file.path)
sheet = creek.sheets[0]
# ...
end
Unit.import(units)
end
If you don’t mind ImportWorker knowing that you use the disk service, ask the service for the path to the file on disk. ActiveStorage::Service::DiskService#path_for(key) is private in Rails 5.2, so either forcibly call it with send or upgrade to Rails master, where it’s public:
def perform(import_id)
import = ImportTemp.find(import_id)
units = []
path = ActiveStorage::Blob.service.send(:path_for, import.file.key)
book = Creek::Book.new(path)
sheet = creek.sheets[0]
# ...
Unit.import(units)
end
The answer now seems to be (unless I am missing something):
Creek::Book.new file.service_url, check_file_extension: false, remote: true

Rails: Helper method behaving differently between console and application

I am trying to write a helper method that can download a CSV file from S3 storage, read the first few rows of the file and then save those first few rows to a new local file.
All is working well when I include the helper in the rails console and call the methods on the object, but when calling it in exactly the same way through the controller, the local file contains all of the rows from the S3 file, rather than just the first few.
My code, in the helper file (I've replaced AWS credentials with comments for the purpose of posting the question):
def download_file(data_source)
s3 = Aws::S3::Client.new(#API keys etc.)
File.open(data_source.file.data['id'], 'wb') do |file|
reap = s3.get_object({ bucket:#Bucket Name, key: 'store/' + data_source.file.data['id'] }, target: file)
end
end
def reduce_csv(filename)
data = CSV.open(filename, 'r') { |csv| csv.first(3) }
csv_string = CSV.generate do |csv|
data.each do |d|
csv << d
end
end
File.open('test.csv', 'wb') do |file|
file << csv_string
end
end
def make_small_data_source(data_source)
download_file(data_source)
reduce_csv(data_source.file.data['id'])
end
And in the controller:
if #data_source.save
make_small_data_source(#data_source)
Any ideas would be much appreciated!

Placing folders, inserting files, zipping all and providing download

I have a project running.
And I have a few fundamental Ruby-On-Rails-questions on:
where to place a folder in my rails installation, so i have access to it from within my controllers
how to place files inside that folder when models are saved
how to zip it
providing the zip as a download
now, let me explain:
I want to save to a "pages" folder that has a subfolder "blog"
to that blog-folder I want to add subfolders each representing a post
the name of the post-folder is created by the user, as he provides a title
each post-folder has a MarkDown file in it called "post.md"
if the user clicks a download-button the whole "pages"-folder should be zipped and sent to the client as a download
Technically you can put your folder anywhere, and access your .md file like following (as long as you have permission to read that file):
def show
markdown=Redcarpet::Markdown.new(Redcarpet::Render::HTML)
md_file = IO.read("#{Rails.root}\static\pages\blog\post.md")
#post = markdown.render(md_file)
end
and at view
<%= #post.html_safe %>
According to documentation of 'rubyzip' gem you will need something like this to make your directory zipable:
class ZipFileGenerator
# Initialize with the directory to zip and the location of the output archive.
def initialize(inputDir, outputFile)
#inputDir = inputDir
#outputFile = outputFile
end
# Zip the input directory.
def write()
entries = Dir.entries(#inputDir); entries.delete("."); entries.delete("..")
io = Zip::File.open(#outputFile, Zip::File::CREATE);
writeEntries(entries, "", io)
io.close();
end
# A helper method to make the recursion work.
private
def writeEntries(entries, path, io)
entries.each { |e|
zipFilePath = path == "" ? e : File.join(path, e)
diskFilePath = File.join(#inputDir, zipFilePath)
puts "Deflating " + diskFilePath
if File.directory?(diskFilePath)
io.mkdir(zipFilePath)
subdir =Dir.entries(diskFilePath); subdir.delete("."); subdir.delete("..")
writeEntries(subdir, zipFilePath, io)
else
io.get_output_stream(zipFilePath) { |f| f.print(File.open(diskFilePath, "rb").read())}
end
}
end
end
and at your zip action at controller:
directoryToZip = "/pages"
outputFile = "/public/out#{rand(6**length).to_s(6)}.zip"
zf = ZipFileGenerator.new(directoryToZip, outputFile)
zf.write()
and then you can provide the link of outputFile to your user and you may also need to delete that zip file.

How to handle a file_as_string (generated by Prawn) so that it is accepted by Carrierwave?

I'm using Prawn to generate a PDF from the controller of a Rails app,
...
respond_to do |format|
format.pdf do
pdf = GenerateReportPdf.new(#object, view_context)
send_data pdf.render, filename: "Report", type: "application/pdf", disposition: "inline"
end
end
This works fine, but I now want to move GenerateReportPdf into a background task, and pass the resulting object to Carrierwave to upload directly to S3.
The worker looks like this
def perform
pdf = GenerateReportPdf.new(#object)
fileString = ???????
document = Document.new(
object_id: #object.id,
file: fileString )
# file is field used by Carrierwave
end
How do I handle the object returned by Prawn (?????) to ensure it is a format that can be read by Carrierwave.
fileString = pdf.render_file 'filename' writes the object to the root directory of the app. As I'm on Heroku this is not possible.
file = pdf.render returns ArgumentError: string contains null byte
fileString = StringIO.new( pdf.render_file 'filename' ) returns TypeError: no implicit conversion of nil into String
fileString = StringIO.new( pdf.render ) returns ActiveRecord::RecordInvalid: Validation failed: File You are not allowed to upload nil files, allowed types: jpg, jpeg, gif, png, pdf, doc, docx, xls, xlsx
fileString = File.open( pdf.render ) returns ArgumentError: string contains null byte
....and so on.
What am I missing? StringIO.new( pdf.render ) seems like it should work, but I'm unclear why its generating this error.
It turns out StringIO.new( pdf.render ) should indeed work.
The problem I was having was that the filename was being set incorrectly and, despite following the advise below on Carrierwave's wiki, a bug elsewhere in the code meant that the filename was returning as an empty string. I'd overlooked this an assumed that something else was needed
https://github.com/carrierwaveuploader/carrierwave/wiki/How-to:-Upload-from-a-string-in-Rails-3
my code ended up looking like this
def perform
s = StringIO.new(pdf.render)
def s.original_filename; "my file name"; end
document = Document.new(
object_id: #object.id
)
document.file = s
document.save!
end
You want to create a tempfile (which is fine on Heroku as long as you don't expect it to persist across requests).
def perform
# Create instance of your Carrierwave Uploader
uploader = MyUploader.new
# Generate your PDF
pdf = GenerateReportPdf.new(#object)
# Create a tempfile
tmpfile = Tempfile.new("my_filename")
# set to binary mode to avoid UTF-8 conversion errors
tmpfile.binmode
# Use render to write the file contents
tmpfile.write pdf.render
# Upload the tempfile with your Carrierwave uploader
uploader.store! tmpfile
# Close the tempfile and delete it
tmpfile.close
tmpfile.unlink
end
Here's a way you can use StringIO like Andy Harvey mentioned, but without adding a method to the StringIO intstance's eigenclass.
class VirtualFile < StringIO
attr_accessor :original_filename
def initialize(string, original_filename)
#original_filename = original_filename
super(string)
end
end
def perform
pdf_string = GenerateReportPdf.new(#object)
file = VirtualFile.new(pdf_string, 'filename.pdf')
document = Document.new(object_id: #object.id, file: file)
end
This one took me couple of days, the key is to call render_file controlling the filepath so you can keep track of the file, something like this:
in one of my Models e.g.: Policy i have a list of documents and this is just the method for updating the model connected with the carrierwave e.g.:PolicyDocument < ApplicationRecord mount_uploader :pdf_file, PdfDocumentUploader
def upload_pdf_document_file_to_s3_bucket(document_type, filepath)
policy_document = self.policy_documents.where(policy_document_type: document_type)
.where(status: 'processing')
.where(pdf_file: nil).last
policy_document.pdf_file = File.open(file_path, "r")
policy_document.status = 's3_uploaded'
policy_document.save(validate:false)
policy_document
rescue => e
policy_document.status = 's3_uploaded_failed'
policy_document.save(validate:false)
Rails.logger.error "Error uploading policy documents: #{e.inspect}"
end
end
in one of my Prawn PDF File Generators e.g.: PolicyPdfDocumentX in here please note how im rendering the file and returning the filepath so i can grab from the worker object itself
def generate_prawn_pdf_document
Prawn::Document.new do |pdf|
pdf.draw_text "Hello World PDF File", size: 8, at: [370, 462]
pdf.start_new_page
pdf.image Rails.root.join('app', 'assets', 'images', 'hello-world.png'), width: 550
end
end
def generate_tmp_file(filename)
file_path = File.join(Rails.root, "tmp/pdfs", filename)
self.generate_prawn_pdf_document.render_file(file_path)
return filepath
end
in the "global" Worker for creating files and uploading them in the s3 bucket e.g.: PolicyDocumentGeneratorWorker
def perform(filename, document_type, policy)
#here we create the instance of the prawn pdf generator class
pdf_generator_class = document_type.constantize.new
#here we are creating the file, but also `returning the filepath`
file_path = pdf_generator_class.generate_tmp_file(filename)
#here we are simply updating the model with the new file created
policy.upload_pdf_document_file_to_s3_bucket(document_type, file_path)
end
finally how to test, run rails c and:
the_policy = Policies.where....
PolicyDocumentGeneratorWorker.new.perform('report_x.pdf', 'PolicyPdfDocumentX',the_policy)
NOTE: im using meta-programming in case we have multiple and different file generators, constantize.new is just creating new prawn pdf doc generator instance so is similar to PolicyPdfDocument.new that way we can only have one pdf doc generator worker class that can handle all of your prawn pdf documents so for instance if you need a new document you can simply PolicyDocumentGeneratorWorker.new.perform('report_y.pdf', 'PolicyPdfDocumentY',the_policy)
:D
hope this helps someone to save some time

Carrierwave: Process Temp file and then upload via fog

I am processing a pdf uploaded by an user by extracting the text from it and saving the output in an text file for processing later.
Locally I store the pdf in my public folder but when I work on Heroku I need to use S3.
I thought that the pdf path was the problem, so I included
if Rails.env.test? || Rails.env.cucumber?
But still I receive
ArgumentError (input must be an IO-like object or a filename):
Is there a way of temporarily storing the pdf in my root/tmp folder on Heroku, get the text from it, and then after that is done, upload the document to S3?
def convert_pdf
if Rails.env.test? || Rails.env.cucumber?
pdf_dest = File.join(Rails.root, "public", #application.document_url)
else
pdf_dest = #application.document_url
end
txt_file_dest = Rails.root + 'tmp/pdf-parser/text'
document_file_name = /\/uploads\/application\/document\/\d{1,}\/(?<file_name>.*).pdf/.match(#application.document_url)[:file_name]
PDF::Reader.open(pdf_dest) do |reader|
File.open(File.join(txt_file_dest, document_file_name + '.txt'), 'w+') do |f|
reader.pages.each do |page|
f.puts page.text
end
end
end
end
You're going to want to set up a custom processor in your uploader. And on top of that, since the output file (.txt) isn't going to have the same extension as the input file (.pdf), you're going to want to change the filename. The following belongs in your Uploader:
process :convert_to_text
def convert_to_text
temp_dir = Rails.root.join('tmp', 'pdf-parser', 'text')
temp_path = temp_dir.join(filename)
FileUtils.mkdir_p(temp_dir)
PDF::Reader.open(current_path) do |pdf|
File.open(temp_path, 'w') do |f|
pdf.pages.each do |page|
f.puts page.text
end
end
end
File.unlink(current_path)
FileUtils.cp(temp_path, current_path)
end
def filename
super + '.txt' if original_filename.present?
end
I haven't run this code, so there are probably some bugs, but that should give you the idea at least.

Resources