2
votes

Node.js comment attendre un appel asynchrone (readdir et stat)

Je travaille sur la méthode de publication côté serveur pour récupérer tous les fichiers dans le répertoire demandé (non récursif), et ci-dessous se trouve mon code.

J'ai des difficultés à renvoyer la réponse ( res.json (pathContent); ) avec le pathContent mis à jour sans utiliser le setTimeout .

Je comprends que c'est en raison du comportement asynchrone des méthodes de système de fichiers utilisées ( readdir et stat ) et doivent utiliser une sorte de technique de rappel, asynchrone ou promesse.

J'ai essayé d'utiliser async.waterfall avec le corps entier de readdir comme une fonction et le res.json (pathContent) comme le autre, mais il n'a pas envoyé le tableau mis à jour côté client.

Je sais qu'il y a eu des milliers de questions concernant cette opération asynchrone mais je n'ai pas pu trouver comment résoudre mon cas après avoir lu le nombre de posts.

Tout commentaire serait apprécié. Merci.

const express = require('express');
const bodyParser = require('body-parser');
const fs = require('fs');
const path = require('path');

var pathName = '';
const pathContent = [];

app.post('/api/files', (req, res) => {
    const newPath = req.body.path;
    fs.readdir(newPath, (err, files) => {
        if (err) {
            res.status(422).json({ message: `${err}` });
            return;
        }
        // set the pathName and empty pathContent
        pathName = newPath;
        pathContent.length = 0;

        // iterate each file
        const absPath = path.resolve(pathName);
        files.forEach(file => {
            // get file info and store in pathContent
            fs.stat(absPath + '/' + file, (err, stats) => {
                if (err) {
                    console.log(`${err}`);
                    return;
                }
                if (stats.isFile()) {
                    pathContent.push({
                        path: pathName,
                        name: file.substring(0, file.lastIndexOf('.')),
                        type: file.substring(file.lastIndexOf('.') + 1).concat(' File'),
                    })
                } else if (stats.isDirectory()) {
                    pathContent.push({
                        path: pathName,
                        name: file,
                        type: 'Directory',
                    });
                }
            });
        });
    });    
    setTimeout(() => { res.json(pathContent); }, 100);
});


2 commentaires

Jetez un œil à cet article, on dirait qu'ils ont utilisé les méthodes synchrones stackoverflow.com/questions/44019316/...


Merci pour la réponse rapide! Sur la base de la référence, j'ai utilisé les méthodes synchrones (readdirSync et statSync) et je l'ai fait fonctionner.


4 Réponses :


10
votes

Le moyen le plus simple et le plus propre serait d'utiliser await / async , de cette façon vous pouvez utiliser les promesses et le code ressemblera presque à du code synchrone.

Vous avez donc besoin d'une version promis de readdir et stat qui peut être créée par le promisify de la bibliothèque principale utils .

const { promisify } = require('util')

const readdir = promisify(require('fs').readdir)
const stat = promisify(require('fs').stat)

async function getPathContent(newPath) {
  // move pathContent otherwise can have conflicts with concurrent requests
  const pathContent = [];

  let files = await readdir(newPath)

  let pathName = newPath;
  // pathContent.length = 0;  // not needed anymore because pathContent is new for each request

  const absPath = path.resolve(pathName);

  // iterate each file

  // replace forEach with (for ... of) because this makes it easier 
  // to work with "async" 
  // otherwise you would need to use files.map and Promise.all
  for (let file of files) {
    // get file info and store in pathContent
    try {
      let stats = await stat(absPath + '/' + file)
      if (stats.isFile()) {
        pathContent.push({
          path: pathName,
          name: file.substring(0, file.lastIndexOf('.')),
          type: file.substring(file.lastIndexOf('.') + 1).concat(' File'),
        })
      } else if (stats.isDirectory()) {
        pathContent.push({
          path: pathName,
          name: file,
          type: 'Directory',
        });
      }
    } catch (err) {
      console.log(`${err}`);
    }
  }

  return pathContent;
}

app.post('/api/files', (req, res, next) => {
  const newPath = req.body.path;
  getPathContent(newPath).then((pathContent) => {
    res.json(pathContent);
  }, (err) => {
    res.status(422).json({
      message: `${err}`
    });
  })
})

Et vous ne devez pas concaténer les chemins en utilisant + ( absPath + '/' + file ), utilisez path.join (absPath, file) ou path.resolve (absPath, file) à la place.

Et vous ne devriez jamais écrire votre code d'une manière qui le code exécuté pour la requête, relais sur des variables globales comme var pathName = ''; et const pathContent = []; . Cela peut fonctionner dans votre environnement de test, mais entraînera certainement des problèmes en production. Où deux requêtes travaillent sur la variable en "même temps"


8 commentaires

Si vous mappez sur les fichiers avec une fonction asynchrone et Promise.all , vous obtenez un peu d'augmentation des performances, car toutes les statistiques peuvent s'exécuter en parallèle, au lieu d'être séquentiellement . ( pathContent = wait Promise.all (files.map (async (file) => {wait stat (…); return {chemin: chemin d'accès…}})) )


@GarrettMotzner Cela dépend du cas d'utilisation si vous avez un tas de requêtes parallèles frappant votre système, alors vous voudrez peut-être rester avec une solution séquentielle par requête.


