Leer todos los archivos en un directorio, almacenarlos en objetos y enviar el objeto

91

No sé si esto es posible, pero aquí va. Y trabajar con devoluciones de llamada lo hace aún más difícil.

Tengo un directorio con archivos html que quiero enviar al cliente en fragmentos de objetos con node.js y socket.io.

Todos mis archivos están en / tmpl

Entonces, socket necesita leer todos los archivos en / tmpl.

para cada archivo, debe almacenar los datos en un objeto con el nombre del archivo como clave y el contenido como valor.

  var data;
  // this is wrong because it has to loop trough all files.
  fs.readFile(__dirname + '/tmpl/filename.html', 'utf8', function(err, html){
      if(err) throw err;
      //filename must be without .html at the end
      data['filename'] = html;
  });
  socket.emit('init', {data: data});

La devolución de llamada final también es incorrecta. Tiene que ser llamado cuando todos los archivos en el directorio estén listos.

Pero no sé cómo crear el código, ¿alguien sabe si esto es posible?

Saif Bechan
fuente
4
Si el acceso sincrónico está bien, puede omitir el controlador de eventos utilizando los métodos (bloqueo) readfileSyncy readdirSync. nodejs.org/docs/v0.4.8/api/fs.html#fs.readdirSync
rjz
Ok, no sabía nada sobre readdir, eso puede ser útil. Y cuáles son las desventajas del bloqueo. Pensé que el objetivo de node.js era que no bloqueaba. ¿Por qué podemos bloquear de repente?
Saif Bechan
Para devoluciones de llamada asíncronas, lea esto: stackoverflow.com/questions/18983138/… Hay muchas respuestas incorrectas, pero algunas son correctas. Uno de ellos usa contadores.
Vanuan

Respuestas:

169

Entonces, hay tres partes. Leer, almacenar y enviar.

Aquí está la parte de lectura:

var fs = require('fs');

function readFiles(dirname, onFileContent, onError) {
  fs.readdir(dirname, function(err, filenames) {
    if (err) {
      onError(err);
      return;
    }
    filenames.forEach(function(filename) {
      fs.readFile(dirname + filename, 'utf-8', function(err, content) {
        if (err) {
          onError(err);
          return;
        }
        onFileContent(filename, content);
      });
    });
  });
}

Aquí está la parte de almacenamiento:

var data = {};
readFiles('dirname/', function(filename, content) {
  data[filename] = content;
}, function(err) {
  throw err;
});

La parte de envío depende de usted. Es posible que desee enviarlos uno por uno o después de completar la lectura.

Si desea enviar archivos después de completar la lectura, debe usar versiones sincronizadas de fsfunciones o promesas. Las devoluciones de llamada asíncronas no son un buen estilo.

Además, preguntaste sobre la eliminación de una extensión. Debe proceder con las preguntas una por una. Nadie escribirá una solución completa solo para ti.

Stewe
fuente
Gracias, creo que usaré esto. 0===--cAunque una cosa, ¿puedes explicar qué hace?
Saif Bechan
1
Podrías escribirlo como dos líneas c--y luego if (c===0)eso es lo mismo. Sólo disminuye cpor 1y comprueba si se llegó a cero
Stewe
Pero siempre será 0, ¿o no? Agrega 1 en el foreach, y en el mismo foreach elimina 1, por lo que siempre permanece 0, ¿o me equivoco? ¿No es necesario que el cheque sea if(c===files.length)algo así?
Saif Bechan
5
Debido a la naturaleza asíncrona, lo más probable es que el bucle foreach finalice antes de que readFilese devuelva el primer html, por lo que cdebería subir a x(número de archivos) inmediatamente y luego disminuir cuando el html llega desde el disco (que es mucho más tarde)
stewe
1
Oh, eso es algo de lógica profunda, bonita. Tengo mucho que aprender sobre node. ¡Gracias por la ayuda!
Saif Bechan
16

