417

How can I download a file that is in my server to my machine accessing a page in a nodeJS server?

I'm using the ExpressJS and I've been trying this:

app.get('/download', function(req, res){

  var file = fs.readFileSync(__dirname + '/upload-folder/dramaticpenguin.MOV', 'binary');

  res.setHeader('Content-Length', file.length);
  res.write(file, 'binary');
  res.end();
});

But I can't get the file name and the file type ( or extension ). Can anyone help me with that?

KARTHIKEYAN.A
  • 14,686
  • 4
  • 102
  • 112
  • 13
    Just FYI. For using in production, you are better off using node.js behind nginx, and make nginx handle static content. Apparently, it is much better suited for handling that. – Munim Apr 09 '13 at 08:14
  • 4
    The up-votes prove that there is no such thing as a dumb question :) – user2180794 Jun 15 '16 at 03:29
  • 3
    @user2180794 but there is such a thing. Many other questions that get flagged and down-voted are proof of that. This question most certainly isn't one though. It matches the guidelines :) – Assimilater Sep 08 '16 at 02:08
  • The question you point out is different, here OP want to return a file to a client while this other question is about how to download a file using your server Node as a client (e.g a file from a 3rd party). At lesast that's what I understood. – Eric Burel Jun 07 '18 at 19:11

7 Answers7

747

Update

Express has a helper for this to make life easier.

app.get('/download', function(req, res){
  const file = `${__dirname}/upload-folder/dramaticpenguin.MOV`;
  res.download(file); // Set disposition and send it.
});

Old Answer

As far as your browser is concerned, the file's name is just 'download', so you need to give it more info by using another HTTP header.

res.setHeader('Content-disposition', 'attachment; filename=dramaticpenguin.MOV');

You may also want to send a mime-type such as this:

res.setHeader('Content-type', 'video/quicktime');

If you want something more in-depth, here ya go.

var path = require('path');
var mime = require('mime');
var fs = require('fs');

app.get('/download', function(req, res){

  var file = __dirname + '/upload-folder/dramaticpenguin.MOV';

  var filename = path.basename(file);
  var mimetype = mime.lookup(file);

  res.setHeader('Content-disposition', 'attachment; filename=' + filename);
  res.setHeader('Content-type', mimetype);

  var filestream = fs.createReadStream(file);
  filestream.pipe(res);
});

You can set the header value to whatever you like. In this case, I am using a mime-type library - node-mime, to check what the mime-type of the file is.

Another important thing to note here is that I have changed your code to use a readStream. This is a much better way to do things because using any method with 'Sync' in the name is frowned upon because node is meant to be asynchronous.