C'est une préoccupation intéressante, mais je pense que cela serait mieux résolu en limitant les taux ailleurs. Pour moi, cela n'a pas de sens dans la plupart des cas de rendre les appels statistiques séquentiels, car ils n'ont aucune dépendance entre les appels. Il peut y avoir des cas extrêmes où vous vous retrouvez avec trop d'appels système parallèles, mais si tel est le cas, vous avez probablement de plus gros problèmes.


@GarrettMotzner Je ne veux pas contredire ce que vous avez dit au départ. Et oui, la limite de taux doit alors être ajoutée ailleurs. Mais l'avoir séquentiel, puis la charge par rapport au nombre de demandes actuellement actives est plus prévisible, et la limite de débit peut être plus facile à gérer. Mais quelle solution est la meilleure dépend du cas d'utilisation. S'il s'agit d'une action rare, un coup d'oeil avec map et Promise.all pourrait être mieux.


Est-ce que l'un ou l'autre de vous voudrait expliquer pourquoi readdir n'a pas besoin d'être inclus dans le bloc try-catch mais juste stat? Merci!


@sheIsTrue le cas d'erreur de readdir est intercepté par le deuxième rappel si le `getPathContent (newPath) .then (´


@ t.niese Merci pour l'explication. Alors, pourquoi stat ne peut-il pas être traité de la même manière que readdir et doit être à l'intérieur du bloc try-catch? Désolé, je suis très nouveau dans ce domaine.


@sheIsTrue dépend de ce que vous voulez réaliser. Votre code d'origine ignore uniquement les fichiers pour lesquels une erreur s'est produite. Si vous supprimez le bloc try-catch , alors le 422 serait émis si les stats entraînaient une erreur pour au moins un des fichiers, même si stats donnerait un résultat pour les autres fichiers.



0
votes

Voici quelques options:

  • Utilisez les méthodes de fichiers synchrones (consultez la documentation, mais elles se terminent généralement par Sync ). Plus lent, mais un changement de code assez simple et très facile à comprendre.
  • Utilisez des promesses (ou util.promisify ) pour créer une promesse pour chaque statistique, et Promise.all pour attendre que toutes les statistiques soient terminées. Après cela, vous pouvez utiliser les fonctions asynchrones et attendre également pour un code plus facile à lire et une gestion des erreurs plus simple. (Probablement le changement de code le plus important, mais il rendra le code asynchrone plus facile à suivre)
  • Gardez un compteur du nombre de statistiques que vous avez effectuées, et si ce nombre est de la taille que vous attendez, appelez le formulaire res.json dans le rappel de statistiques (plus petit changement de code, mais très erreur enclin)

0 commentaires

0
votes

Il existe différentes manières de procéder:

  1. Vous pouvez d'abord promettre la fonction en utilisant new Promise () puis ensuite, utiliser async / await ou .then ()
  2. Vous pouvez utiliser la fonction ProsifyAll () du package Bluebird ( https: //www.npmjs. com / package / bluebird )
  3. Vous pouvez utiliser la version synchrone des fonctions fs

0 commentaires

1
votes

Sur la base du commentaire initial que j'ai reçu et de la référence, j'ai utilisé readdirSync et statSync à la place et j'ai pu le faire fonctionner. J'examinerai également d'autres réponses et j'apprendrai d'autres façons de mettre en œuvre cela.

Merci à tous pour vos aimables contributions.

Voici ma solution.

const express = require('express');
const bodyParser = require('body-parser');
const fs = require('fs');
const path = require('path');

var pathName = '';
const pathContent = [];

app.post('/api/files', (req, res) => {
    const newPath = req.body.path;

    // validate path
    let files;
    try {
        files = fs.readdirSync(newPath);
    } catch (err) {
        res.status(422).json({ message: `${err}` });
        return;
    }

    // set the pathName and empty pathContent
    pathName = newPath;
    pathContent.length = 0;

    // iterate each file
    let absPath = path.resolve(pathName);
    files.forEach(file => {
        // get file info and store in pathContent
        let fileStat = fs.statSync(absPath + '/' + file);
        if (fileStat.isFile()) {
            pathContent.push({
                path: pathName,
                name: file.substring(0, file.lastIndexOf('.')),
                type: file.substring(file.lastIndexOf('.') + 1).concat(' File'),
            })
        } else if (fileStat.isDirectory()) {
            pathContent.push({
                path: pathName,
                name: file,
                type: 'Directory',
            });
        }
    });
    res.json(pathContent);
});


2 commentaires

Vous devriez vraiment déplacer var pathName et const pathContent dans votre (req, res) => {...} afin qu'ils ne soient pas globaux plus, sinon vous obtiendrez des résultats erronés "aléatoires" inattendus si vous avez deux requêtes ou plus en même temps.


Dans ce cas (c'est pour une affectation scolaire), il n'y aura qu'une seule demande à la fois. Le serveur doit les conserver en tant que variables globales pour d'autres requêtes (c'est-à-dire get) pour obtenir également les mêmes informations. (à moins qu'il n'y ait une meilleure approche) J'apprécie vraiment votre précieuse contribution btw. Je viens de tester votre code et de vérifier qu'il fonctionne. Accepter votre réponse comme réponse.