Streaming a file from remote S3 to react client through rails API - ruby-on-rails

The app already does this using Zipline and allowing to stream one zip file with all the files selected. But what I want to accomplish is that if only one file is being sent achieve the same behavior but instead of sending a compressed zip file sending the file as it is in S3 (with its respective extension such as docx, jpeg, xlsx, etc). So this is what I have so far.
controller
def to_zip_or_single
if params[:attachments].present?
fileNames = {}
files = Attachment.where(id: params[:attachments]
.split(','))
.map do |attachment|
file = attachment.is_image? ? AbstractFileStruct.new(attachment.upload_annotated) : attachment.upload
if !fileNames[attachment.name]
fileNames[attachment.name] = 0
else
fileNames[attachment.name] += 1
end
attachmentName = File.basename(attachment.name, File.extname(attachment.name))
attachmentName = if fileNames[attachment.name] > 0
attachmentName + " (#{fileNames[attachment.name]})" + File.extname(attachment.name)
else
attachmentName + File.extname(attachment.name)
end
[file, attachmentName]
end
end
filename = params[:filename].present? ? params[:filename] : 'attachments.zip'
if files.one?
headers['Content-Disposition'] = 'attachment; filename="' + files[0][1] + '"'
headers['Content-Type'] = 'application/octet-stream'
response.headers['Last-Modified'] = Time.now.httpdate
response.cache_control[:public] ||= false
response.sending_file = true
file = normalize(files[0][0])
the_remote_uri = URI(file[:url])
Net::HTTP.get_response(the_remote_uri) do |response|
# DO SOMETHING WITH THE RESPONSE
end
elsif files
zipline(files, filename)
end
end
def normalize(file)
file = file.file if defined?(CarrierWave::Uploader::Base) && file.is_a?(CarrierWave::Uploader::Base)
if defined?(Paperclip) && file.is_a?(Paperclip::Attachment)
if file.options[:storage] == :filesystem
{ file: File.open(file.path) }
else
{ url: file.expiring_url }
end
elsif defined?(CarrierWave::Storage::Fog::File) && file.is_a?(CarrierWave::Storage::Fog::File)
{ url: file.url }
elsif defined?(CarrierWave::SanitizedFile) && file.is_a?(CarrierWave::SanitizedFile)
{ file: File.open(file.path) }
elsif is_io?(file)
{ file: file }
elsif defined?(ActiveStorage::Blob) && file.is_a?(ActiveStorage::Blob)
{ blob: file }
elsif is_active_storage_attachment?(file) || is_active_storage_one?(file)
{ blob: file.blob }
elsif file.respond_to? :url
{ url: file.url }
elsif file.respond_to? :path
{ file: File.open(file.path) }
elsif file.respond_to? :file
{ file: File.open(file.file) }
elsif is_url?(file)
{ url: file }
else
raise(ArgumentError, 'Bad File/Stream')
end
end
I am a complete noobie to Rails and ruby. What I ultimately want is to be able to download the file from a react client using something like this
axios({
url: path,
method: 'GET',
responseType: 'blob',
}).then( async (response) => {
const fileHandle = await window.showSaveFilePicker({suggestedName: "download", types: [{accept: {"application/octet-stream":[".docx"]}}]});
const writable = await fileHandle.createWritable();
await writable.write( response.data );
await writable.close();
})
I am also not familiarized on how to work with files that good. I understand I have to use something like a writter and stream the file by chunks. I have tried some code such as
open 'large_file', 'w' do |io|
response.read_body do |chunk|
io.write chunk
end
end
and
response.read_body do |chunk|
chunk
end
But none of those have worked. If anyone could point me in the right direction or give me some ideas of what could I try in this scenario I would appreciate a lot
UPDATE
I have tried the following approach
Net::HTTP.get_response(the_remote_uri) do |response|
reader.close
open 'large_file', 'w' do |_io|
response.read_body do |chunk|
toWrite = chunk.force_encoding('UTF-8')
writer.write(toWrite)
end
end
end
But it gives me this error
<Errno::EPIPE: Broken pipe>

