Ler todos os arquivos em um diretório, armazená-los em objetos e enviar o objeto

93

Não sei se isso é possível, mas aqui vai. E trabalhar com retornos de chamada torna isso ainda mais difícil.

Eu tenho um diretório com arquivos html que desejo enviar de volta ao cliente em blocos de objeto com node.js e socket.io.

Todos os meus arquivos estão em / tmpl

Portanto, o socket precisa ler todos os arquivos em / tmpl.

para cada arquivo, ele deve armazenar os dados em um objeto com o nome do arquivo como a chave e o conteúdo como o 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});

O retorno de chamada final também está errado. Ele deve ser chamado quando todos os arquivos do diretório estiverem prontos.

Mas não sei criar o código, alguém sabe se isso é possibel?

Saif Bechan
fonte
4
Se o acesso síncrono estiver ok, você pode pular o manipulador de eventos usando os métodos (bloqueio) readfileSynce readdirSync. nodejs.org/docs/v0.4.8/api/fs.html#fs.readdirSync
rjz
Ok, eu não sabia sobre o readdir, isso pode ser útil. E quais são as desvantagens do bloqueio. Eu pensei que o ponto principal do node.js era que ele não bloqueia? Por que podemos bloquear de repente.
Saif Bechan
Para callbacks assíncronos, leia isto: stackoverflow.com/questions/18983138/… Existem muitas respostas erradas, mas algumas estão corretas. Um deles usa contadores.
Vanuan

Respostas:

171

Portanto, existem três partes. Ler, armazenar e enviar.

Aqui está a parte da leitura:

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

Aqui está a parte de armazenamento:

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

O envio é com você. Você pode querer enviá-los um por um ou após a conclusão da leitura.

Se você deseja enviar arquivos após a conclusão da leitura, você deve usar as versões de sincronização do fs funções ou usar promessas. Callbacks assíncronos não é um bom estilo.

Além disso, você perguntou sobre a remoção de uma extensão. Você deve prosseguir com as perguntas uma por uma. Ninguém escreverá uma solução completa só para você.

ensopado
fonte
Obrigado, acho que vou usar isso. Uma coisa tho, você pode explicar o que 0===--cfaz.
Saif Bechan
1
Você pode escrever duas linhas c--e então if (c===0)isso é o mesmo. Ele só diminui cpor 1e verifica se ele chegou a zero
Stewe
Mas sempre será 0, ou não? Você adiciona 1 no foreach e no mesmo foreach remove 1, então ele sempre permanece 0, ou estou errado? O cheque não precisa ser if(c===files.length)algo assim.
Saif Bechan
5
Devido à natureza assíncrona, o loop foreach provavelmente termina antes que o primeiro readFilehtml seja retornado, então cdeve aumentar para x(número de arquivos) imediatamente e, em seguida, diminuir quando o html chega do disco (o que é muito mais tarde)
stewe
1
Oh, essa é uma lógica profunda, boa. Tenho muito que aprender sobre o nó. Obrigado pela ajuda!
Saif Bechan
17

Esta é uma Promiseversão moderna da anterior, usando uma Promise.allabordagem para resolver todas as promessas quando todos os arquivos foram lidos:

/**
 * 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);
            });
        });
  });
}

Como usá-lo:

Tão simples quanto fazer:

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

Suponha que você tenha outra lista de pastas que também pode iterar sobre esta lista, uma vez que a promessa interna. Tudo resolverá cada uma delas de forma assíncrona:

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

Como funciona

O promiseAllfaz a mágica. Leva um bloco de função de assinatura function(item,index,resolve,reject), onde itemé o item atual na matriz, indexsua posição na matriz resolvee rejectas Promisefunções de retorno de chamada. Cada promessa será enviada em uma matriz no atual indexe com o atual itemcomo argumentos por meio de uma chamada de função anônima:

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

Então todas as promessas serão resolvidas:

return Promise.all(promises);
Loretoparisi
fonte
1
Ótimo código Loreto, mas por que não usar em return block(item,index,resolve,reject);vez de return block.apply(this,[item,index,resolve,reject]);, acho que applytorna mais difícil de entender - há algum benefício que eu não esteja ciente?
Ponteiro NULL
1
@NULLpointer obrigado. Um dos benefícios do apply é que você pode usar arrays para passar args e mais você pode passar o contexto onde as variáveis ​​são definidas como self.apply (someObjContext, [arg1, arg2]). Neste caso específico, você não precisa realmente dele, mas se você estiver em uma biblioteca, este contexto de objeto pode ser outra coisa ...
loretoparisi
11

Para todos os exemplos abaixo, você precisa importar módulos fs e path :

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

Leia arquivos de forma assíncrona

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

Leia arquivos de forma síncrona, armazene em array, classificação 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/');

Leia arquivos assíncronos usando promessa

Mais informações sobre promisify neste artigo .

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: volte undefinedpara as pastas, se quiser, pode filtrá-las:

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

Você é uma pessoa preguiçosa como eu e adora o módulo npm : D então veja isso.

npm install node-dir

exemplo para ler arquivos:

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
fonte
4

Se você tiver o Node.js 8 ou posterior, pode usar o novo util.promisify. (Estou marcando como opcionais as partes do código que têm a ver com a reformatação como um objeto, que a postagem original solicitou.)

  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
fonte
3

Outra versão com o método moderno da Promise. É mais curto que as outras respostas com base na promessa:

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));
Paulo
fonte
agradável e nítido! Obrigado @Paul
Chaos Legion
1

Para que o código funcione sem problemas em ambientes diferentes, path.resolve pode ser usado em locais onde o path é manipulado. Aqui está o código que funciona melhor.

Parte de leitura:

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

Armazenando parte:

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

Acabei de escrever isso e parece mais claro para mim:

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

Você pode modificar o responseformato de acordo com sua necessidade. O responseformato deste código será semelhante a:

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

Sachin Jani
fonte
0

assíncrono / esperar

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

Quem quer experimentar geradores?

Isaac Pak
fonte