Esta es una Promiseversión moderna de la anterior, que utiliza un Promise.allenfoque para resolver todas las promesas cuando se han leído todos los archivos:

/**
 * Promise all
 * @author Loreto Parisi (loretoparisi at gmail dot com)
 */
function promiseAllP(items, block) {
    var promises = [];
    items.forEach(function(item,index) {
        promises.push( function(item,i) {
            return new Promise(function(resolve, reject) {
                return block.apply(this,[item,index,resolve,reject]);
            });
        }(item,index))
    });
    return Promise.all(promises);
} //promiseAll

/**
 * read files
 * @param dirname string
 * @return Promise
 * @author Loreto Parisi (loretoparisi at gmail dot com)
 * @see http://stackoverflow.com/questions/10049557/reading-all-files-in-a-directory-store-them-in-objects-and-send-the-object
 */
function readFiles(dirname) {
    return new Promise((resolve, reject) => {
        fs.readdir(dirname, function(err, filenames) {
            if (err) return reject(err);
            promiseAllP(filenames,
            (filename,index,resolve,reject) =>  {
                fs.readFile(path.resolve(dirname, filename), 'utf-8', function(err, content) {
                    if (err) return reject(err);
                    return resolve({filename: filename, contents: content});
                });
            })
            .then(results => {
                return resolve(results);
            })
            .catch(error => {
                return reject(error);
            });
        });
  });
}

Cómo usarlo:

Tan simple como hacer:

readFiles( EMAIL_ROOT + '/' + folder)
.then(files => {
    console.log( "loaded ", files.length );
    files.forEach( (item, index) => {
        console.log( "item",index, "size ", item.contents.length);
    });
})
.catch( error => {
    console.log( error );
});

Supongamos que tiene otra lista de carpetas, también puede iterar sobre esta lista, ya que la promesa interna.all resolverá cada una de ellas de forma asincrónica:

var folders=['spam','ham'];
folders.forEach( folder => {
    readFiles( EMAIL_ROOT + '/' + folder)
    .then(files => {
        console.log( "loaded ", files.length );
        files.forEach( (item, index) => {
            console.log( "item",index, "size ", item.contents.length);
        });
    })
    .catch( error => {
        console.log( error );
    });
});

Cómo funciona

El promiseAllhace la magia. Toma un bloque de funciones de firma function(item,index,resolve,reject), donde itemestá el elemento actual en la matriz, indexsu posición en la matriz resolvey rejectlas Promisefunciones de devolución de llamada. Cada promesa se insertará en una matriz en la actual indexy con la actual itemcomo argumentos a través de una llamada de función anónima:

promises.push( function(item,i) {
        return new Promise(function(resolve, reject) {
            return block.apply(this,[item,index,resolve,reject]);
        });
    }(item,index))

Entonces todas las promesas se resolverán:

return Promise.all(promises);
Loretoparisi
fuente
1
Excelente código Loreto, pero ¿por qué no usarlo en return block(item,index,resolve,reject);lugar de return block.apply(this,[item,index,resolve,reject]);, creo que lo applyhace más difícil de entender? ¿Hay algún beneficio del que no tenga conocimiento?
Puntero NULO
1
@NULLpointer gracias. Uno de los beneficios de la aplicación es que puede usar matrices para pasar argumentos y más, puede pasar el contexto donde se definen las variables como self.apply (someObjContext, [arg1, arg2]). En este caso específico, en realidad no lo necesita, pero si está en una biblioteca, este contexto de objeto tal vez sea algo más ...
loretoparisi
10

Para todos los ejemplos a continuación, debe importar fs y módulos de ruta :

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

Leer archivos de forma asincrónica

