Обещание разрешено до загрузки Google Cloud Bucket - PullRequest
4 голосов
/ 27 февраля 2020

Я пишу некоторый код, который зацикливается на CSV и создает JSON файл на основе CSV . В JSON включен array с именем photos, который должен содержать возвращенные URL-адреса для изображений, загружаемых в Облачное хранилище Google в функции. Тем не менее, ожидание загрузки для fini sh поставило меня в тупик, поскольку все работает асинхронно и завершает компиляцию promise и JSON до завершения загрузки корзины и возврата URL-адреса. Как я могу сделать разрешение promise после того, как URL были получены и добавлены в currentJSON.photos?

const csv=require('csvtojson')
const fs = require('fs');
const {Storage} = require('@google-cloud/storage');
var serviceAccount = require("./my-firebase-storage-spot.json");
const testFolder = './Images/';
var csvFilePath = './Inventory.csv';

var dirArr = ['./Images/Subdirectory-A','./Images/Subdirectory-B','./Images/Subdirectory-C'];
var allData = [];

csv()
.fromFile(csvFilePath)
.subscribe((json)=>{
  return new Promise((resolve,reject)=>{
    for (var i in dirArr ) {
      if (json['Name'] == dirArr[i]) {

        var currentJSON = {
          "photos" : [],
        };         

        fs.readdir(testFolder+json['Name'], (err, files) => {
          files.forEach(file => {
            if (file.match(/.(jpg|jpeg|png|gif)$/i)){
              var imgName = testFolder + json['Name'] + '/' + file;
              bucket.upload(imgName, function (err, file) {
                if (err) throw new Error(err);
                //returned uploaded img address is found at file.metadata.mediaLink
                currentJSON.photos.push(file.metadata.mediaLink);
              });              
            }else {
              //do nothing
            }
          });
        });
        allData.push(currentJSON);
      }
    }

    resolve(); 
  })
},onError,onComplete);

function onError() {
  // console.log(err)
}
function onComplete() {
  console.log('finito');
}

Я попытался переместить resolve() вокруг, а также попытался поместить раздел загрузчика в функция onComplete() (которая создала новые проблемы, основанные на обещаниях).

Ответы [ 4 ]

4 голосов
/ 01 марта 2020

Проблема в том, что ваш код не ждет в вашем forEach. Я очень рекомендую искать поток и стараться делать что-то параллельно, насколько это возможно. Есть одна библиотека, которая очень мощная и выполняет эту работу за вас. Библиотека etl .

Вы можете читать строки из csv параллельно и обрабатывать их параллельно, а не по одной.

Я пытался объяснить строки в код ниже. Надеюсь, это имеет смысл.

const etl = require("etl");
const fs = require("fs");
const csvFilePath = `${__dirname }/Inventory.csv`;
const testFolder = "./Images/";

const dirArr = [
  "./Images/Subdirectory-A",
  "./Images/Subdirectory-B",
  "./Images/Subdirectory-C"
];

fs.createReadStream(csvFilePath)
  .pipe(etl.csv()) // parse the csv file
  .pipe(etl.collect(10)) // this could be any value depending on how many you want to do in parallel.
  .pipe(etl.map(async items => {
    return Promise.all(items.map(async item => { // Iterate through 10 items
      const finalResult = await Promise.all(dirArr.filter(i => i === item.Name).map(async () => { // filter the matching one and iterate
        const files = await fs.promises.readdir(testFolder + item.Name); // read all files
        const filteredFiles = files.filter(file => file.match(/\.(jpg|jpeg|png|gif)$/i)); // filter out only images
        const result = await Promise.all(filteredFiles).map(async file => {
          const imgName = `${testFolder}${item.Name}/${file}`;
          const bucketUploadResult = await bucket.upload(imgName); // upload image
          return bucketUploadResult.metadata.mediaLink;
        });
        return result; // This contains all the media link for matching files
      }));
      // eslint-disable-next-line no-console
      console.log(finalResult); // Return arrays of media links for files
      return finalResult;
    }));
  }))
  .promise()
  .then(() => console.log("finsihed"))
  .catch(err => console.error(err));

