Javascript - Open PDF blob in browser with a nice looking url - javascript

I am using Node to grab a PDF from the server and send it to my React frontend. Then I want to display that PDF in the browser in a new tab. It's working fairly well, except the URL of the new tab with the PDF is not ideal. The URL of the new tab looks like: blob:http://localhost:3000/71659 but I would like it to look like http://localhost:3000/71659.pdf. No 'blob' and with a pdf extension like when I would click on a pdf on the web just like the examples here: https://file-examples.com/index.php/sample-documents-download/sample-pdf-download/
My current code that handles the saving of the blob and opening it is this:
.then((res) => {
console.log(res);
const file = new Blob([res.data], {
type: 'application/pdf'
});
//Build a URL from the file
const fileURL = URL.createObjectURL(file);
window.open(fileURL, '_blank');
});
And this is my Node route the sends the stream:
router.get('/openPDFFile', async (req, res) => {
console.log('we got to the server!! with: ', req.query);
const pdfFilename = req.query.pdf;
const pdfFilepath = `./path/to/pdf/${pdfFilename}`;
router.get();
const src = fs.createReadStream(pdfFilepath);
res.writeHead(200, {
'Content-Type': 'application/pdf',
'Content-Disposition': 'inline; filename=sample.pdf'
});
src.pipe(res);
});
Now I'm wondering if instead of sending the stream over the wire and converting it to a blob, if I can just simply create a route to that PDF from Node. Something like /PDF/${pdfFilename}. And then my React will just open that URL in a new tab?
Update - Here is my latest Node route based on x00's answer:
router.get('/openPDFFile', async (req, res) => {
console.log('we got to the server!! with: ', req.query);
const pretty_PDF_name = req.query.pdf;
const pdfFilename = (await SDS.getPDFFileName({ pretty_PDF_name }))
.dataValues.sheet_file_name;
console.log('pdfFilename: ', pdfFilename);
const cleanPDFName =
pretty_PDF_name
.substring(0, pretty_PDF_name.length - 4)
.replace(/[ ,.]/g, '') + '.pdf';
const pdfFilepath = '\\path\\to\\file\\' + pdfFilename;
const fullFilePath = path.join(__dirname + '/../' + pdfFilepath);
console.log(cleanPDFName, fullFilePath);
router.get('/pdf/' + cleanPDFName, async (req, res) => {
res.sendFile(fullFilePath);
});
// router.get('/pdf/' + cleanPDFName, express.static(fullFilePath));
// const src = fs.createReadStream(pdfFilepath);
//
// res.writeHead(200, {
// 'Content-Type': 'application/pdf',
// 'Content-Disposition': 'inline; filename=sample.pdf'
// });
//
// src.pipe(res);
// return res.json({ fileuploaded: cleanPDFName });
});
I had seen the express.static way as well and was trying that too.

As I understood from the comments you don't have any special requirements (at least you didn't mention any when answering my comment). So you can just do this:
client
window.open(`/pdf/${pdfFilepath}`, '_blank');
// no need for
// fetch('/openPDFFile', ... pdf: pdfFilepath ... })
// .then(res => ... Blob ... )
// or whatever you where doing
server
router.get('/pdf/:pdfFilename', async (req, res) => {
...
res.sendFile(__dirname + `/path/to/pdf/${req.params.pdfFilename}`)
})
As a result you'll get url in the form of http://localhost:3000/pdf/71659.pdf. Also you can get the url without /pdf/ part, but I don't see any reason for that.
Update
About the colon: see "Route parameters" section here https://expressjs.com/en/guide/routing.html
Full working example:
<!DOCTYPE html>
<html lang="en">
<head></head>
<body>
<div id="get_pdf">Get PDF</div>
</body>
<script>
// here can be your business logic
// for example the name of pdf can be entered by the user through <input>
const pdfFile = "1"
// click will open new window with url = `http://localhost:3000/pdf/1.pdf`
document
.getElementById("get_pdf")
.addEventListener("click", () => {
window.open(`http://localhost:3000/pdf/${pdfFile}.pdf`, '_blank')
// if you want the ".pdf" extension on the url - you must add it yourself
})
</script>
</html>
const express = require('express')
const app = express()
app.get('/pdf/:pdf', async (req, res) => {
const requested_pdf = req.params.pdf // === "1.pdf"
console.log(requested_pdf)
// here can be your business logic for mapping
// requested_pdf from request to filepath of pdf
// or maybe even to generated pdf with no file underneath
// but I'll simply map to some static path
const map_to_pdf_path = name => __dirname + `/path/to/pdf/${name}`
res.sendFile(map_to_pdf_path(requested_pdf))
})
const listener = app.listen(process.env.PORT || constants.server_port, err => {
if (err) return console.error(err)
console.log(`Find the server at: http://localhost:${listener.address().port}`)
})

