Basic static file server in NodeJS
Asked Answered
S

8

87

I'm trying to create a static file server in nodejs more as an exercise to understand node than as a perfect server. I'm well aware of projects like Connect and node-static and fully intend to use those libraries for more production-ready code, but I also like to understand the basics of what I'm working with. With that in mind, I've coded up a small server.js:

var http = require('http'),
    url = require('url'),
    path = require('path'),
    fs = require('fs');
var mimeTypes = {
    "html": "text/html",
    "jpeg": "image/jpeg",
    "jpg": "image/jpeg",
    "png": "image/png",
    "js": "text/javascript",
    "css": "text/css"};

http.createServer(function(req, res) {
    var uri = url.parse(req.url).pathname;
    var filename = path.join(process.cwd(), uri);
    path.exists(filename, function(exists) {
        if(!exists) {
            console.log("not exists: " + filename);
            res.writeHead(200, {'Content-Type': 'text/plain'});
            res.write('404 Not Found\n');
            res.end();
        }
        var mimeType = mimeTypes[path.extname(filename).split(".")[1]];
        res.writeHead(200, mimeType);

        var fileStream = fs.createReadStream(filename);
        fileStream.pipe(res);

    }); //end path.exists
}).listen(1337);

My question is twofold

  1. Is this the "right" way to go about creating and streaming basic html etc in node or is there a better/more elegant/more robust method ?

  2. Is the .pipe() in node basically just doing the following?

.

var fileStream = fs.createReadStream(filename);
fileStream.on('data', function (data) {
    res.write(data);
});
fileStream.on('end', function() {
    res.end();
});

Thanks everyone!

Selection answered 1/9, 2011 at 8:52 Comment(5)
I wrote a module that lets you do that without compromsing flexibility. It also automatically caches all your resources. Check it out: github.com/topcloud/cachemereObadias
A bit funny that you choose(?) to return '404 Not Found' with HTTP status code '200 OK'. If there is no resource to be found at the URL, then the appropriate code should be 404 (and what you write in document body is usually of secondary importance). You will otherwise be confusing a lot of user agents (including web crawlers and other bots) giving them documents with no real value (which they also may cache).Epstein
Thanks. Still working nicely many years after.Agnail
Thanks! this code is working perfectly. But now use fs.exists() instead of path.exists() in above code. Cheers! and yeah! don't forget return:Styptic
NOTE: 1) fs.exists() is deprecated. Use fs.access() or even better as for the above use case, fs.stat(). 2) url.parse is deprecated; use the newer new URL Interface instead.M16
S
45
  • Your basic server looks good, except:

    There is a return statement missing.

    res.write('404 Not Found\n');
    res.end();
    return; // <- Don't forget to return here !!
    

    And:

    res.writeHead(200, mimeType);

    should be:

    res.writeHead(200, {'Content-Type':mimeType});

  • Yes pipe() does basically that, it also pauses/resumes the source stream (in case the receiver is slower). Here is the source code of the pipe() function: https://github.com/joyent/node/blob/master/lib/stream.js

Sorcim answered 1/9, 2011 at 9:28 Comment(4)
what will happen if file name is like blah.blah.css ?Bluhm
mimeType shall be blah in that case xPBluhm
Isn't that the rub though? if you write your own, you are asking for these types of bugs. Good learning excerise but I am learning to appreciate "connect" rather than rolling my own. The problem with this page is people are looking just to find out how to do a simple file server and stack overflow comes up first. This answer is right but people aren't looking for it, just a simple answer. I had to find out the simpler one myself so put it here.Warta
+1 for not pasting a link to a solution in the form of a library but actually writing an answer the question.Sidewheel
W
57

Less is more

Just go command prompt first on your project and use

$ npm install express

Then write your app.js code like so:

var express = require('express'),
app = express(),
port = process.env.PORT || 4000;

app.use(express.static(__dirname + '/public'));
app.listen(port);

You would then create a "public" folder where you place your files. I tried it the harder way first but you have to worry about mime types which is just having to map stuff which is time consuming and then worry about response types, etc. etc. etc.... no thank you.

