【问题标题】:Unable to Pipe File Read Stream from Google Cloud Storage to Google Drive API无法将文件读取流从 Google Cloud Storage 传送到 Google Drive API
【发布时间】:2019-11-30 18:39:14
【问题描述】:

我正在做一个项目,我正在从 Google Drive 读取使用 SimpleMind 创建的思维导图文件,修改这些文件,然后将它们上传回 Google Drive。

SimpleMind 创建的 SMMX 文件是包含 XML 文件和媒体文件的 zip 文件。

我的程序在本地运行时运行良好,我对思维导图所做的更改会显示在 SimpleMind 中。

我现在想使用 App Engine 在 Google Cloud Platform 上运行该程序。

由于安全限制,我不能只将我从 Google Drive 下载的文件写入云端应用服务器的文件系统。相反,我创建了一个存储桶来存储文件。

但是,当我这样做时,我的文件被损坏了,在我运行我的程序后,它不是 zip 文件的内容,而是一个 JSON 文件,显然是读取流的字符串表示形式。

在本地运行 - 工作

这是我的代码的简化版本,没有对 zip 文件进行实际修改,我将其省略了,因为它与问题以及任何错误处理无关 - 从来没有任何错误。

当我在本地运行代码时,我使用写入流和读取流在本地文件系统上保存和加载文件:

#!/usr/bin/env node

const { readFileSync, createReadStream, createWriteStream } = require('fs');
const { google } = require('googleapis');

const tokenPath = 'google-drive-token.json';
const clientId = 'xxxxxxxxxxxx-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx.apps.googleusercontent.com';
const redirectUri = 'urn:ietf:wg:oauth:2.0:oob';
const clientSecret = 'xxxxxxxxxxxxxxxxxxxxxxxx';
const fileId = 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx';
const fileName = 'deleteme.smmx';

(async () => {
  const auth = new google.auth.OAuth2(clientId, clientSecret, redirectUri);
  const token = JSON.parse(readFileSync(tokenPath));
  auth.setCredentials(token);
  const writeStream = createWriteStream(fileName);
  const drive = google.drive({ version: 'v3', auth });
  let progress = 0;
  const res = await drive.files.get({ fileId, alt: 'media' }, { responseType: 'stream' });
  await new Promise(resolve => {
    res.data.on('data', d => (progress += d.length)).pipe(writeStream);
    writeStream.on('finish', () => {
      console.log(`Done downloading file ${fileName} from Google Drive to local file system (${progress} bytes)`);
      resolve();
    });
  });
  const readStream = createReadStream(fileName);
  progress = 0;
  const media = {
    mimeType: 'application/x-zip',
    body: readStream
      .on('data', d => {
        progress += d.length;
      })
      .on('end', () => console.log(`${progress} bytes read from local file system`))
  };
  await drive.files.update({
    fileId,
    media
  });
  console.log(`File ${fileName} successfully uploaded to Google Drive`);
})();

当我在本地运行这个脚本时,它工作正常,程序输出总是:

完成将文件 deleteme.smmx 从 Google Drive 下载到本地文件系统(371 字节)

从本地文件系统读取 371 字节

文件 deleteme.smmx 已成功上传到 Google Drive

我可以多次运行它,每次都会在 Google Drive 上创建文件的新版本,每个文件大小为 371 字节。

在 Google Cloud 中运行 - 不工作

这是上面脚本的一个版本,我正在使用它来尝试做同样的事情,在 Google Cloud 中从 Google Drive 下载文件并将文件上传到 Google Drive,并在 App Engine 上运行:

const { readFileSync } = require('fs');
const { google } = require('googleapis');
const { Storage } = require('@google-cloud/storage');

const tokenPath = 'google-drive-token.json';
const clientId = 'xxxxxxxxxxxx-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx.apps.googleusercontent.com';
const redirectUri = 'urn:ietf:wg:oauth:2.0:oob';
const clientSecret = 'xxxxxxxxxxxxxxxxxxxxxxxx';
const fileId = 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx';
const fileName = 'deleteme.smmx';
const storageBucketId = 'xxxxxxxxxxx';

module.exports = async () => {
  const auth = new google.auth.OAuth2(clientId, clientSecret, redirectUri);
  const token = JSON.parse(readFileSync(tokenPath));
  auth.setCredentials(token);
  const storage = new Storage();
  const bucket = storage.bucket(storageBucketId);
  const file = bucket.file(fileName);
  const writeStream = file.createWriteStream({ resumable: false });
  const drive = google.drive({ version: 'v3', auth });
  let progress = 0;
  const res = await drive.files.get({ fileId, alt: 'media' }, { responseType: 'stream' });
  await new Promise(resolve => {
    res.data.on('data', d => (progress += d.length)).pipe(writeStream);
    writeStream.on('finish', () => {
      console.log(`Done downloading file ${fileName} from Google Drive to Cloud bucket (${progress} bytes)`);
      resolve();
    });
  });
  const readStream = file.createReadStream();
  progress = 0;
  const media = {
    mimeType: 'application/x-zip',
    body: readStream
      .on('data', d => {
        progress += d.length;
      })
      .on('end', () => console.log(`${progress} bytes read from storage`))
  };
  await drive.files.update({
    fileId,
    media
  });
  console.log(`File ${fileName} successfully uploaded to Google Drive`);
  return 0;
};