3 голосов
/ 29 февраля 2020

Действительно, ваш код не ожидает асинхронного вызова функции обратного вызова readdir и функции обратного вызова bucket.upload.

Асинхронное кодирование упрощается при использовании версии обещания этих функций .

bucket.upload вернет обещание, если опустить функцию обратного вызова, так что это просто.

Чтобы readdir возвратить обещание, вам нужно использовать обещание fs API : тогда вы можете использовать метод readdir, основанный на обещаниях, и использовать обещания во всем коде.

Поэтому используйте fs = require('fs').promises вместо fs = require('fs')

С помощью этой подготовки ваш код может быть преобразован в следующее:

const testFolder = './Images/';
var csvFilePath = './Inventory.csv';
var dirArr = ['./Images/Subdirectory-A','./Images/Subdirectory-B','./Images/Subdirectory-C'];

(async function () {
    let arr = await csv().fromFile(csvFilePath);
    arr = arr.filter(obj => dirArr.includes(obj.Name));
    let allData = await Promise.all(arr.map(async obj => {
        let files = await fs.readdir(testFolder + obj.Name);
        files = files.filter(file => file.match(/\.(jpg|jpeg|png|gif)$/i));
        let photos = await Promise.all(
            files.map(async file => {
                var imgName = testFolder + obj.Name + '/' + file;
                let result = await bucket.upload(imgName);
                return result.metadata.mediaLink;
            })
        );
        return {photos};
    }));
    console.log('finito', allData);
})().catch(err => {  // <-- The above async function runs immediately and returns a promise
    console.log(err);
});

Некоторые замечания:

  • В вашем регулярном выражении есть недостаток. Вы намеревались соответствовать буквальной точке, но не избежали ее (исправлено в приведенном выше коде).

  • allData будет содержать массив { photos: [......] } объектов, и мне интересно, почему Вы не хотели бы, чтобы все элементы фото были частью одного массива. Тем не менее, я сохранил ваши логики c, так что выше все равно будет производить их в этих кусках. Возможно, вы намеревались иметь и другие свойства (рядом с photos), что могло бы сделать полезными эти отдельные объекты.

0 голосов
/ 07 марта 2020

Вы ищете эту библиотеку ELT.

Вы можете читать строки из CSV параллельно и обрабатывать их параллельно, а не по одной.

Я попытался объяснить строки в код ниже. Надеюсь, это имеет смысл.

const etl = require("etl");
const fs = require("fs");
const csvFilePath = `${__dirname }/Inventory.csv`;
const testFolder = "./Images/";

const dirArr = [
  "./Images/Subdirectory-A",
  "./Images/Subdirectory-B",
  "./Images/Subdirectory-C"
];

fs.createReadStream(csvFilePath)
  .pipe(etl.csv()) // parse the csv file
  .pipe(etl.collect(10)) // this could be any value depending on how many you want to do in parallel.
  .pipe(etl.map(async items => {
    return Promise.all(items.map(async item => { // Iterate through 10 items
      const finalResult = await Promise.all(dirArr.filter(i => i === item.Name).map(async () => { // filter the matching one and iterate
        const files = await fs.promises.readdir(testFolder + item.Name); // read all files
        const filteredFiles = files.filter(file => file.match(/\.(jpg|jpeg|png|gif)$/i)); // filter out only images
        const result = await Promise.all(filteredFiles).map(async file => {
          const imgName = `${testFolder}${item.Name}/${file}`;
          const bucketUploadResult = await bucket.upload(imgName); // upload image
          return bucketUploadResult.metadata.mediaLink;
        });
        return result; // This contains all the media link for matching files
      }));
      // eslint-disable-next-line no-console
      console.log(finalResult); // Return arrays of media links for files
      return finalResult;
    }));
  }))
  .promise()
  .then(() => console.log("finsihed"))
  .catch(err => console.error(err));
