You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Heroku部署的NodeJS应用从Google Cloud Storage批量下载图片时内容重复问题求助

问题分析与解决方案

这个问题的核心在于全局变量的异步竞态和异步操作未正确等待,导致批量下载时所有文件都被最后一个请求的内容覆盖。我们一步步拆解问题并修复:

1. 全局变量引发的竞态问题

你定义了全局变量downloadFileName,当批量调用downloadFromGCS时,循环会快速覆盖这个变量,而.download()是异步操作——等异步任务真正执行时,downloadFileName已经被更新为循环的最后一个文件名,所以所有下载最终都指向了最后一个文件的内容。

2. 低效的文件存在性检查

你通过getFiles()遍历存储桶所有文件来判断目标文件是否存在,这在文件多的时候会非常慢。GCS提供了直接检查单个文件是否存在的方法,完全不需要遍历全量文件。

3. 异步操作未正确等待

user.recipe.forEach()里调用downloadImage是异步的,但你没有等待所有下载完成就直接返回了响应。不仅会导致文件还没下载完就返回,也无法保证异步操作的顺序和正确性。


修复后的代码

下面是修改后的核心代码,解决了上述所有问题:

const fs = require('fs');
const path = require('path');
const { Storage } = require('@google-cloud/storage');
const mongoose = require('mongoose');
const User = mongoose.model('Users');
const filePath = 'uploads';

// 确保uploads目录存在,避免下载时出错
if (!fs.existsSync(filePath)) {
  fs.mkdirSync(filePath);
}

const recipesReadList = async (req, res) => {
  getUser(req, res, async (req, res) => {
    try {
      const user = await User.findById(req.params.userid).select('recipe').exec();
      if (user.recipe && user.recipe.length > 0) {
        // 用for...of循环等待所有下载完成,避免竞态
        for (const recipe of user.recipe) {
          const localPath = path.join(filePath, recipe.imageName);
          if (!fs.existsSync(localPath)) {
            await downloadImage(recipe.imageName);
          }
        }
        return res.status(200).json(user.recipe);
      } else {
        return res.status(200).json([]);
      }
    } catch (err) {
      return res.status(500).json({ error: err.message });
    }
  });
};

const getUser = (req, res, callback) => {
  if (req.payload && req.payload.email) {
    User.findOne({ email: req.payload.email }).exec((err, user) => {
      if (err || !user) {
        return res.status(401).json({ error: 'User not found' });
      }
      callback(req, res, user.userName);
    });
  } else {
    return res.status(401).json({ error: 'Invalid credentials' });
  }
};

const downloadImage = async (file) => {
  await downloadFromGCS(file); // 这里要await确保下载完成
};

const GCS_Storage = new Storage({
  projectId: process.env.GCLOUD_PROJECT,
  keyFilename: path.join(__dirname, '../../', `${process.env.GCS_KEYFILE}`),
});

async function downloadFromGCS(fileName) {
  const bucket = GCS_Storage.bucket(process.env.GCS_BUCKET);
  const remoteFile = bucket.file(fileName);
  
  // 直接检查文件是否存在,无需遍历全桶
  const [exists] = await remoteFile.exists();
  if (!exists) {
    console.log(`File not found on cloud storage: ${fileName}`);
    return;
  }

  const localPath = path.join(filePath, fileName);
  // 去掉全局变量,直接使用局部变量,避免竞态
  await remoteFile.download({ destination: localPath });
  console.log(`Successfully downloaded ${fileName} to ${localPath}`);
}

module.exports = {
  recipesReadList,
};

关键修改点说明

  1. 移除全局变量:用局部变量localPath和remoteFile代替全局的downloadFileName,彻底避免异步竞态。
  2. 高效检查文件存在:使用remoteFile.exists()直接验证GCS上的文件,性能大幅提升。
  3. 正确等待异步操作:
    • 将recipesReadList改为async函数,用for...of循环遍历食谱列表,确保每一张图片下载完成后再处理下一张。
    • downloadImage里await downloadFromGCS(file),确保下载完成后再继续。
  4. 增加错误处理:添加try/catch捕获异步操作中的错误,避免服务器崩溃。

验证下载完成的方法

  • 用await包裹所有异步下载操作,确保代码执行到返回响应时,所有文件都已经下载完成。
  • 可以在downloadFromGCS里添加日志(如示例中的console.log),观察每一个文件的下载完成状态。
  • 也可以在返回响应前,检查本地文件的大小和修改时间,确认文件已正确写入。

内容的提问来源于stack exchange,提问作者JonW

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.06 06:48:53