Something as simple as this worked for now, I dont think I am streaming the response
but still it does work, hopefully it will work with large files as well
Net::HTTP.get_response(the_remote_uri) do |resp|
self.response_body = resp.read_body
end

Related

Why does S3 change content type of my CSV

I'm using ruby on rails to generate presigned url so I can upload a CSV file. This works perfectly, I can even get the CSV using a presigned URL. The problem is when I get the CSV file a random block of text appears at the top of the csv.
------WebKitFormBoundaryrnmGKBwtkSrSvPUR
Content-Disposition: form-data; name="file"; filename="MOCK_DATA.csv"
Content-Type: application/octet-stream
id,first_name,last_name,email,gender,job_title,city,country
1,Emlyn,Dayce,edayce0#example.com,Male,Quality Control Specialist,Debrecen,Hungary
So when I try to loop through the csv using the below code:
require 'csv'
require 'open-uri'
csv = CSV.new(open(presigned_url), headers: false)
csv.each do |csv|
puts csv.to_s
end
I get the following error:
CSV::MalformedCSVError (Illegal quoting in line 2.):
Which is refering to the line:
csv.each do |csv|
Any solutions on a: how to remove this block of text before looping / while parsing the CSV. Or better yet preventing the block of text from being added in the first place using S3 Presigned URLs.
Note: I have tried to add
content_type: 'text/csv'
to presigned request, however, it doesn't recognize the param.
UPLOADING PROCESS:
I am using Vuejs to upload the csv to S3.
let formData = new FormData();
formData.append("file", this.$refs.file.files[0], { contentType: 'text/csv'
});
this.axios
.put(this.presigned_url, formData, {
headers: {
'Content-Type': 'multipart/form-data'
}
})
.then(response => {
// Handle response
if(response.status == 200){
this.original_file_name = "Processing CSV..."
this.processCsv();
}
});
I was struggling with the same issue, the problem is that you are sending the entire form into S3 and not just the file.
A correct code snippet in your case would be something like this:
this.axios
.put(this.presigned_url, this.$refs.file.files[0], {
headers: {
'Content-Type': 'text/csv'
}
})
.then(response => {
// Handle response
if(response.status == 200){
this.original_file_name = "Processing CSV..."
this.processCsv();
}
});

Rails download zip from binary data stream

I have a data-API to receive data from the file system. The data-API is a nodeJS server. Our webserver is a Rails server. From the webserver I send parameters to the API which files I want to download. The data-API then zips the request files and sends the zip back as binary data.
So far everything works fine. Now come the part where I'm stuck. I want to present the binary data as a download to the browser. I can either convert the binary data into a zip file and send that to the browser or have another clever solution that will present the binary data as a download.
Here's what I've got so far
Rails webserver side:
app/controllers/simulation_controller.rb
def download_multiple
files = JSON.parse(params[:files])
file_list = #simulation.sweep_points.find(params[:sweep_point_id]).file_list
send_data file_list.zip(files: files), filename: 'archive.zip', type: 'application/zip', disposition: 'attachment'
end
app/models/file_list.rb
def zip
uuid = SecureRandom.uuid
simulation = sweep_point.simulation
files = files[:files].join(" ")
url = URI.parse("http://localhost:3003/files/zip/#{simulation.name}/#{uuid}");
req = Net::HTTP::Get.new(url.to_s)
req.add_field("files", files)
res = Net::HTTP.start(url.host, url.port) { |http| http.request(req) }
content = res.body
content.force_encoding("ASCII-8BIT")
end
nodeJS data-API side:
exports.zip_simulation_files = (req, res) => {
const { headers, method, url } = req;
var simulation = req.params.simulation;
var uuid = req.params.uuid;
var files, command;
req.on("error", (err) => {
console.error(err);
});
files = req.headers.files;
command = "cd postprocess/bumblebee-zip " + "run" + simulation + " " + uuid + " " + "'" + files + "'";
execute(command, (data) => {
res.on("error", (err) => {
console.error(err);
});
const zipFilePath = "/home/samuel/test_bumblebee/.zips/run" + simulation + "-files-" + uuid + ".zip"
var zipFile = fs.readFileSync(zipFilePath);
var stats = fs.statSync(zipFilePath);
res.writeHead(200, {
'Content-Type': 'application/zip',
'Content-Disposition': 'attachment; filename="archive.zip"',
'Content-Length': stats.size,
'Content-Transfer-Encoding': 'binary'
});
res.end(zipFile, 'binary');
});
}
So far I'm getting back a response that is a string which seems to be a binary string. Which starts and end with this:
"PK\x03\x04\x14\x00\x00\x00\b\x00\xA8\x89\xDBJ\xB1\xB8\xA1\xBF2\x03\x00\x00\xB7F\x00\x006\x00\x1C\x00runZhangD3FINAL/13.0V/annihilation_abs_profile_001.outUT\t\x00\x0 ....... x06\x00\x00\x00\x00\x05\x00\x05\x00l\x02\x00\x00)\x10\x00\x00\x00\x00"
I've looked into different solutions in trying to turn the binary data string into a zip file or presenting the stream directly to the browser but nothing worked. Either the zip is not created or the binary was not seen as proper data.
What would be a good solution?