loganfsmyth
  • 146,797
  • 27
  • 317
  • 241
  • 4
    Thanks.. Is there a way to get this information from the fs.readFileSync? I'm using a static file in this example but I'll use this download api for any files, passing the name of it. – Thiago Miranda de Oliveira Sep 02 '11 at 20:33
  • Setting output filename works with `res.setHeader('Content-disposition', 'attachment; filename=' + filename);` tnx! – Capy Jan 21 '13 at 05:31
  • how to download multiple documents using res.download() method. – R J. Apr 25 '13 at 11:42
  • 1
    @RJ. If you have a question, create a new one, don't leave a comment. – loganfsmyth Apr 25 '13 at 14:34
  • What if you want to use res.download, but change the content type header? I am trying to serve a chrome extension of my site, but the Chrome web store only allows this to work if the content type of the file is 'application/x-chrome-extension'. So, using res.download, is it possible to do that? – frosty Dec 14 '13 at 06:39
  • Please create a new question, comments are for commenting. – loganfsmyth Dec 14 '13 at 17:31
  • Note that it appears that `res.download` does *not* use streaming. – ZachB Jan 11 '14 at 00:24
  • @ZachB What makes you say that? It does as far as I can see. – loganfsmyth Jan 11 '14 at 00:27
  • @loganfsmyth I was reading the source code and it just calls `send`. See similar observations posted here: http://stackoverflow.com/questions/13106096/stream-files-in-node-express-to-client. Would be nice if I'm wrong. – ZachB Jan 11 '14 at 20:14
  • I don't know about older versions of Express, but in `3.x` it just calls `.sendfile`, which uses streaming. https://github.com/visionmedia/express/blob/8a7a695836e6c2fe1145ad100a6eaae6235722e4/lib/response.js#L338-346 – loganfsmyth Jan 12 '14 at 09:38
  • @loganfsmyth Not sure what I was looking at, but you're right -- it does stream. Thanks for correcting me. – ZachB Jan 12 '14 at 18:06
  • @loganfsmyth How can I set the file path with res.download? The file I want to send is a directory level above and __dirname+'../otherdir' isn't correct. – user137717 Dec 18 '15 at 08:51
  • If you have a question, please ask it as a question, don't leave a comment. – loganfsmyth Dec 18 '15 at 17:59
  • 7
    Express 4.x uses `.set()` instead of `.setHeader()` btw – Dana Woodman Mar 17 '16 at 19:37
  • I couldn't do it with the first solution, but the express helper you edited in worked like a charm. Thanks! – Besto Dec 09 '16 at 01:06
  • if you pass in the path, is this method safe for directory traversal attacks? – MikeSchem Jul 24 '18 at 00:00
  • @MikeSchem It is not. – loganfsmyth Jul 24 '18 at 00:46
  • @loganfsmyth I just used this code to get docx on client.I am using angular5 in clent. I request the docx from a http get request.But when i get the file browser throws error. syntax error: uexpected token P at JSON position 0. – rohitwtbs Dec 13 '18 at 12:38
  • @rohitwtbs That sounds like you are treating to file as JSON when it isn't JSON. I can't really say why that would be. – loganfsmyth Dec 13 '18 at 17:31
67

Use res.download()

It transfers the file at path as an “attachment”. For instance:

var express = require('express');
var router = express.Router();

// ...

router.get('/:id/download', function (req, res, next) {
    var filePath = "/my/file/path/..."; // Or format the path using the `id` rest param
    var fileName = "report.pdf"; // The default name the browser will use

    res.download(filePath, fileName);    
});
Jossef Harush Kadouri
  • 28,860
  • 9
  • 119
  • 117
  • 7
    What if the data was coming in from a HTTP request instead of a file and we had to let users download the file in a streaming way? – summerNight Sep 20 '17 at 15:37
  • 3
    @summerNight - well, that is a different case than the question specified. search for `nodejs proxy file download response` for best practice – Jossef Harush Kadouri Sep 20 '17 at 17:13
  • @summerNight i am still looking for this particular solution, if some link is there then plz help – 1UC1F3R616 Jun 29 '20 at 18:20
  • @1UC1F3R616 I ended up solving the problem like this: `router.get(API_PREFIX + '/file-download', function (req, res, next) { var file = process.env.FILE_DOWNLOAD_LOCATION + '/' + req.query.filename res.download(file); });` – summerNight Jul 24 '20 at 16:24
  • @summerNight and @1UC1F3R616 note that you are vulnerable to directory traversal attacks. for instance `https://.../api?filename=../../../keys/my-secret-ssl-key.pem`. to avoid that, you need to validate the query param – Jossef Harush Kadouri Jul 25 '20 at 13:36
20

For static files like pdfs, Word docs, etc. just use Express's static function in your config:

// Express config
var app = express().configure(function () {
    this.use('/public', express.static('public')); // <-- This right here
});

And then just put all your files inside that 'public' folder, for example:

/public/docs/my_word_doc.docx

And then a regular old link will allow the user to download it:

<a href="public/docs/my_word_doc.docx">My Word Doc</a>
jordanb
  • 1,635
  • 15
  • 8
  • 1
    That works well for assets (although a dedicated serving proxy like nginx is recommended). But for anything that requires secured access, the accepted method is better. Generally speaking for docs and files containing information, I wouldn't recommend using the public method. – nembleton Jun 17 '15 at 04:44
  • 1
    you could add middleware to ensure that only appropriate users can access the files – MalcolmOcean Dec 11 '15 at 16:07
  • 1
    e.g. `this.use('/topsecret', mGetLoggedInUser, mEnsureAccess, express.static('topsecret'))` ...and then each request goes through mEnsureAccess. Of course, that means you'll need to be able to figure out a user's access level just based on the url of the secure document, or whatever. – MalcolmOcean Dec 11 '15 at 16:09