You can get a pretty filename if you hijack a bit of DOM for your purposes as indicated in this older solution, but you'll hit a number of issues in different browsers. The FileSaver.js project is probably your best bet for a near-universal support for what you're trying to accomplish. It handles blob downloads with names in a cross-browser way, and even offers some fallback options if you need IE <10 support.

This is a EASY way to do it... By no means am I trying to say that this is a good way to do it; however.
You can change the name of the URL after it has loaded using:
window.history.pushState("","","/71659.pdf");
Assuming that you can load the pdf by already going to that url, this is all you would have to do. (you wouldn't want people who are sharing that url to be sharing a broken url) Otherwise, you would need to make a new route that will accept your desired url.
If you want to you could add some error checking to see if the loaded url is the one that you want to change using:
window.location.href

Related

Downloading Image locally from GitHub Raw link using fs.writeFileSync() JS

Currently trying to download image from GitHub locally. Everything seems to work, the fetch goes through with a 200 OK response, however, I don't understand how to store image itself:
const rawGitLink = "https://raw.githubusercontent.com/cardano-foundation/CIPs/master/CIP-0001/CIP_Flow.png"
const folder = "/Folder"
const imageName = "/Test"
const imageResponse = await axios.get(rawGitLink)
fs.writeFileSync(___dirname + folder + imageName, imageResponse, (err) => {
//Error handling
}
)
Four problems had to be fixed:
Image name must include png format for this case
The response must be in the correct format as a buffer for an image
You must write the response data and not the object itself
__dirname only needs two underscores
const rawGitLink = "https://raw.githubusercontent.com/cardano-foundation/CIPs/master/CIP-0001/CIP_Flow.png"
const folder = "/Folder"
const imageName = "/Test.png"
const imageResponse = await axios.get(rawGitLink, { responseType: 'arraybuffer' });
fs.writeFileSync(__dirname + folder + imageName, imageResponse.data)
Axios returns a special object: https://github.com/axios/axios#response-schema
let {data} = await axios.get(...)
await fs.writeFile(filename, data) // you can use fs.promises instead of sync
As #Leau said you should include the extension on the filename
Another sugestion is to use the path module to create the filename:
filename = path.join(__dirname, "/Folder", "Test.png")

Express.js and PDFKit: saving as file works, stream produces empty file

I am trying to create a PDF on a server with express.js and PDFKit. If I save the PDF as a file, it works as expected. However, if I try to send the content to the browser as a stream, I get a PDF, but it is empty (the 3 text blocks are missing).
function createPDF(req, res) {
return new Promise(resolve => {
res.setHeader('content-type', 'application/pdf');
const doc = new PDFDocument;
const stream = doc.pipe(res);
//const stream = doc.pipe(fs.createWriteStream('sample.pdf'));
// end res if pdf document finished
stream.on('finish', () => {
res.end();
resolve(true);
});
// fill pdf
doc.text('text');
doc.text('text');
doc.text('text');
doc.end();
});
}
app.use('/pdf-download', new PdfDownload(options, app), createPDF);
PdfDownload is my class which gets data for the pdf from my database. I'll later access these data by res.data.
Thanks for your help
Marc

Discord.js - 'Not a well formed URL' in embed

In Node.js, I'm trying to send an image as the thumbnail in the top right of an embed. But if I just put the url as it is into the embed.setThumbnail() function, the image loads forever or cannot be loaded. The url would be this one:
https://static-cdn.jtvnw.net/ttv-boxart/./Oddworld:%20Abe%27s%20Oddysee-140x180.jpg
I noticed that the special characters are making problems because urls without those work perfectly fine. So I tried to encode the url with
const querystring = require('querystring');
var boxart_url = 'https://static-cdn.jtvnw.net/ttv-boxart/./Oddworld:%20Abe%27s%20Oddysee-140x180.jpg';
const embed = new Discord.MessageEmbed();
embed.setThumbnail(querystring.stringify(boxart_url));
But this still gives me the same error. The same goes when I try to encode the filename in the url only or when I try to use querystring.escape(boxart_url). So do you know, how to encode the url? :)
Edit: As mentioned by Karizma, I tried to send the embed with just the url as the following:
const boxart_url = "https://static-cdn.jtvnw.net/ttv-boxart/./Oddworld:%20Abe%27s%20Oddysee-140x180.jpg";
const embeded = new Discord.MessageEmbed();
embeded.setThumbnail(boxart_url);
message.channel.send({embed: embeded});
The problem remains the same (https://imgur.com/a/swoSweH)
Edit: After some experimenting, I at least found, that the spaces are not the problem. It's the colon and apostrophy. I tried several stuff with this, like replacing the apostrophy with
var boxart_url = "https://static-cdn.jtvnw.net/ttv-boxart/./" + encodeURIComponent("Oddworld: Abe's Oddysee-140x180.jpg").replace(/'/g, '%23');
or different version of encodeURI() / encodeURIComponent() and I also tried to use a simple object as embed like
const embeded = {
thumbnail: {
url: boxart_url
}
}
but nothing worked so far.
Edit: I came across my own solution. I cannot retrieve the image from the url with the embeds so I download the image locally before and then use this image as attachment for the embed. This seems to work :) i'll share the code here in case somebody else has the same problem.
const request = require('request'); //depricated !
const fs = require('fs');
const directory = "../data/images/boxart/";
var download = function(url, filename, callback){
request.head(url, function(err, res, body){
request(url).pipe(fs.createWriteStream(filename)).on('close', callback);
});
};
download('https://static-cdn.jtvnw.net/ttv-boxart/./Oddworld:%20Abe%27s%20Oddysee-70x99.jpg', directory + 'oddysee.jpg', function(){
console.log('image downloaded');
});
let boxart_url = "data/images/boxart/oddysee.jpg"; // locally
const filename = boxart_url.substring( boxart_url.lastIndexOf('/') + 1 );
const file = new Discord.MessageAttachment(boxart_url);
const embeded = new Discord.MessageEmbed();
embeded.setThumbnail('attachment://'+filename);
message.channel.send({files: [file], embed: embeded});

How to read filename from response - reactJs

I'm downloading a file from a server. There I'm setting a file name which I would like to read on a frontend. Here is how I'm doing it on server:
string fileName = "SomeText" + DateTime.UtcNow.ToString() + ".csv";
return File(stream, "text/csv", fileName);
Basically I will return different types of files, sometimes csv sometimes xlsxand sometimes pdf. So I want to get filename on a frontend so I might save it as it should be for example SomeText.pdf it will be saved automatically on local machine as pdf.
Here is my frontend code:
getFileFromServer(params).then(response => {
console.log('server response download:', response);
const type = response.headers['content-type'];
const blob = new Blob([response.data], { type: type, encoding: 'UTF-8' });
//const url = window.URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = window.URL.createObjectURL(blob);
link.download = 'file.xlsx'; // Here I want to get rid of hardcoded value instead I want filename from server
link.click();
link.remove(); // Probably needed to remove html element after downloading?
});
I saw in Network tab under Response Headers that there is a Content-Disposition which holds that info:
Content-Disposition: attachment; filename="SomeText22/08/2019 10:42:04.csv";
But I don't know is it available in my response and how can I read it in my frontend part so I might replace
link.download = 'file.xlsx';
with Path from a server ..
Thanks a lot guys
Cheers
this is a special type of header so to get this header in frontend the backend person should allow from his end. then you can the headers in the response
response.headers.get("content-disposition")
the below code I have used in my project
downloadReport(url, data) {
fetch("url of api")
.then(async response => {
const filename = response.headers
.get("content-disposition")
.split('"')[1];
const text = await response.text();
return { filename, text };
})
.then(responseText => this.download(responseText))
.catch(error => {
messageNotification("error", error.message);
throw new Error(error.message);
});
}
You can get filename from Content-Disposition header by this way
getFileFromServer(params).then(response => {
// your old code
const [, filename] = response.headers['content-disposition'].split('filename=');
const link = document.createElement('a');
link.download = filename;
// your old code
});
Hope this will help
To get the filename from Content-Disposition on client side(frontend) you must give permission from server side(Backend). Spring users can use
#CrossOrigin(value = {"*"}, exposedHeaders = {"Content-Disposition"})
#Controller
#RequestMapping("some endpoint")
in their controller class.
Then from frontend we can get filename using.
const filename = response.headers['content-disposition'].split('filename=')[1];
Hope this helps. Above solution worked fine for me

Node/Express Generate a one time route / link / download?

How would I go about creating a one time download link in nodeJS or Express?
I'm trying to find the simplest way to accomplish this. My ideas so far are:
Use fs stream to read and then delete the file
or
Somehow generate a link/route that gets removed once the download button is clicked
Are any of these implementations possible?
Is there a simpler way?
Any help or example code would be greatly appreciated!
-Thanks
Check this simple implementation:
You store the information of the download in a file. The filename is the download session id. The file content is the real path of the file to be downloaded.
Use these three functions to manage the lifecycle of the download sessions:
var fs = require('fs');
var crypto = require('crypto');
var path = require('path');
// Path where we store the download sessions
const DL_SESSION_FOLDER = '/var/download_sessions';
/* Creates a download session */
function createDownload(filePath, callback) {
// Check the existence of DL_SESSION_FOLDER
if (!fs.existsSync(DL_SESSION_FOLDER)) return callback(new Error('Session directory does not exist'));
// Check the existence of the file
if (!fs.existsSync(filePath)) return callback(new Error('File doest not exist'));
// Generate the download sid (session id)
var downloadSid = crypto.createHash('md5').update(Math.random().toString()).digest('hex');
// Generate the download session filename
var dlSessionFileName = path.join(DL_SESSION_FOLDER, downloadSid + '.download');
// Write the link of the file to the download session file
fs.writeFile(dlSessionFileName, filePath, function(err) {
if (err) return callback(err);
// If succeeded, return the new download sid
callback(null, downloadSid);
});
}
/* Gets the download file path related to a download sid */
function getDownloadFilePath(downloadSid, callback) {
// Get the download session file name
var dlSessionFileName = path.join(DL_SESSION_FOLDER, downloadSid + '.download');
// Check if the download session exists
if (!fs.existsSync(dlSessionFileName)) return callback(new Error('Download does not exist'));
// Get the file path
fs.readFile(dlSessionFileName, function(err, data) {
if (err) return callback(err);
// Return the file path
callback(null, data);
});
}
/* Deletes a download session */
function deleteDownload(downloadSid, callback) {
// Get the download session file name
var dlSessionFileName = path.join(DL_SESSION_FOLDER, downloadSid + '.download');
// Check if the download session exists
if (!fs.existsSync(dlSessionFileName)) return callback(new Error('Download does not exist'));
// Delete the download session
fs.unlink(dlSessionFileName, function(err) {
if (err) return callback(err);
// Return success (no error)
callback();
});
}
Use createDownload() to create download sessions wherever you need to. It returns the download sid, then you can use it to build your download URL like: http://your.server.com/download?sid=<RETURNED SID>.
Finally you can add a simple handler to your /download route:
app.get('/download', function(req, res, next) {
// Get the download sid
var downloadSid = req.query.sid;
// Get the download file path
getDownloadFilePath(downloadSid, function(err, path) {
if (err) return res.end('Error');
// Read and send the file here...
// Finally, delete the download session to invalidate the link
deleteDownload(downloadSid, function(err) {
// ...
});
});
});
With this method, you don't have to create/move/delete big download files, which could cause slow responses and unnecessary resource consumption.
You can delete routes from the app.routes object. See Remove route mappings in NodeJS Express for more info.
Here is my quick and not very well tested way of doing what you ask:
var express = require('express');
var app = express();
app.get('/download', function(req,res,next){
res.download('./path/to/your.file');
//find this route and delete it.
for(i = 0; i < app.routes.get.length; i++){
if(app.routes.get[i].path === '/download'){
app.routes.get.splice(i,1);
}
}
});
app.listen(80);
I'd probably map a single route to manage downloads, and then upon downloading the file, move or delete it. That way I can prevent a lot of cashing of routes, or a lot of small temp files from the other two answers, but YMMV. Something like this:
// say your downloads are in /downloads
app.get('/dl/:filename', function(req, res) {
var fileStream = fs.createReadStream('/downloads' + req.params.filename);
// error handler, ie. file not there...
fileStream.on('error', function(err) {
if(err) {
res.status(404); // or something
return res.end();
}
});
// here you ow pipe that stream to the response,
fileStream.on('data', downloadHandler);
// and here delete the file or move it to other folder or whatever, do cleanup
fileStream.on('end', deleteFileHandler);
}
Note: This is a possible security vulnerability, it could let the adversary download files outside your downloads location. That filename param is passed directly to fs.

Categories

Resources