Sending a file to the client from Node.js with Express - javascript

I have a unique situation in terms of difficulty.
I need to send HTML to the server, have the server convert the HTML to a PDF, send that PDF back to the client, and then download the PDF using client-side code.
I have to do it this way because I'm using client-side routing, so the only way I can access my endpoint that should perform this action is via a GET Request with Ajax or Fetch from client-side JavaScript. I am aware of res.sendFile(), but that attempts to render the file in the browser - I don't want that - rather, I want to be able to use client-side code to download the file.
Is it possible, then, to send a PDF file from temporary storage on the server down to the client, allowing client-side code to do whatever it wants to the file thereafter - in my case, downloading it?
I don't believe I have to provide any code because this is more of a theoretical question.

My issue stemmed from the fact that I could not just use res.sendFile() or res.download() from Express because the route was not being accessed by the browser URL bar, rather, my application uses client-side routing, and thus I had to make an HTTP GET Request via Fetch or XMLHttpRequest.
The second issue is that I needed to build the PDF file on the server based on an HTML string sent from the client - so again, I need to make a GET Request sending along a request body.
My solution, then, using Fetch, was to make the Get Request from the client:
fetch('/route' , {
method: 'GET',
body: 'My HTML String'
});
On the server, I have my code that converts the HTML string to a PDF, using the HTML-PDF Node module, and then, I convert that file to a Base64 String, setting the MIME Type and appending data:application/pdf;base64,.
app.get('/route', (req, res) => {
// Use req.body to build and save PDF to temp storage (os.tempdir())
// ...
fs.readFile('./myPDF.pdf', (err, data) => {
if (err) res.status(500).send(err);
res.contentType('application/pdf')
.send(`data:application/pdf;base64,${new Buffer.from(data).toString('base64')}`);
});
});
Back on the client, I have my aforementioned Fetch Request, meaning I just need to tack on the promise to get the response:
fetch('/route', {
method: 'POST',
body: 'My HTML String' // Would define object and stringify.
})
.then(res => res.text())
.then(base64String => {
// Now I just need to download the base64String as a PDF.
});
To make the download, I dynamically create an anchor tag, set its href attribute to the Base64 String in the response from the server, give it a title, and then programmatically click it:
const anchorTag = document.createElement('a');
anchorTag.href = base64String;
anchorTag.download = "My PDF File.pdf";
anchorTag.click();
So, all together and on the client:
fetch('/route', {
method: 'POST',
body: 'My HTML String' // Would define object and stringify.
})
.then(res => res.text())
.then(base64String => {
const anchorTag = document.createElement('a');
anchorTag.href = base64String;
anchorTag.download = "My PDF File.pdf";
anchorTag.click();
});
The solution for using an anchor tag to trigger the download came from another StackOverflow answer. It's also important to note that Base64 Encoding is not very efficient. Better solutions exist, but for my purposes, Base64 will work fine.
It is also imperative to note that Base64 Encoding is precisely that - an Encoding Scheme, not, I repeat, not an Encryption Scheme. So if your PDF files contain privileged information, you would likely want to add token authentication to the endpoint and encrypt the file.

Related

GET vs POST file download using express.js - changing the REST verb results in larger and incorrect file