19

Here's how I do it:

  1. create file
  2. send file to client
  3. remove file

Code:

let fs = require('fs');
let path = require('path');

let myController = (req, res) => {
  let filename = 'myFile.ext';
  let absPath = path.join(__dirname, '/my_files/', filename);
  let relPath = path.join('./my_files', filename); // path relative to server root

  fs.writeFile(relPath, 'File content', (err) => {
    if (err) {
      console.log(err);
    }
    res.download(absPath, (err) => {
      if (err) {
        console.log(err);
      }
      fs.unlink(relPath, (err) => {
        if (err) {
          console.log(err);
        }
        console.log('FILE [' + filename + '] REMOVED!');
      });
    });
  });
};
Vedran
  • 1,056
  • 1
  • 16
  • 34
  • 2
    this is the only solution i have found in about two days of searching that works for my particular scenario of getting an audio file. the only thing is that i don't think [res.download()](https://expressjs.com/en/api.html#res.download) works with [$.ajax](https://api.jquery.com/jquery.ajax) calls unfortunately - i had to use `window.open("/api/get_audio_file");`, see: https://stackoverflow.com/a/20177012 – user1063287 Jul 14 '19 at 12:51
16

In Express 4.x, there is an attachment() method to Response:

res.attachment();
// Content-Disposition: attachment

res.attachment('path/to/logo.png');
// Content-Disposition: attachment; filename="logo.png"
// Content-Type: image/png
Benoit Blanchon
  • 11,973
  • 4
  • 63
  • 74
7
'use strict';

var express = require('express');
var fs = require('fs');
var compress = require('compression');
var bodyParser = require('body-parser');

var app = express();
app.set('port', 9999);
app.use(bodyParser.json({ limit: '1mb' }));
app.use(compress());

app.use(function (req, res, next) {
    req.setTimeout(3600000)
    res.header('Access-Control-Allow-Origin', '*');
    res.header('Access-Control-Allow-Headers', 'Origin, X-Requested-With, Content-Type, Accept,' + Object.keys(req.headers).join());

    if (req.method === 'OPTIONS') {
        res.write(':)');
        res.end();
    } else next();
});

function readApp(req,res) {
  var file = req.originalUrl == "/read-android" ? "Android.apk" : "Ios.ipa",
      filePath = "/home/sony/Documents/docs/";
  fs.exists(filePath, function(exists){
      if (exists) {     
        res.writeHead(200, {
          "Content-Type": "application/octet-stream",
          "Content-Disposition" : "attachment; filename=" + file});
        fs.createReadStream(filePath + file).pipe(res);
      } else {
        res.writeHead(400, {"Content-Type": "text/plain"});
        res.end("ERROR File does NOT Exists.ipa");
      }
    });  
}

app.get('/read-android', function(req, res) {
    var u = {"originalUrl":req.originalUrl};
    readApp(u,res) 
});

app.get('/read-ios', function(req, res) {
    var u = {"originalUrl":req.originalUrl};
    readApp(u,res) 
});

var server = app.listen(app.get('port'), function() {
    console.log('Express server listening on port ' + server.address().port);
});
KARTHIKEYAN.A
  • 14,686
  • 4
  • 102
  • 112
7

There are several ways to do it This is the better way

res.download('/report-12345.pdf')

or in your case this might be

app.get('/download', function(req, res){
  const file = `${__dirname}/upload-folder/dramaticpenguin.MOV`;
  res.download(file); // Set disposition and send it.
});

Know More

MD SHAYON
  • 7,911
  • 47
  • 35
  • I am baffled at this answer. I started my server with "node server.js". It's running, but I can't seem to get this working. This should download a file into your downloads folder in your computer no? In my setup it just doesn't work. I've been trying to do this for an hour now. – Manny Alvarado Mar 07 '22 at 22:26