Servidor de archivos estáticos básico en NodeJS

85

Estoy tratando de crear un servidor de archivos estático en nodejs más como un ejercicio para entender el nodo que como un servidor perfecto. Soy consciente de proyectos como Connect y node-static y tengo la intención de usar esas bibliotecas para obtener más código listo para producción, pero también me gusta comprender los conceptos básicos de lo que estoy trabajando. Con eso en mente, he codificado un pequeño 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);

Mi pregunta es doble

  1. ¿Es esta la forma "correcta" de crear y transmitir html básico, etc. en el nodo o hay un método mejor / más elegante / más robusto?

  2. ¿El .pipe () en el nodo básicamente está haciendo lo siguiente?

.

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

¡Gracias a todos!

bofetada
fuente
2
Escribí un módulo que te permite hacer eso sin comprometer la flexibilidad. También almacena automáticamente en caché todos sus recursos. Échale un vistazo: github.com/topcloud/cachemere
Jon
2
Es un poco curioso que elija (?) Para devolver '404 Not Found' con el código de estado HTTP '200 OK'. Si no hay ningún recurso en la URL, entonces el código apropiado debería ser 404 (y lo que escriba en el cuerpo del documento suele tener una importancia secundaria). De lo contrario, confundirá a muchos agentes de usuario (incluidos los rastreadores web y otros bots) dándoles documentos sin valor real (que también pueden almacenar en caché).
amn
1
Gracias. Todavía funciona muy bien muchos años después.
statosdotcom
1
¡Gracias! este código funciona perfectamente. Pero ahora use en fs.exists()lugar de path.exists()en el código anterior. ¡Salud! ¡y si! no olvides return:
Kaushal28
NOTA : 1) fs.exists() está en desuso . Uso fs.access()o incluso mejor que para el caso de uso anterior, fs.stat(). 2) url.parse está en desuso ; utilice la new URLinterfaz más nueva en su lugar.
rags2riches

Respuestas:

44
  • Su servidor básico se ve bien, excepto:

    Falta una returndeclaración.

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

    Y:

    res.writeHead(200, mimeType);

    debiera ser:

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

  • pipe(), básicamente hace eso, también pausa / reanuda el flujo de origen (en caso de que el receptor sea más lento). Aquí está el código fuente de la pipe()función: https://github.com/joyent/node/blob/master/lib/stream.js

Stewe
fuente
2
¿Qué pasará si el nombre del archivo es como blah.blah.css?
ShrekOverflow
2
mimeType será bla en ese caso xP
ShrekOverflow
5
¿No es ese el problema? si escribe el suyo, está preguntando por este tipo de errores. Buen ejercicio de aprendizaje, pero estoy aprendiendo a apreciar "conectar" en lugar de rodar por mi cuenta. El problema con esta página es que la gente está buscando solo para averiguar cómo hacer un servidor de archivos simple y el desbordamiento de pila aparece primero. Esta respuesta es correcta, pero la gente no la busca, solo una respuesta simple. Tuve que encontrar el más simple yo mismo, así que póngalo aquí.
Jason Sebring
1
+1 por no pegar un enlace a una solución en forma de biblioteca, sino escribir una respuesta a la pregunta.
Shawn Whinnery
57

Menos es más

Simplemente vaya al símbolo del sistema primero en su proyecto y use

$ npm install express

Luego escribe tu código app.js así:

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

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

Luego, crearía una carpeta "pública" donde colocaría sus archivos. Primero lo intenté de la manera más difícil, pero tienes que preocuparte por los tipos de mímica, que es simplemente tener que mapear cosas, lo que lleva mucho tiempo y luego preocuparte por los tipos de respuesta, etc., etc., etc., no, gracias.

