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

异步Google Cloud Functions中drive.files.create未执行问题求助

问题诊断与修复建议

你的核心问题是Google Cloud Functions(GCF)在uploadFile函数还没完成异步上传操作时就提前终止了,导致drive.files.create的回调根本没机会执行。以下是具体原因和修复方案:

核心原因

  1. 未等待异步操作完成:uploadFile是async函数,但内部调用drive.files.create时用了回调模式,没有将其包装成Promise并等待。GCF检测到async函数执行到末尾(无pending的Promise)就会立即终止进程,此时上传逻辑还没启动。
  2. request库的兼容性问题:request已被废弃,且在GCF的Serverless环境中,流式处理若未正确绑定生命周期,容易被提前中断。
  3. 冗余API调用:getRecordingData被getRecordingDate和zoomDownloadLink各调用一次,重复请求Zoom API,既浪费资源又可能触发速率限制。
  4. 全局变量today的冷启动问题:GCF冷启动时全局变量仅初始化一次,后续调用可能复用旧日期值。

具体修复步骤

1. 将drive.files.create改为Promise形式并等待完成

googleapis的方法默认支持Promise,去掉回调,用await等待上传完成:

// 替换原drive.files.create部分
try {
  const res = await drive.files.create({
    resource: fileMetadata,
    media: media,
    fields: "id",
    uploadType: "resumable",
    supportsAllDrives: true,
  });
  console.log("File Id: ", res.data.id);
} catch (err) {
  console.error("上传失败:", err);
  throw err; // 抛出错误让GCF捕获,标记函数执行失败
}

2. 替换request为axios流式处理

用axios获取Zoom文件的流,替代已废弃的request库:

// 在uploadFile函数中替换request部分
const response = await axios.get(buffer, { responseType: 'stream' });
const bs = new stream.PassThrough();
response.data.pipe(bs);

3. 优化Zoom API调用,避免重复请求

修改townHall主函数,只调用一次getRecordingData,将结果传给后续函数:

exports.townHall = async () => {
  const townHallFile = await searchFile(auth);
  // 只调用一次getRecordingData
  const recordingData = await getRecordingData();
  const lastRecordingDate = getRecordingDate(recordingData); // 修改为同步函数,直接传入数据
  const downloadLink = zoomDownloadLink(recordingData); // 同理修改

  if (townHallFile !== lastRecordingDate) {
    await uploadFile(auth, downloadLink, lastRecordingDate); // 传入下载链接和文件名
  } else {
    console.log("今日无新录制文件");
  }
};

// 对应的修改getRecordingDate和zoomDownloadLink为同步函数
const getRecordingDate = (recording) => {
  const lastRecordingDate = recording.data.start_time;
  return `${lastRecordingDate.substring(0,4)}.${lastRecordingDate.substring(5, 7)}.${lastRecordingDate.substring(8,10)} - Town Hall.mp4`;
};

const zoomDownloadLink = (recording) => {
  return `${recording.data.recording_files[0].download_url}?access_token=${zoomAccessToken}`;
};

4. 修正today的作用域

将today移到uploadFile内部,确保每次执行都获取当前日期(或直接用Zoom录制的日期作为文件名,避免不一致):

const uploadFile = async (auth, downloadLink, fileName) => {
  // 直接使用传入的Zoom录制日期作为文件名
  const fileMetadata = {
    name: fileName,
    parents: ["XXXXXXXXXXXXXXXXX"],
  };
  // ... 后续代码
};

5. 添加完整的错误捕获

在主函数和uploadFile中添加try/catch,确保错误能被日志记录:

exports.townHall = async () => {
  try {
    // ... 原逻辑
  } catch (err) {
    console.error("函数执行失败:", err);
    throw err;
  }
};

完整修复后的代码示例

const axios = require("axios");
require("dotenv").config();
const stream = require("stream");
const { google } = require("googleapis");

const KEYFILEPATH = "./credentials.json";
const SCOPES = ["https://www.googleapis.com/auth/drive"];

const auth = new google.auth.GoogleAuth({
  keyFile: KEYFILEPATH,
  scopes: SCOPES,
});

let zoomAccessToken;