function readFiles(dir, processFile) {
  // read directory
  fs.readdir(dir, (error, fileNames) => {
    if (error) throw error;

    fileNames.forEach(filename => {
      // get current file name
      const name = path.parse(filename).name;
      // get current file extension
      const ext = path.parse(filename).ext;
      // get current file path
      const filepath = path.resolve(dir, filename);

      // get information about the file
      fs.stat(filepath, function(error, stat) {
        if (error) throw error;

        // check if the current path is a file or a folder
        const isFile = stat.isFile();

        // exclude folders
        if (isFile) {
          // callback, do something with the file
          processFile(filepath, name, ext, stat);
        }
      });
    });
  });
}

Uso:

// use an absolute path to the folder where files are located
readFiles('absolute/path/to/directory/', (filepath, name, ext, stat) => {
  console.log('file path:', filepath);
  console.log('file name:', name);
  console.log('file extension:', ext);
  console.log('file information:', stat);
});

Leer archivos de forma sincrónica, almacenar en matriz, clasificación natural

/**
 * @description Read files synchronously from a folder, with natural sorting
 * @param {String} dir Absolute path to directory
 * @returns {Object[]} List of object, each object represent a file
 * structured like so: `{ filepath, name, ext, stat }`
 */
function readFilesSync(dir) {
  const files = [];

  fs.readdirSync(dir).forEach(filename => {
    const name = path.parse(filename).name;
    const ext = path.parse(filename).ext;
    const filepath = path.resolve(dir, filename);
    const stat = fs.statSync(filepath);
    const isFile = stat.isFile();

    if (isFile) files.push({ filepath, name, ext, stat });
  });

  files.sort((a, b) => {
    // natural sort alphanumeric strings
    // https://stackoverflow.com/a/38641281
    return a.name.localeCompare(b.name, undefined, { numeric: true, sensitivity: 'base' });
  });

  return files;
}

Uso:

// return an array list of objects
// each object represent a file
const files = readFilesSync('absolute/path/to/directory/');

Leer archivos asincrónicos usando promesa

Más información sobre promisificar en este artículo .

const { promisify } = require('util');

const readdir_promise = promisify(fs.readdir);
const stat_promise = promisify(fs.stat);

function readFilesAsync(dir) {
  return readdir_promise(dir, { encoding: 'utf8' })
    .then(filenames => {
      const files = getFiles(dir, filenames);

      return Promise.all(files);
    })
    .catch(err => console.error(err));
}

function getFiles(dir, filenames) {
  return filenames.map(filename => {
    const name = path.parse(filename).name;
    const ext = path.parse(filename).ext;
    const filepath = path.resolve(dir, filename);

    return stat({ name, ext, filepath });
  });
}

function stat({ name, ext, filepath }) {
  return stat_promise(filepath)
    .then(stat => {
      const isFile = stat.isFile();

      if (isFile) return { name, ext, filepath, stat };
    })
    .catch(err => console.error(err));
}

Uso:

readFiles('absolute/path/to/directory/')
  // return an array list of objects
  // each object is a file
  // with those properties: { name, ext, filepath, stat }
  .then(files => console.log(files))
  .catch(err => console.log(err));

Nota: regrese undefinedpara carpetas, si lo desea, puede filtrarlas:

readFiles('absolute/path/to/directory/')
  .then(files => files.filter(file => file !== undefined))
  .catch(err => console.log(err));
pldg
fuente
5

¿Eres una persona perezosa como yo y te encanta el módulo npm : D? Entonces mira esto.

npm install node-dir

ejemplo para leer archivos:

var dir = require('node-dir');

dir.readFiles(__dirname,
    function(err, content, next) {
        if (err) throw err;
        console.log('content:', content);  // get content of files
        next();
    },
    function(err, files){
        if (err) throw err;
        console.log('finished reading files:', files); // get filepath 
   });    
Bimal Grg
fuente
4