Warta answered 18/8, 2012 at 15:9 Comment(9)
+1 There's a lot to be said for using tested code instead of rolling your own.Miquelon
I tried looking at the documentation, but can't seem to find much, can you explain what your snippet is doing? I tried to use this particular variation and I don't know what can be replaced with what.Alister
To clarify, change 'public' to the path where your own folder of files is, then to view the file check http://<yourip>:3000/<filename> if you don't enter filename, you will get a Cannot Get / (I thought it would list the directory but it turns out it doesn't.Alister
If you want the directory listing, simply add .use(connect.directory('public')) right after the connect.static line, replacing public, with your path. Sorry for the hijacking, but I think it clears things up for me.Alister
You might as well 'Use jQuery'! This is not an aswer to the OP's question but a solution to a problem that doesn't even exist. OP stated that the point of this experiment was to learn Node.Sidewheel
@ShawnWhinnery is correct about not answering the fine print except about using jQuery :) but most people are like me and searched on Google for a simple file server in node.js. I think its the fault of the questioner to lead people in that way given the title he put. Hence, the votes I received as more people were similar to myself.Warta
@JasonSebring Why require('http') at the second line?Imponderabilia
This answer is outdated.Sibby
There is also a lot to be said about understanding the internals and rolling your own code instead of using perfectly valid and tested code that was designed in someone elses mind according to their understanding of what good abstractions and good architecture are.Epstein
S
45
  • Your basic server looks good, except:

    There is a return statement missing.

    res.write('404 Not Found\n');
    res.end();
    return; // <- Don't forget to return here !!
    

    And:

    res.writeHead(200, mimeType);

    should be:

    res.writeHead(200, {'Content-Type':mimeType});

  • Yes pipe() does basically that, it also pauses/resumes the source stream (in case the receiver is slower). Here is the source code of the pipe() function: https://github.com/joyent/node/blob/master/lib/stream.js

Sorcim answered 1/9, 2011 at 9:28 Comment(4)
what will happen if file name is like blah.blah.css ?Bluhm
mimeType shall be blah in that case xPBluhm
Isn't that the rub though? if you write your own, you are asking for these types of bugs. Good learning excerise but I am learning to appreciate "connect" rather than rolling my own. The problem with this page is people are looking just to find out how to do a simple file server and stack overflow comes up first. This answer is right but people aren't looking for it, just a simple answer. I had to find out the simpler one myself so put it here.Warta
+1 for not pasting a link to a solution in the form of a library but actually writing an answer the question.Sidewheel
R
21

I like understanding what's going on under the hood as well.

I noticed a few things in your code that you probably want to clean up:

  • It crashes when filename points to a directory, because exists is true and it tries to read a file stream. I used fs.lstatSync to determine directory existence.

  • It isn't using the HTTP response codes correctly (200, 404, etc)

  • While MimeType is being determined (from the file extension), it isn't being set correctly in res.writeHead (as stewe pointed out)

  • To handle special characters, you probably want to unescape the uri

  • It blindly follows symlinks (could be a security concern)

Given this, some of the apache options (FollowSymLinks, ShowIndexes, etc) start to make more sense. I've update the code for your simple file server as follows:

var http = require('http'),
    url = require('url'),
    path = require('path'),
    fs = require('fs');
var mimeTypes = {
    "html": "text/html",
    "jpeg": "image/jpeg",
    "jpg": "image/jpeg",
    "png": "image/png",
    "js": "text/javascript",
    "css": "text/css"};