///////////////////////////////////////////////////////////////// 搜索Google Drive中最新的Town Hall录制文件
const searchFile = async (auth) => {
  const service = google.drive({ version: "v3", auth });
  try {
    const res = await service.files.list({
      corpora: "drive",
      includeItemsFromAllDrives: true,
      supportsAllDrives: true,
      driveId: "XXXXXXXXXXXXXXXX",
      q: '"XXXXXXXXXXXXXXX" in parents',
      fields: "nextPageToken, files(id, name)",
      spaces: "drive",
    });
    const filesArray = res.data.files;
    if (!filesArray || filesArray.length === 0) return null;
    const filesName = filesArray.map((x) => x.name).sort().reverse()[0];
    console.log(filesName);
    return filesName;
  } catch (err) {
    console.error("搜索Drive文件失败:", err);
    throw err;
  }
};

///////////////////////////////////////////////////////////////// 获取Zoom OAuth访问令牌
const getAccessToken = async () => {
  try {
    const response = await axios({
      method: "post",
      url: `https://zoom.us/oauth/token?grant_type=account_credentials&account_id=${process.env.ZOOM_ACCOUNT_ID}`,
      headers: {
        Authorization: "Basic " + new Buffer.from(process.env.ZOOM_CLIENT_ID + ":" + process.env.ZOOM_CLIENT_SECRET).toString("base64"),
      },
    });
    zoomAccessToken = response.data.access_token;
    return response;
  } catch (err) {
    console.error("获取Zoom Token失败:", err);
    throw err;
  }
};

///////////////////////////////////////////////////////////////// 获取最新Town Hall录制的原始数据
const getRecordingData = async () => {
  await getAccessToken();
  try {
    return axios({
      method: "get",
      url: "https://api.zoom.us/v2/meetings/XXXXXXXXX/recordings",
      headers: {
        "Content-Type": "application/json",
        Authorization: `Bearer ${zoomAccessToken}`,
      },
    });
  } catch (err) {
    console.error("获取Zoom录制数据失败:", err);
    throw err;
  }
};

///////////////////////////////////////////////////////////////// 格式化最新录制文件的日期作为文件名
const getRecordingDate = (recording) => {
  const lastRecordingDate = recording.data.start_time;
  return `${lastRecordingDate.substring(0,4)}.${lastRecordingDate.substring(5, 7)}.${lastRecordingDate.substring(8,10)} - Town Hall.mp4`;
};

///////////////////////////////////////////////////////////////// 获取最新录制文件的下载链接
const zoomDownloadLink = (recording) => {
  return `${recording.data.recording_files[0].download_url}?access_token=${zoomAccessToken}`;
};

///////////////////////////////////////////////////////////////// 上传Zoom录制文件到Google Drive
const uploadFile = async (auth, downloadLink, fileName) => {
  try {
    // 获取Zoom文件流
    const zoomResponse = await axios.get(downloadLink, { responseType: 'stream' });
    const bs = new stream.PassThrough();
    zoomResponse.data.pipe(bs);

    const drive = google.drive({ version: "v3", auth });
    const fileMetadata = {
      name: fileName,
      parents: ["XXXXXXXXXXXXXXXXX"],
    };
    const media = {
      mimeType: "video/mp4",
      body: bs,
    };

    // 等待上传完成
    const res = await drive.files.create({
      resource: fileMetadata,
      media: media,
      fields: "id",
      uploadType: "resumable",
      supportsAllDrives: true,
    });
    console.log("File Id: ", res.data.id);
    return res.data.id;
  } catch (err) {
    console.error("上传文件失败:", err);
    throw err;
  }
};

///////////////////////////////////////////////////////////////// 对比Drive和Zoom的文件,有更新则上传
exports.townHall = async () => {
  try {
    const townHallFile = await searchFile(auth);
    const recordingData = await getRecordingData();
    const lastRecordingDate = getRecordingDate(recordingData);
    const downloadLink = zoomDownloadLink(recordingData);

    if (townHallFile !== lastRecordingDate) {
      await uploadFile(auth, downloadLink, lastRecordingDate);
      console.log("新录制文件已上传");
    } else {
      console.log("今日无新录制文件");
    }
  } catch (err) {
    console.error("主函数执行失败:", err);
    throw err;
  }
};

额外注意事项

  • GCF部署时,确保credentials.json被正确打包到部署包中,或改用GCF的服务账号密钥(推荐,避免本地文件依赖)。
  • 调整GCF的资源配置:视频上传需要足够内存和超时时间,建议设置内存512MB以上,超时时间延长至5-10分钟。
  • Zoom下载链接有有效期,需在获取后立即使用。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.22 15:24:16