这里唯一的区别是,我没有使用来自 Node.js fs 模块的 createWriteStreamcreateReadStream,而是使用来自 Google Cloud Storage 库的相应方法 file.createWriteStreamfile.createReadStream

当我第一次在云端的 App Engine 上运行此代码时,一切似乎都正常,输出与我在本地运行时相同:

完成将文件 deleteme.smmx 从 Google Drive 下载到云存储桶(371 字节)

从存储中读取 371 字节

文件 deleteme.smmx 已成功上传到 Google Drive

但是,当我在 Google Drive 网络前端查看文件的最新版本时,它不再是我的 smmx 文件,而是一个 JSON 文件,它看起来像是读取流的字符串表示:

{
  "_readableState": {
    "objectMode": false,
    "highWaterMark": 16384,
    "buffer": { "head": null, "tail": null, "length": 0 },
    "length": 0,
    "pipes": null,
    "pipesCount": 0,
    "flowing": true,
    "ended": false,
    "endEmitted": false,
    "reading": false,
    "sync": false,
    "needReadable": true,
    "emittedReadable": false,
    "readableListening": false,
    "resumeScheduled": true,
    "paused": false,
    "emitClose": true,
    "destroyed": false,
    "defaultEncoding": "utf8",
    "awaitDrain": 0,
    "readingMore": false,
    "decoder": null,
    "encoding": null
  },
  "readable": true,
  "_events": {},
  "_eventsCount": 4,
  "_writableState": {
    "objectMode": false,
    "highWaterMark": 16384,
    "finalCalled": false,
    "needDrain": false,
    "ending": false,
    "ended": false,
    "finished": false,
    "destroyed": false,
    "decodeStrings": true,
    "defaultEncoding": "utf8",
    "length": 0,
    "writing": false,
    "corked": 0,
    "sync": true,
    "bufferProcessing": false,
    "writecb": null,
    "writelen": 0,
    "bufferedRequest": null,
    "lastBufferedRequest": null,
    "pendingcb": 0,
    "prefinished": false,
    "errorEmitted": false,
    "emitClose": true,
    "bufferedRequestCount": 0,
    "corkedRequestsFree": { "next": null, "entry": null }
  },
  "writable": true,
  "allowHalfOpen": true,
  "_transformState": {
    "needTransform": false,
    "transforming": false,
    "writecb": null,
    "writechunk": null,
    "writeencoding": null
  },
  "_destroyed": false
}

似乎将读取流从云存储桶传输到写入流以上传到 Google 云端硬盘并不能像我希望的那样工作。

我做错了什么?我需要进行哪些更改才能让我的代码在云端正确运行?

如果你有兴趣,full source code of my project can be found on GitHub

更新:解决方法

我找到了解决这个问题的方法:

  • 将读取流中的数据从云存储桶中读取到缓冲区中
  • 从此缓冲区创建可读流as described in this tutorial
  • 将此“缓冲流”传递给drive.files.update 方法

这样一来,Google Drive 上的 zip 文件就不会损坏,新版本的存储内容与以前相同。

然而,我觉得这很丑陋。使用大型思维导图文件,例如有很多图像的,它会给服务器带来压力,因为文件的全部内容必须存储在内存中。

我希望让从云存储桶到 Google Drive API 的直接管道正常工作。

【问题讨论】:

  • 尝试sys.pump 方法,看看是否有帮助? stackoverflow.com/questions/4589732/…。也可以尝试只使用body: readStream,在流上不使用任何on 事件处理程序
  • @TarunLalwani 感谢您的 cmets。 sys.pump 或后来被称为 util.pump 自 Node.js 版本 4.0.0 以来已被弃用,并已从后续版本中删除。我正在使用 Node.js 版本 10 (LTS)。所以我认为你链接的答案已经过时了。不幸的是,删除 on 事件处理程序也无济于事。
  • 那么 json 文件也出现在第一个 App 引擎运行中?还是从第二次开始?
  • 脚本第一次被应用引擎运行后,Google Drive上的文件被JSON代码替换
  • @PatrickHund 如果在创建写入流时指定contentType 会怎样?

标签: node.js google-app-engine google-cloud-platform google-drive-api


【解决方案1】:

显然,您可以使用直通流

const file = storage.bucket(bucketName).file(object.name)
const fileStream = file.createReadStream();

const dataStream = new stream.PassThrough();
fileStream.pipe(dataStream);

await uploadFileToGDrive(dataStream, {
   name: object.name,
   mimeType: object.contentType,
   parents: ['shared_dir_in_g_drive'],
})

源代码:https://github.com/googleapis/google-api-nodejs-client/issues/2015

【讨论】:

    猜你喜欢
    • 2019-06-04
    • 2018-06-15
    • 2020-06-13
    • 1970-01-01
    • 2019-11-17
    • 2014-09-18
    • 2020-02-23
    • 1970-01-01
    • 2021-04-25
    相关资源
    最近更新 更多