http.createServer(function(req, res) {
  var uri = url.parse(req.url).pathname;
  var filename = path.join(process.cwd(), unescape(uri));
  var stats;

  try {
    stats = fs.lstatSync(filename); // throws if path doesn't exist
  } catch (e) {
    res.writeHead(404, {'Content-Type': 'text/plain'});
    res.write('404 Not Found\n');
    res.end();
    return;
  }


  if (stats.isFile()) {
    // path exists, is a file
    var mimeType = mimeTypes[path.extname(filename).split(".").reverse()[0]];
    res.writeHead(200, {'Content-Type': mimeType} );

    var fileStream = fs.createReadStream(filename);
    fileStream.pipe(res);
  } else if (stats.isDirectory()) {
    // path exists, is a directory
    res.writeHead(200, {'Content-Type': 'text/plain'});
    res.write('Index of '+uri+'\n');
    res.write('TODO, show index?\n');
    res.end();
  } else {
    // Symbolic link, other?
    // TODO: follow symlinks?  security?
    res.writeHead(500, {'Content-Type': 'text/plain'});
    res.write('500 Internal server error\n');
    res.end();
  }

}).listen(1337);
Reede answered 28/8, 2012 at 17:58 Comment(4)
can i suggest "var mimeType = mimeTypes[path.extname(filename).split(".").reverse()[0]];" instead? some filenames have more than one "." eg "my.cool.video.mp4" or "download.tar.gz"Liberalism
Does this somehow stop someone from using a url like folder/../../../home/user/jackpot.privatekey? I see the join to ensure the path is downstream, but I'm wondering if using the ../../../ type of notation will get around that or not. Perhaps I'll test it myself.Butta
It does not work. I'm not sure why, but that's nice to know.Butta
nice, a RegEx match can also collect the extension; var mimeType = mimeTypes[path.extname(filename).match(/\.([^\.]+)$/)[1]];Militiaman
A
4
var http = require('http')
var fs = require('fs')

var server = http.createServer(function (req, res) {
  res.writeHead(200, { 'content-type': 'text/plain' })

  fs.createReadStream(process.argv[3]).pipe(res)
})

server.listen(Number(process.argv[2]))
Ambur answered 14/5, 2015 at 3:32 Comment(1)
Might want to explain this a bit more.Plano
B
3

How about this pattern, which avoids checking separately that the file exists

        var fileStream = fs.createReadStream(filename);
        fileStream.on('error', function (error) {
            response.writeHead(404, { "Content-Type": "text/plain"});
            response.end("file not found");
        });
        fileStream.on('open', function() {
            var mimeType = mimeTypes[path.extname(filename).split(".")[1]];
            response.writeHead(200, {'Content-Type': mimeType});
        });
        fileStream.on('end', function() {
            console.log('sent file ' + filename);
        });
        fileStream.pipe(response);
Bridges answered 18/2, 2014 at 16:46 Comment(3)
you forgot the mimetype in case of success. I'm using this design, but instead of immediatly piping the streams, I'm piping them in the 'open' event of the filestream : writeHead for the mimetype, then pipe. The end isn't needed : readable.pipe.Allopathy
Modified as per @Allopathy 's comment.Holder
Should be fileStream.on('open', ...Rangy
O
2

I made a httpServer function with extra features for general usage based on @Jeff Ward answer

  1. custtom dir
  2. index.html returns if req === dir

Usage:

httpServer(dir).listen(port);

https://github.com/kenokabe/ConciseStaticHttpServer

Thanks.

Ornie answered 5/8, 2013 at 5:45 Comment(1)
URL seems to be github.com/uplusplus/ConciseStaticHttpServer nowCheyney
F
0

the st module makes serving static files easy. Here is an extract of README.md:

var mount = st({ path: __dirname + '/static', url: '/static' })
http.createServer(function(req, res) {
  var stHandled = mount(req, res);
  if (stHandled)
    return
  else
    res.end('this is not a static file')
}).listen(1338)
Fisherman answered 8/7, 2014 at 9:54 Comment(0)
S
0

@JasonSebring answer pointed me in the right direction, however his code is outdated. Here is how you do it with the newest connect version.

var connect = require('connect'),
    serveStatic = require('serve-static'),
    serveIndex = require('serve-index');

var app = connect()
    .use(serveStatic('public'))
    .use(serveIndex('public', {'icons': true, 'view': 'details'}))
    .listen(3000);

In connect GitHub Repository there are other middlewares you can use.

Sibby answered 5/12, 2014 at 12:4 Comment(2)
I just used express instead for a simpler answer. The newest express version has the static baked in but not much else. Thanks!Warta
Looking at connect documentation, it is only a wrapper for middleware. All the other interesting middleware are from express repository, so technically you could use those APIs using the express.use().Sibby

© 2022 - 2024 — McMap. All rights reserved.