Trying to delete a uploaded file from directory file system

I have a web app that uploads files to file system and show them in a list. I am trying to delete the item with a button. I know I need to get the path of the directory file to be able to delete it and I believe this is where I am stuck:
def delete = {
def doc = Document.get(params.id)
def path = Document.get(path.id)
doc.delete(path)
redirect( action:'list' )
}
error I am getting: No such property: path for class: file_down.DocumentController Possible solutions: flash
It seems to me def path = Document.get(path.id) is wrong, in that case how do we find the path of a document ?
This is my upload method where I upload the files, assign it to a specific filesize, date, and fullPath( which is the uploaded folder)
def upload() {
def file = request.getFile('file')
if(file.empty) {
flash.message = "File cannot be empty"
} else {
def documentInstance = new Document()
documentInstance.filename = file.originalFilename
documentInstance.fullPath = grailsApplication.config.uploadFolder + documentInstance.filename
documentInstance.fileSize = file.getSize() / (1024 * 1024)
documentInstance.company = Company.findByName(params.company)
if (documentInstance.company == null) {
flash.message = "Company doesn't exist"
redirect (action: 'admin')
}
else {
file.transferTo(new File(documentInstance.fullPath))
documentInstance.save()
redirect (action:'list', params: ['company': params.company])
}
}
}
I think you have an error in this line:
def path = Document.get(path.id)
You try to get path.id from the path variable you are just declaring.
I'm pretty sure that you mean
def path = new File(doc.fullPath)
path.delete() // Remove the file from the file-system
doc.delete() // Remote the domain instance in DB
Alternative:
class Document {
// Add this to your Document domain
def beforeDelete = {
new File(fullPath).delete()
}
}
and then you could just do this in your controller:
def delete = {
def doc = Document.get(params.id)
doc.delete() // Delete the domain instance in DB
redirect( action:'list' )
}

How to get the object url with alias name from aws s3 using CloudFront