0 голосов
/ 06 марта 2020

Вот способ сделать это, где мы извлекаем некоторые функциональные возможности в несколько отдельных вспомогательных методов и урезаем часть кода. Я должен был вывести некоторые из ваших требований, но это, кажется, довольно близко соответствует тому, как я понял цель вашего исходного кода:

const csv=require('csvtojson')
const fs = require('fs');
const {Storage} = require('@google-cloud/storage');
var serviceAccount = require("./my-firebase-storage-spot.json");
const testFolder = './Images/';
var csvFilePath = './Inventory.csv';

var dirArr = ['./Images/Subdirectory-A','./Images/Subdirectory-B','./Images/Subdirectory-C'];
var allData = [];

// Using nodejs 'path' module ensures more reliable construction of file paths than string manipulation:
const path = require('path');

// Helper function to convert bucket.upload into a Promise
// From other responses, it looks like if you just omit the callback then it will be a Promise
const bucketUpload_p = fileName => new Promise((resolve, reject) => {
  bucket.upload(fileName, function (err, file) {
    if (err) reject(err);

    resolve(file);
  });
});

// Helper function to convert readdir into a Promise
// Again, there are other APIs out there to do this, but this is a rl simple solution too:
const readdir_p = dirName => new Promise((resolve, reject) => {
  fs.readdir(dirName, function (err, files) {
    if (err) reject(err);

    resolve(files);
  });
});

// Here we're expecting the string that we found in the "Name" property of our JSON from "subscribe".
// It should match one of the strings in `dirArr`, but this function's job ISN'T to check for that,
// we just trust that the code already found the right one.
const getImageFilesFromJson_p = jsonName => new Promise((resolve, reject) => {
  const filePath = path.join(testFolder, jsonName);

  try {
    const files = await readdir_p(filePath);

    resolve(files.filter(fileName => fileName.match(/\.(jpg|jpeg|png|gif)$/i)));
  } catch (err) {
    reject(err);
  }
});

csv()
.fromFile(csvFilePath)
.subscribe(async json => {
  // Here we appear to be validating that the "Name" prop from the received JSON matches one of the paths that
  // we're expecting...?  If that's the case, this is a slightly more semantic way to do it.
  const nameFromJson = dirArr.find(dirName => json['Name'] === dirName);

  // If we don't find that it matches one of our expecteds, we'll reject the promise.
  if (!nameFromJson) {
    // We can do whatever we want though in this case, I think it's maybe not necessarily an error:
    // return Promise.resolve([]);
    return Promise.reject('Did not receive a matching value in the Name property from \'.subscribe\'');
  }

  // We can use `await` here since `getImageFilesFromJson_p` returns a Promise
  const imageFiles = await getImageFilesFromJson_p(nameFromJson);
  // We're getting just the filenames; map them to build the full path
  const fullPathArray = imageFiles.map(fileName => path.join(testFolder, nameFromJson, fileName));

  // Here we Promise.all, using `.map` to convert the array of strings into an array of Promises;
  // if they all resolve, we'll get the array of file objects returned from each invocation of `bucket.upload`
  return Promise.all(fullPathArray.map(filePath => bucketUpload_p(filePath)))
    .then(fileResults => {
      // So, now we've finished our two asynchronous functions; now that that's done let's do all our data
      // manipulation and resolve this promise

      // Here we just extract the metadata property we want
      const fileResultsMediaLinks = fileResults.map(file => file.metadata.mediaLink);

      // Before we return anything, we'll add it to the global array in the format from the original code
      allData.push({ photos: fileResultsMediaLinks });

      // Returning this array, which is the `mediaLink` value from the metadata of each of the uploaded files.
      return fileResultsMediaLinks;
    })
}, onError, onComplete);
...