Si tiene Node.js 8 o posterior, puede usar el nuevo util.promisify. (Estoy marcando como opcionales las partes del código que tienen que ver con reformatear como un objeto, que solicitó la publicación original).

  const fs = require('fs');
  const { promisify } = require('util');

  let files; // optional
  promisify(fs.readdir)(directory).then((filenames) => {
    files = filenames; // optional
    return Promise.all(filenames.map((filename) => {
      return promisify(fs.readFile)(directory + filename, {encoding: 'utf8'});
    }));
  }).then((strArr) => {
    // optional:
    const data = {};
    strArr.forEach((str, i) => {
      data[files[i]] = str;
    });
    // send data here
  }).catch((err) => {
    console.log(err);
  });
Marcus
fuente
3

Otra versión con el método moderno de Promise. Es más corto que las otras respuestas basadas en Promise:

const readFiles = (dirname) => {

  const readDirPr = new Promise( (resolve, reject) => {
    fs.readdir(dirname, 
      (err, filenames) => (err) ? reject(err) : resolve(filenames))
  });

  return readDirPr.then( filenames => Promise.all(filenames.map((filename) => {
      return new Promise ( (resolve, reject) => {
        fs.readFile(dirname + filename, 'utf-8',
          (err, content) => (err) ? reject(err) : resolve(content));
      })
    })).catch( error => Promise.reject(error)))
};

readFiles(sourceFolder)
  .then( allContents => {

    // handle success treatment

  }, error => console.log(error));
Paul
fuente
agradable y crujiente! Gracias @Paul
Chaos Legion
1

Para que el código funcione sin problemas en diferentes entornos , path.resolve se puede usar en lugares donde se manipula path. Aquí hay un código que funciona mejor.

Parte de lectura:

var fs = require('fs');

function readFiles(dirname, onFileContent, onError) {
  fs.readdir(dirname, function(err, filenames) {
    if (err) {
      onError(err);
      return;
    }
    filenames.forEach(function(filename) {
      fs.readFile(path.resolve(dirname, filename), 'utf-8', function(err, content) {
        if (err) {
          onError(err);
          return;
        }
        onFileContent(filename, content);
      });
    });
  });
}

Almacenar parte:

var data = {};
readFiles(path.resolve(__dirname, 'dirname/'), function(filename, content) {
  data[filename] = content;
}, function(error) {
  throw err;
});
rsa
fuente
1

Acabo de escribir esto y me parece más limpio:

const fs = require('fs');
const util = require('util');

const readdir = util.promisify(fs.readdir);
const readFile = util.promisify(fs.readFile);

const readFiles = async dirname => {
    try {
        const filenames = await readdir(dirname);
        console.log({ filenames });
        const files_promise = filenames.map(filename => {
            return readFile(dirname + filename, 'utf-8');
        });
        const response = await Promise.all(files_promise);
        //console.log({ response })
        //return response
        return filenames.reduce((accumlater, filename, currentIndex) => {
            const content = response[currentIndex];
            accumlater[filename] = {
                content,
            };
            return accumlater;
        }, {});
    } catch (error) {
        console.error(error);
    }
};

const main = async () => {

    const response = await readFiles(
        './folder-name',
    );
    console.log({ response });
};

Puede modificar el responseformato según sus necesidades. El responseformato de este código se verá así:

{
   "filename-01":{
      "content":"This is the sample content of the file"
   },
   "filename-02":{
      "content":"This is the sample content of the file"
   }
}

Sachin Jani
fuente
0

async / await

const { promisify } = require("util")
const directory = path.join(__dirname, "/tmpl")
const pathnames = promisify(fs.readdir)(directory)

try {
  async function emitData(directory) {
    let filenames = await pathnames
    var ob = {}
    const data = filenames.map(async function(filename, i) {
      if (filename.includes(".")) {
        var storedFile = promisify(fs.readFile)(directory + `\\${filename}`, {
          encoding: "utf8",
        })
        ob[filename.replace(".js", "")] = await storedFile
        socket.emit("init", { data: ob })
      }
      return ob
    })
  }

  emitData(directory)
} catch (err) {
  console.log(err)
}

¿Quién quiere probar con generadores?

Isaac Pak
fuente