create file for download on the fly within POST request - javascript

I am trying to figure out, if its possible to create a file without storing it in the drive, for only purpose to download it right away within POST request.
const specialRequests = async (req, res, next) => { // POST request
... // processing
let xmlString = ... // get processed xml string
// create xml file from that xmlString
// initialise download of the file, dont save it
let file = ???
res.download(file);
};
If its possible how it can be done

The download method is explicitly designed to transfer "the file at path".
You can create a download with some data, but you can't use that method.
res.set('Content-Type', 'text/xml');
res.set('Content-Disposition', 'attachment; filename="example.xml"')
res.send(xmlString);

Related

How to download a json object as file?

I am currently trying to let the user of my website download a json object as a json-file.
With my following code i get the error message:
Error: Can't set headers after they are sent.
at SendStream.headersAlreadySent (\node_modules\send\index.js:390:13)
at SendStream.send (\node_modules\send\index.js:617:10)
at onstat (\node_modules\send\index.js:729:10)
at FSReqCallback.oncomplete (fs.js:168:5)
router.post('/about', ensureAuthenticated,
function (req, res, next) {
console.log(req.user);
var jsonVariable = JSON.stringify(req.user);
var path_tmp = create_tmp_file(jsonVariable);
res.download(path_tmp);
res.redirect('/about');
next();
}
);
Is there a better way to download a json object directly with no need to save it in the filesystem?
You can always inject some HTML into a page (or redirect to a page with some client-side JavaScript on it), and download it with the client. Just send the JSON string somehow to the new page you are redirecting to (it can even be a GET parameter), then download it with the following code (assuming the JSON string is in a variable called json):
var a = document.createElement("a")
a.href = URL.createObjectURL(
new Blob([json], {type:"application/json"})
)
a.download = "myFile.json"
a.click()
var jsonVariable = JSON.stringify(req.user);
var path_tmp = create_tmp_file(jsonVariable);
res.download(path_tmp);
Send the data with res.json and use content-disposition to make it a download.
res.set('Content-Disposition', 'attachment; filename=example.json')
res.json(req.user);
res.redirect('/about');
next();
And don't do that (which is the cause of the error). You are responding with a download. You can't say "Here is the file you asked for" while, at the same time say, "The file you asked for isn't here, go to this URL instead".

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

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.

Read file content from a form upload file

I use the following code to read the get files from the file system
The code is from a blog post called Building a File Uploader with NodeJs.
I was able to see the UI, etc when I ran my project.
I cannot use the following code since I don't have an uploads folder in my project (form.uploadDir)
app.post('/upload', function(req, res){
// create an incoming form object
var form = new formidable.IncomingForm();
// specify that we want to allow the user to upload multiple files in a single request
form.multiples = true;
// store all uploads in the /uploads directory - cannot use it
//form.uploadDir = path.join(__dirname, '/uploads');
// every time a file has been uploaded successfully,
// rename it to it's original name
form.on('file', function(field, file) {
fs.rename(file.path, path.join(form.uploadDir, file.name));
});
// log any errors that occur
form.on('error', function(err) {
console.log('An error has occurred: \n' + err);
});
// once all the files have been uploaded, send a response to the client
form.on('end', function() {
res.end('success');
});
// parse the incoming request containing the form data
form.parse(req);
});
My question is how should I get the file from the UI with the code
above? I need to get the file content from the form when I choose my file.
The application is deployed to the cloud and when I use localhost, I use the following code (which works)
const readStream = fs.createReadStream("./file.html");
...
const writeStream = sftp.createWriteStream("/index.html");
...
readStream.pipe(writeStream);
Which creates a file from the file system with the correct path and overwrites it with another file (like here index.html).
As #aedneo said, when the user choose file the file is created in the upload folder , I just needed to rename it and give the right path to the write stream method and it works!

Piping zip file from SailsJS backend to React Redux Frontend

I have a SailsJS Backend where i generate a zip File, which was requested by my Frontend, a React App with Redux. I'm using Sagas for the Async Calls and fetch for the request. In the backend, it tried stuff like:
//zipFilename is the absolute path
res.attachment(zipFilename).send();
or
res.sendfile(zipFilename).send();
or
res.download(zipFilename)send();
or pipe the stream with:
const filestream = fs.createReadStream(zipFilename);
filestream.pipe(res);
on my Frontend i try to parse it with:
parseJSON(response) => {
return response.clone().json().catch(() => response.text());
}
everything i tried ends up with an empty zip file. Any suggestions?
There are various issues with the options that you tried out:
res.attachment will just set the Content-Type and Content-Disposition headers, but it will not actually send anything.
You can use this to set the headers properly, but you need to pipe the ZIP file into the response as well.
res.sendfile: You should not call .send() after this. From the official docs' examples:
app.get('/file/:name', function (req, res, next) {
var options = { ... };
res.sendFile(req.params.name, options, function (err) {
if (err) {
next(err);
} else {
console.log('Sent:', fileName);
}
});
});
If the ZIP is properly built, this should work fine and set the proper Content-Type header as long as the file has the proper extension.
res.download: Same thing, you should not call .send() after this. From the official docs' examples:
res.download('/report-12345.pdf', 'report.pdf', function(err) { ... });
res.download will use res.sendfile to send the file as an attachment, thus setting both Content-Type and Content-Disposition headers.
However, you mention that the ZIP file is being sent but it is empty, so you should probably check if you are creating the ZIP file properly. As long as they are built properly and the extension is .zip, res.download should work fine.
If you are building them on the fly, check this out:
This middleware will create a ZIP file with multiples files on the fly and send it as an attachment. It uses lazystream and archiver
const lazystream = require('lazystream');
const archiver = require('archiver');
function middleware(req, res) {
// Set the response's headers:
// You can also use res.attachment(...) here.
res.writeHead(200, {
'Content-Type': 'application/zip',
'Content-Disposition': 'attachment; filename=DOWNLOAD_NAME.zip',
});
// Files to add in the ZIP:
const filesToZip = [
'assets/file1',
'assets/file2',
];
// Create a new ZIP file:
const zip = archiver('zip');
// Set up some callbacks:
zip.on('error', errorHandler);
zip.on('finish', function() {
res.end(); // Send the response once ZIP is finished.
});
// Pipe the ZIP output to res:
zip.pipe(res);
// Add files to ZIP:
filesToZip.map((filename) => {
zip.append(new lazystream.Readable(() => fs
.createReadStream(filename), {
name: filename,
});
});
// Finalize the ZIP. Compression will start and output will
// be piped to res. Once ZIP is finished, res.end() will be
// called.
zip.finalize();
}
You can build around this to cache the built ZIPs instead of building them on the fly every time, which is time and resource consuming and totally unadvisable for most uses cases.

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)

Categories

Resources