异步Google Cloud Functions中drive.files.create未执行问题求助
问题诊断与修复建议
你的核心问题是Google Cloud Functions(GCF)在uploadFile函数还没完成异步上传操作时就提前终止了,导致drive.files.create的回调根本没机会执行。以下是具体原因和修复方案:
核心原因
- 未等待异步操作完成:
uploadFile是async函数,但内部调用drive.files.create时用了回调模式,没有将其包装成Promise并等待。GCF检测到async函数执行到末尾(无pending的Promise)就会立即终止进程,此时上传逻辑还没启动。 request库的兼容性问题:request已被废弃,且在GCF的Serverless环境中,流式处理若未正确绑定生命周期,容易被提前中断。- 冗余API调用:
getRecordingData被getRecordingDate和zoomDownloadLink各调用一次,重复请求Zoom API,既浪费资源又可能触发速率限制。 - 全局变量
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
相关产品推荐
相关产品推荐

