I'm trying to trigger a download from a POST request handler in Koa with koa-router. Essentially, I'm trying to do something like this:
app.js
const Koa = require('koa')
const router = require('./router')
const app = new Koa()
app.use(router.routes())
app.use(router.allowedMethods())
app.listen(3000)
router.js
const fs = require('fs')
const Router = require('koa-router')
const router = new Router()
router.post('/generate', function * () {
const path = `${__dirname}/test.txt`
this.body = fs.createReadStream(path)
this.set('Content-disposition', 'attachment; filename= test.txt')
})
module.exports = router
client.js
const { fetch } = window;
const request = {
method: 'POST',
body: JSON.stringify({ fake: 'data' })
}
// Make the POST request
fetch('/generate', request)
However, when the POST request is sent, nothing happens. I don't get any error in the server console or the browser console either. Any help would be appreciated!
You should set file stream in the body and send Content-disposition to attachment with that file name. Use below code
const Router = require('koa-router');
const router = new Router();
router.post('/generate', function * () {
const path = `${__dirname}/file.txt`;
this.body = fs.createReadStream(path);
this.set('Content-disposition', 'attachment; filename= file.txt');
});
module.exports = router;
UPDATE: Complete working code:
var app = require('koa')();
var router = require('koa-router')();
const fs = require('fs');
router.post('/generate', function () {
const path = `${__dirname}/file.txt`;
this.body = fs.createReadStream(path);
this.set('Content-disposition', 'attachment; filename= file.txt');
});
app
.use(router.routes())
.use(router.allowedMethods());
app.listen(3000);
Client:
<button id="btnDownload">Download</button>
<script type="text/javascript">
const request = {
method: 'POST',
body: JSON.stringify({
fake: 'data'
})
}
document.getElementById('download').onclick = () => {
fetch('/generate', request)
.then(res => {
return res.text()
})
.then(content => {});
}
</script>
You could try using https://github.com/koajs/send
router.post('/generate', function * (next) {
yield send(this, 'file.txt');
});
And in client side, you'll need to create and trigger download upon receiving file content via post request. Put this code in request callback
fetch('/generate', request)
.then(res => { return res.text() })
.then(content => {
uriContent = "data:application/octet-stream," + encodeURIComponent(content);
newWindow = window.open(uriContent, 'somefile');
});
same functionality can be achieved using a tag download.I prefer this.It works without JS but not in safari.
<!DOCTYPE html>
<html>
<body>
<p>Click on the w3schools logo to download the image:<p>
<a href="http://www.w3schools.com/images/myw3schoolsimage.jpg" download>
<img border="0" src="http://www.w3schools.com/images/myw3schoolsimage.jpg" alt="W3Schools" width="104" height="142">
</a>
<p><b>Note:</b> The download attribute is not supported in Edge version 12, IE, Safari or Opera version 12 (and earlier).</p>
</body>
</html>
refernce:
http://www.w3schools.com/tags/att_a_download.asp
Related
Learning nodejs and trying to post a form from HTML (with image upload) to nodejs (express) but the request.body returning empty object.
Tried few solutions on this site but no one is working.
Here is my code for creating a dynamic form. (HTML)
function show(data) {
const d = data.temp_form;
let content = ''
// console.log(d)
d.forEach((item) => {
if (item === 'image_backup' || item === 'image_banner') {
content += `<label for='${item}'>${item}</label><input name='${item}' type='file' id='${item}' value=''><br/>`
}else{
content += `<label for='${item}'>${item}</label><input name='${item}' type='text' id='${item}' value=''><br/>`
}
})
content += ` <input type="submit" id="handle_submit">`
getFormContianer.innerHTML = content
}
Code handling form submit
async function handleForm(e) {
e.preventDefault();
let dataForm = new FormData(e.target)
let obj = {}
dataForm.forEach((value, key) => {
obj[key] = value
if( typeof value === 'object'){
console.log(value.name)
obj[key] = value.name
}
});
let data = JSON.stringify(obj);
await fetch(file_api, {
method: 'POST',
body: data
}).then((res)=>{
return res.json();
}).then((data)=>{
console.log('api err: '+data);
}).catch((err) =>{
console.log('api err: '+ err)
})
}
Then, in my nodejs
const express = require('express');
const cors = require('cors');
const config = require('./config')
var bodyParser = require('body-parser');
var multer = require('multer');
var upload = multer();
const app = express()
const templates = require('./routes/templates-routes');
const files = require('./routes/files-routes');
app.use(cors());
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({ extended: true }));
app.use(upload.array());
app.use(express.static('public'))
app.use('/api', templates.routes);
app.use('/create', files.routes);
app.listen(config.port, () => {
console.log(`Example app listening at http://localhost:${config.port}`)
})
and in the route.js
const express = require('express');
const router = express.Router();
const {replaceValue } = require('../controllers/filesController');
router.post('/file', replaceValue);
module.exports={
routes: router
}
for the fileController.js
const replaceValue = (request, response) =>{
console.log(request.body)
response.send(request.body)}
Hope that can get some comment for you, thank you so much!
let data = JSON.stringify(obj);
await fetch(file_api, {
method: 'POST',
body: data
You are passing a string to body and haven't specified a Content-Type header so fetch will generate a Content-Type: text/plain header.
Since plain text isn't JSON, the JSON parsing middleware you have set up in Express won't process it.
await fetch(file_api, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: data
Note that this will make it a preflighted request, so make sure you follow the instructions for the CORS module to support that.
This is the GET request in the React frontend. Currently it starts the download and works fine, but I actually want it to open in a new tab and then download the file. I tried adding link.target = '_blank' but that did not work, it just downloaded the file regularly.
download2016(){
var headers = new Headers();
headers.append('Content-Type','application/pdf')
fetch("http://localhost:5000/download/report/2016", {
method: "get",
headers: headers,
}).then((response) => response.blob())
.then((blob) => {
const url = window.URL.createObjectURL(
new Blob([blob]),
);
const link = document.createElement('a');
link.href = url;
link.setAttribute(
'download',
'Mypdfname.pdf'
);
document.body.appendChild(link);
link.click();
link.parentNode.removeChild(link);
});
}
This is the server side route code for the GET request
const router = require('express').Router();
const fs = require('fs');
const path = require('path');
router.get('/report/2016',function(req, res){
var tempfile = __dirname + '\\mypdfname.pdf';
console.log(tempfile);
fs.readFile(tempfile,function (err,data){
res.contentType("application/pdf");
res.send(data);
console.log("Visitor downloading 2016 file");
});
});
module.exports = router;
Any help would be appreciated. Have a nice day
Instead of sending buffer to the front end try this.
Front end -
download2016(){
window.open("http://localhost:5000/download/report/2016")
}
Backend Code -
const router = require('express').Router();
const fs = require('fs');
const path = require('path');
router.get('/report/2016',function(req, res){
var tempfile = __dirname + '\\mypdfname.pdf';
res.download(tempfile, function(err) {
if(err) {
console.log('Something went wrong : ', err)
} else {
console.log('File downloaded.')
}
});
});
module.exports = router;
I managed to store files on my pc using the HTML form action attribute and then handling this request on my Express webserver.
When I now try to replace this with an eventlistener on the submit button of the form instead of using the action attribute to send the post request I can not get it to work.
I get a error message 400 bad request.
Fetch
let form = document.querySelector('#uploadForm')
let inpFile = document.querySelector('#inpFile')
form.addEventListener('submit', async event => {
event.preventDefault()
const formData = new FormData()
formData.append('inpFile', inpFile.files[0])
fetch('http://myip/upload', {
method: 'POST',
headers: {
'Content-Type' : 'multipart/form-data'
},
body: formData
}).catch(console.error)
})
HTML
<form ref='uploadForm'
id='uploadForm'
method='post'
encType="multipart/form-data">
<input type="file" name="sampleFile" id="inpFile" />
<input type='submit' value='Submit' />
</form>
Express Server
const express = require('express')
const app = express();
const path = require('path')
const things = require('./routes/things')
const fileUpload = require('express-fileupload')
app.post('/upload', (req, res) => {
let sampleFile = req.files.sampleFile
sampleFile.mv(__dirname + '\\files\\' + sampleFile.name, (err) => {
if (err)
return res.status(500).send(err)
res.send('File uploaded!')
})
})
According to your html and fetch code your express code should looks like this:
const express = require('express')
const app = express();
const path = require('path')
const things = require('./routes/things')
const fileUpload = require('express-fileupload')
app.use('/upload', fileUpload({
createParentPath: true
}));
app.post('/upload', (req, res) => {
const { inpFile } = req.files;
inpFile.mv(path.join(__dirname, 'files', inpFile.name))
.then(() => res.send('File uploaded!'))
.catch(err => res.status(500).send(err));
})
You need to bind middleware to the application:
app.use('/upload', fileUpload({
createParentPath: true
}));
And your file object should be in req.files.inpFile.
Also you need to remove headers from your fetch request.
I want to download an image file with nodeJS, by using an API, but the problem is the API link doesn't have .jpg file in the end, how do I do,
below is how I am trying
url = 'https://i.pravatar.cc/225'
const https = require('https')
const fs = require('fs');
result = https.get(url, (resp) => {
console.log('Result of response: ', resp)
fs.writeFileSync('apiResponse', resp)
console.log('Reached end!')
})
When I click the URL it shows the image in browser, how do make my program to write the file on hard-drive,
This code uploads several different pictures
const url = 'https://i.pravatar.cc/225'
const https = require('https')
const fs = require('fs');
for(let i=0; i<10; i++)
https.get(url, resp => resp.pipe(fs.createWriteStream(`./test_${i}.jpeg`)));
Just pipe response to file
const url = 'https://i.pravatar.cc/225'
const https = require('https')
const fs = require('fs');
https.get(url, resp => resp.pipe(fs.createWriteStream('./test.jpeg')));
please use this I have try with it and working fine you can rename the downloded file too.
const https = require("https");
const fs = require("fs");
const file = fs.createWriteStream("file.jpg");
const request = https.get("https://i.pravatar.cc/225", function(response) {
response.pipe(file);
});
Try download-file library
https://www.npmjs.com/package/download-file
Install : npm install download-file --save
var download = require('download-file')
var url = "http://i.imgur.com/G9bDaPH.jpg"
var options = {
directory: "./images/cats/",
filename: "cat.gif"
}
download(url, options, function(err){
if (err) throw err
console.log("meow")
})
In my api router, there is a function called generatePDF which aims to use PDFKit module to generate a PDF file in memory and send to client for download instead of displaying only.
In api.js:
var express = require('express');
var router = express.Router();
const PDFDocument = require('pdfkit');
router.get('/generatePDF', async function(req, res, next) {
var myDoc = new PDFDocument({bufferPages: true});
myDoc.pipe(res);
myDoc.font('Times-Roman')
.fontSize(12)
.text(`this is a test text`);
myDoc.end();
res.writeHead(200, {
'Content-Type': 'application/pdf',
'Content-disposition': 'attachment;filename=test.pdf',
'Content-Length': 1111
});
res.send( myDoc.toString('base64'));
});
module.exports = router;
This does not work. The error message is (node:11444) UnhandledPromiseRejectionWarning: Error [ERR_HTTP_HEADERS_SENT]: Cannot set headers after they are sent to the client.
How can I go about fixing the issue and getting it work?
Also, a relevant question would be how I can separate the business logic of PDF generation from the router and chain them up?
Complete solution.
var express = require('express');
var router = express.Router();
const PDFDocument = require('pdfkit');
router.get('/generatePDF', async function(req, res, next) {
var myDoc = new PDFDocument({bufferPages: true});
let buffers = [];
myDoc.on('data', buffers.push.bind(buffers));
myDoc.on('end', () => {
let pdfData = Buffer.concat(buffers);
res.writeHead(200, {
'Content-Length': Buffer.byteLength(pdfData),
'Content-Type': 'application/pdf',
'Content-disposition': 'attachment;filename=test.pdf',})
.end(pdfData);
});
myDoc.font('Times-Roman')
.fontSize(12)
.text(`this is a test text`);
myDoc.end();
});
module.exports = router;
First I recommend to create a service for the PDF kit. And then a Controller to the route that you want.
I used get-stream to make this easier.
It also answers your question to the accepted answer:
how I can separate the business logic of PDF generation from the
router and chain them up?
This is my professional solution:
import PDFDocument from 'pdfkit';
import getStream from 'get-stream';
import fs from 'fs';
export default class PdfKitService {
/**
* Generate a PDF of the letter
*
* #returns {Buffer}
*/
async generatePdf() {
try {
const doc = new PDFDocument();
doc.fontSize(25).text('Some text with an embedded font!', 100, 100);
if (process.env.NODE_ENV === 'development') {
doc.pipe(fs.createWriteStream(`${__dirname}/../file.pdf`));
}
doc.end();
const pdfStream = await getStream.buffer(doc);
return pdfStream;
} catch (error) {
return null;
}
}
}
And then the method of the Controller:
(...)
async show(req, res) {
const pdfKitService = new PdfKitService();
const pdfStream = await pdfKitService.generatePdf();
res
.writeHead(200, {
'Content-Length': Buffer.byteLength(pdfStream),
'Content-Type': 'application/pdf',
'Content-disposition': 'attachment;filename=test.pdf',
})
.end(pdfStream);
}
And finally the route:
routes.get('/pdf', FileController.show);
For those how don't want to waste RAM on buffering PDFs and send chunks right away to the client:
const filename = `Receipt_${invoice.number}.pdf`;
const doc = new PDFDocument({ bufferPages: true });
const stream = res.writeHead(200, {
'Content-Type': 'application/pdf',
'Content-disposition': `attachment;filename=${filename}.pdf`,
});
doc.on('data', (chunk) => stream.write(chunk));
doc.on('end', () => stream.end());
doc.font('Times-Roman')
.fontSize(12)
.text(`this is a test text`);
doc.end();
You can use blob stream like this.
reference: https://pdfkit.org/index.html
const PDFDocument = require('pdfkit');
const blobStream = require('blob-stream');
// create a document the same way as above
const doc = new PDFDocument;
// pipe the document to a blob
const stream = doc.pipe(blobStream());
// add your content to the document here, as usual
doc.font('fonts/PalatinoBold.ttf')
.fontSize(25)
.text('Some text with an embedded font!', 100, 100);
// get a blob when you're done
doc.end();
stream.on('finish', function() {
// get a blob you can do whatever you like with
const blob = stream.toBlob('application/pdf');
// or get a blob URL for display in the browser
const url = stream.toBlobURL('application/pdf');
iframe.src = url;
});
pipe all your pdf data to your blob and then write it to a file or url.
or u can store the pdf directly into cloud storage like firebase storage and send download link to client.
If you want to generate pdfs dynamically then you can also try out html-pdf library in node which allows you to create a pdf from html template and add dynamic data in it. Also it is more reliable than pdfkit
https://www.npmjs.com/package/html-pdf
Also refer this link
Generate pdf file using pdfkit and send it to browser in nodejs-expressjs