My situation is that in one of my services I provide many REST GET APIs to allow the downloading of files (typically xlsx).
In a new API I have a POST API which does exactly the same thing as another GET API (I've copied pasted the code line by line), except that it's using a POST instead of a GET
export async function postDownloadMyFile (
req: Request,
res: Response,
next: NextFunction
): Promise<Response> {
... // xlsx workBook creation code
const buffer = await workBook.xlsx.writeBuffer();
res.setHeader('Content-Type', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet');
res.setHeader('Content-Disposition', 'attachment; filename=' + 'MyFile.xlsx');
res.write(buffer, 'binary');
res.end();
return res;
}
but when the frontend client makes a request to download the file, the file comes back about twice the size than if I used a GET request and cannot be opened (presumably wrong format/buffer written out wrong).
changing the frontend and backend to use a GET (not modifying the body of the function above) 'fixes' the issue.
Are there some additional headers I'm meant to be setting?
Thanks

How can I send a stream containing an image file to a remote method in loopback?

I am currently trying to produce an API using loopback that allows me to send a 28x28 image file of a handwritten character and have the image processed by a tensorflow network and return the prediction of what the network thinks the character is.
In order to do this, however, I need to be able to send the image to be processed without having to save the file on the server first and cannot find how to do such a thing. Modules such as loopback-component-storage are great, but I do not want to have to use one route to send the image, another to process that image, and then a third to then remove the container containing that image file, making the process require three different requests.
Hence it comes down to this, is there any way that I can attach an image to the request where the stream can be read and interpreted by the API without having to first save a copy of the file somewhere else on the server?
Thanks in advance
I am recommending the following solution:
First, configure your server middleware to parser image request bodies:
Install body-parser dependency.
$ npm install --save body-parser
Configure the raw parser by adding the following content to parse section of your server/middleware.json file:
{
"body-parser#raw": {
"limit": "100kb",
"type": "image/*"
}
}
The "limit" option sets the maximum allowed request body size. You don't want to allow arbitrary size to prevent malicious clients from crashing your server on "out of memory" error.
The "type" option configures content-types that should be parsed by this middleware. In my example above, I am allowing all image types.
Next, implement a remote method accepting the request body. Thanks to the raw body parser, the body stream will be already converted into a Buffer for you. In my example below, I have a simple method that responds with a base64-encoded body.
module.exports = function(Image) {
Image.analyze = async function(data) {
// Reject non-image requests, e.g. JSON
if (!Buffer.isBuffer(data)) {
const err = new Error('Unsupported media type');
err.statusCode = 415;
throw err;
}
// data is a Buffer containing the request body
return data.toString('base64');
};
Image.remoteMethod('analyze', {
accepts: [
// {source: 'body'} is the important part
{arg: 'data', type: 'object', http: {source: 'body'}},
],
returns: {root: true, type: 'string'},
http: {path: '/analyze', verb: 'post'},
});
};

Download Csv file on front end after clicking on button using angularjs , nodejs ,expressjs

I want to download .csv file on frontend.
this is my code:
$http.get('/entity/consultations/_/registerationReport' )
.success(function (data) {
myWindow = window.open('../entity/consultations/_/registerationReport', '_parent');
myWindow.close();
});
and I use json2csv converter to write in csv file.
json2csv({data: report, fields: fields}, function (err, csv) {
if (err) throw err;
res.setHeader('Content-Type', 'application/csv');
res.setHeader("Content-Disposition", "attachment; filename=" + "Report.csv");
res.end(csv, 'binary');
});
but it prints data of csv file on browser instead of downloading csv file.
#Pawan, there's nothing wrong with your json2csv function. The issue is the fact that you're trying to trigger the download with an XMLHttpRequest (XHR) request using Angular's $http service. An XHR call suggests that your code will be handling the response from the server. As such the Content-Disposition headers are ignored by the browser and do not trigger a download on the browser.
From what I can tell you have several options:
If you don't have any pre-processing to do on the client, why not just use a direct link to /entity/consultations/_/registerationReport (using and <a> tag),
You may also write $window.open(...) from your Angular code (this will have the ugly side effect of a flashing popup tab or window)
There are probably a number of other solutions, but these are the only ones that immediately come to mind. The bottom line is that XHR is not the right tool for the task you're trying to accomplish.

NodeJS not trigger save file dialog in browser / EmberJS not receive file sent from server

My scenario:
I have an app written using MEEN stack (MySQL, ExpressJS, EmberJS, NodeJS). In a page, I have a form input. I am supposed to get the input data, send it to server, generate a PDF and display in EmberJS. The server is in NodeJS with ExpressJS, and the generated file is written to disk before getting sent to front-end.
My problem:
I cannot display the PDF file in EmberJS. Or rather, nothing is displayed. In my back-end with NodeJS and ExpressJS, I send a filestream of the PDF file back to EmberJS. Using a REST Client like Postman extension in Chrome, the dialog is called, and I can save the file. However, in EmberJS, no dialog appears, but I can see the content of the filestream using console.log in Chrome Dev Tools.
Why is it like this?
Below is my code for sending the file in NodeJS with ExpressJS.
generatePDF: function(req, res){
var details = req.body;
// #param details
// #return relative path to the file
inventoryController.generatePDF(details, function(relPath){
var filePath = path.resolve(relPath);
// This is the first method
// I explicitly specify the header for the Response Object
// and pipe the ReadableStream to the Response Object
// var name = path.basename(filePath);
// var mimeType = mime.lookup(filePath);
// res.setHeader('Content-disposition', 'attachment; filename=' + name);
// res.setHeader('Content-type', mimeType);
// var fileStream = fs.createReadStream(filePath);
// fileStream.pipe(res);
// This is the second method (currently being used)
// using sendfile() of ExpressJS, the header is automatically set
res.sendfile(filePath);
});
},
The code in Ember.js for sending details to server and getting data back.
var doc = {
// some attributes inside here
};
var url='/pdf';
var request = Ember.$.post(url, doc);
request.then(function(data){
if(data.status === 'ERR'){
// handling error
} else {
console.log('Successfully generated PDF.');
console.log(data); // I can see the filestream here
}
});
I think the problem lies that this isn't an Ember.js issue but a server issue, and how it's handled.
jQuery cannot save the file to disk, as it's a security risk. While extensions can override various security limitations, In Ember, or any other JavaScript framework (which uses jQuery) you cannot force a save as dialog as this is enforced by security. JavaScript cannot write to your local file format, (aside from html 5 local storage which is similar in many ways to cookies).
As such you cannot have a save as PDF. The only way to perhaps get that to work is allow the browser itself to capture the stream back and handle it:
You could try this plugin, but I'm not sure it works, and it's not recommended at all.
I'd recommend you just use a pure link from your browser and have the server send the file back in proper way, rather than an ajax call. You can either have a form submit it or a pure link with query string params.
Client Side:
<a href="/server/getFile?{"contentID"="123", "user" = "test#me"}>Get File</a>
or
<form action="/getFile" method="POST">
<input type="hidden" id="user" name="user" value="test#me" />
<input type="hidden" id="contentID" name="contentID" value="123" />
<input type="submit" value="Get File />
</form>
Server Side:
Add in the server the following headers:
res.setHeader('Content-disposition', 'attachment; filename=<file name.ext>');
It might be a wise idea to also add a mime-type:
res.setHeader('Content-type', '<type>/<subtype>');
full code:
var filename = path.basename(filePath);
res.setHeader('Content-disposition', 'attachment; filename='+ filename);
res.setHeader('Content-type', 'application/pdf');
// it's better to use a stream than read all the file into memory.
var filestream = fs.createReadStream(filePath);
filestream.pipe(res);
Or if you're using express you could use this helper
res.download(filePath)

How to set the name of a file in jquery

(This title may not be clear but whatever you read here is a refinement trying to get past the "quality standards" script, which seem to be a lot stricter based on the tags chosen)
On the client side is there any way in jquery to set the name of the file downloaded?
lets say on my server the file is stored as QXYZO123 , but the file is really information.xls , a spreadsheet. Even the hyperlink says information.xls as I have information linking the random file name to the original file name.
When the user clicks on the link, by default it will try to download the file name as stored on the server, QXYZO123, but I want it instead to say information.xls , how can I set this to suggest the file name to save as?
I call it a suggestion because based on the user's browser settings, it will either auto download with my filename suggestion or ask the user where to download and save it, with the filename suggestion
There is no setting file properties from JQuery directly. JQuery means some Javascript codes and Javascript is a client side script language. This means you can do something, only on client side, not on server side.
But there is some indirect methods to do this. First of all, only way to do this is setting the file name on server side. It depends on what language you are using on server side. Depens on server side language, You can write your own download web api. Pass the file name as a request parameter to your api and let it give you the file with your customized file name. I don't know what language you are using on server side but I prepared a serverside code with node.js that accepts customized file name in 'GET' request as parameter.
var sys = require ('sys'),
url = require('url'),
http = require('http'),
qs = require('querystring');
var path = require('path');
var mime = require('mime');
var fs = require('fs');
var server=http.createServer(
function (request, response) {
if(request.method=='GET') {
var filePath='path_to_your_file_in_your_server_file_system';
var filestream = fs.createReadStream(filePath);
var url_parts = url.parse(request.url,true);
console.log(url_parts);
var fileName= url_parts.query.fileName; //Taking your customized file name from GET request parameters.
if(!fileName)
{
fileName=path.basename(filePath);
}
var mimetype = mime.lookup(filePath);
response.setHeader('Content-disposition', 'attachment; filename=' + fileName);
response.setHeader('Content-type', mimetype);
filestream.pipe(response);
response.writeHead( 200 );
response.end();
}
}
);
server.listen( 9080 );
When you run this server code with node.js you can get file with your customized file name from the url which localhost:9080?fileName=yourCustomizedFileName. As you see we are giving the fileName 'GET' request parameter in url. You can use javascript windows.location or something else to get the file with your customized file name on client side Javascript code.
Don't let the node.js code make you confused. The point of the solution is writing a downloading api which takes fileName parameter from 'GET' request, to send the file with this name to clients.
You can apply this node.js code for your server side language or research how to apply it. Probably, It won't be hard to find and apply it.

Categories

Resources