I am uploading files with unique id like 'd9127dfd01182afe7d34a37' as object name to amazon s3 and storing the file information with my local database including original name of the file. And I am using CloudFront url to download the file.
If I download the file using CloudFront url file name is d9127dfd01182afe7d34a37. But I need to change file name again to it's original name wich I have in my database. I don't want to download it. I want to give the url with original name to the client(WebUI) and client can download it through url.
serverside code
document_url = initialize_cloud_service(document.provider['primary']).get_object_url(document_id, expires_at, 'CloudFront' )
if document_url
item = {}
item['id'] = document['_id'].to_s
item['name'] = document['name']
item['mime_type'] = document['mime_type']
item['url'] = document_url
return {success: true, message: MESSAGES['get_url_succuss'],data: item}.to_json
end
client side code
download: function(response){
file = response.data
link = document.createElement('a');
link.download = file.name;
link.href = file.url;
link.click();
},
Is there any way to achieve this? Please help me out. I am using ruby on rails and mongodb as local database.
Thanks
I have achieved by doing the following changes
Server Side code
begin
expires_at = Time.now.to_i + 30.seconds.to_i
options = nil
selected_provider = provider || document.provider['primary']
case selected_provider
when "s3"
options = {"response-content-disposition" => "attachment; filename=#{document['name']}"}
downloadable_url = initialize_cloud_service(selected_provider).get_downloadable_url(document_id, expires_at, options)
when "google_cloud"
downloadable_url = initialize_cloud_service(selected_provider).get_downloadable_url(document_id, expires_at, options)
downloadable_url += "&response-content-disposition=attachment%3B%20filename%3D#{document['name']}"
end
item = {}
item['id'] = document['_id'].to_s
item['name'] = document['name']
item['mime_type'] = document['mime_type']
item['url'] = downloadable_url
return {success: true, message: MESSAGES['get_url_succuss'],data: item}.to_json
rescue Exception => e
puts 'Exception in download, message: ' + e.message
return {success: false, message: MESSAGES['default']}.to_json
end
client side code
download: function(response){
var hiddenIFrameID = 'hiddenDownloader',
iframe = document.getElementById(hiddenIFrameID);
if (iframe === null) {
iframe = document.createElement('iframe');
iframe.id = hiddenIFrameID;
iframe.style.display = 'none';
document.body.appendChild(iframe);
}
iframe.src = response.data.url;
},

How to export pdf report in jasper reports

I want to export a report as pdf and it should ask the user for a download location. How do I do this in grails?
This is my code:
def exportToPdf(JasperPrint jasperPrint,String path,request){
String cur_time =System.currentTimeMillis();
JRExporter pdfExporter = null;
pdfExporter = new JRPdfExporter();
log.debug("exporting to file..."+JasperExportManager.exportReportToPdfFile(jasperPrint, "C:\\pdfReport"+cur_time+".pdf"));
return ;
}
In jasper controller:
/**
* Generate a html response.
*/
def generateResponse = {reportDef ->
if (!reportDef.fileFormat.inline && !reportDef.parameters._inline) {
response.setHeader("Content-disposition", "attachment; filename=\"" + reportDef.name + "." + reportDef.fileFormat.extension + "\"");
response.contentType = reportDef.fileFormat.mimeTyp
response.characterEncoding = "UTF-8"
response.outputStream << reportDef.contentStream.toByteArray()
} else {
render(text: reportDef.contentStream, contentType: reportDef.fileFormat.mimeTyp, encoding: reportDef.parameters.encoding ? reportDef.parameters.encoding : 'UTF-8');
}
}
Have you looked at the Jasper Plugin? It seems to have the tools already built for you. As far as asking the user for a download location the browser has some controller over how files are received from a web page. Is your real issue that you want control over the download location?
[UPDATE]
Using the location 'c:\' is on your server not the client and this is why it is not downloading.
try something like this...
def controllerMethod = {
def temp_file = File.createTempFile("jasperReport",".pdf") //<-- you don't have to use a temp file but don't forget to delete them off the server at some point.
JasperExportManager.exportReportToPdfFile(jasperPrint, temp_file.absolutePath));
response.setContentType("application/pdf") //<-- you'll have to handle this dynamically at some point
response.setHeader("Content-disposition", "attachment;filename=${temp_file.getName()}")
response.outputStream << temp_file.newInputStream() //<-- binary stream copy to client
}
I have not tested this and there are better ways of handling the files and streams but i think you'll get the general idea.

Resources