Jason Sebring
fuente
2
+1 Hay mucho que decir sobre el uso de código probado en lugar de desarrollar el suyo propio.
jcollum
1
Intenté mirar la documentación, pero parece que no puedo encontrar mucho, ¿puedes explicar qué está haciendo tu fragmento? Traté de usar esta variación en particular y no sé qué se puede reemplazar con qué.
onaclov2000
3
Si desea la lista de directorios, simplemente agregue .use (connect.directory ('public')) justo después de la línea connect.static, reemplazando public, con su ruta. Perdón por el secuestro, pero creo que me aclara las cosas.
onaclov2000
1
¡También podría 'Usar jQuery'! Esta no es una respuesta a la pregunta del OP, sino una solución a un problema que ni siquiera existe. OP declaró que el objetivo de este experimento era aprender Node.
Shawn Whinnery
1
@JasonSebring ¿Por qué require('http')en la segunda línea?
Xiao Peng - ZenUML.com
19

También me gusta entender lo que sucede bajo el capó.

Noté algunas cosas en tu código que probablemente quieras limpiar:

  • Se bloquea cuando el nombre de archivo apunta a un directorio, porque existe es verdadero e intenta leer un flujo de archivo. Usé fs.lstatSync para determinar la existencia del directorio.

  • No está usando los códigos de respuesta HTTP correctamente (200, 404, etc.)

  • Mientras se determina MimeType (a partir de la extensión del archivo), no se configura correctamente en res.writeHead (como señaló Stewe)

  • Para manejar caracteres especiales, probablemente desee anular el escape del uri

  • Sigue ciegamente enlaces simbólicos (podría ser un problema de seguridad)

Dado esto, algunas de las opciones de Apache (FollowSymLinks, ShowIndexes, etc.) comienzan a tener más sentido. Actualicé el código para su servidor de archivos simple de la siguiente manera:

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);
Jeff Ward
fuente
4
¿Puedo sugerir "var mimeType = mimeTypes [path.extname (nombre de archivo) .split (". "). reverse () [0]];" ¿en lugar? algunos nombres de archivo tienen más de un "." p. ej., "my.cool.video.mp4" o "download.tar.gz"
no sincronizado el
¿Esto de alguna manera impide que alguien use una URL como la carpeta /../../../ home / user / jackpot.privatekey? Veo la combinación para asegurarme de que la ruta sea descendente, pero me pregunto si usar el tipo de notación ../../../ solucionará eso o no. Quizás lo probaré yo mismo.
Reynard
No funciona. No estoy seguro de por qué, pero es bueno saberlo.
Reynard
agradable, una coincidencia de RegEx también puede recopilar la extensión; var mimeType = mimeTypes[path.extname(filename).match(/\.([^\.]+)$/)[1]];
John Mutuma
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]))
Chí Nguyễn
fuente
4
Quizás quiera explicar esto un poco más.
Nathan Tuggy
3

¿Qué tal este patrón, que evita verificar por separado que el archivo existe?

        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);
Aerik
fuente
1
olvidó el tipo MIME en caso de éxito. Estoy usando este diseño, pero en lugar de canalizar inmediatamente los flujos, los estoy canalizando en el evento 'abierto' del flujo de archivos: writeHead para el tipo mime, luego canalizar. El final no es necesario: legible.pipe .
GeH
Modificado según el comentario de @GeH.
Brett Zamir
Debería serfileStream.on('open', ...
Petah
0

el módulo st facilita el servicio de archivos estáticos. Aquí hay un extracto de 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)
kaore
fuente
0

La respuesta de @JasonSebring me indicó la dirección correcta, sin embargo, su código está desactualizado. Así es como se hace con la connectversión más reciente.

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);

En el connect repositorio de GitHub hay otros middlewares que puede usar.

ffleandro
fuente
En su lugar, solo usé express para una respuesta más simple. La versión express más nueva tiene la estática incorporada, pero no mucho más. ¡Gracias!
Jason Sebring
Mirando la connectdocumentación, es solo un wrapperfor middleware. Todos los demás interesantes middlewareprovienen del expressrepositorio, por lo que técnicamente podría usar esas API usando el express.use().
ffleandro