如何使用 Apps 脚本将 Drive 中的现有文件上传到 Drive

Posted

技术标签:

【中文标题】如何使用 Apps 脚本将 Drive 中的现有文件上传到 Drive【英文标题】:How to upload an existing file in Drive to Drive using Apps Script 【发布时间】:2020-08-28 14:09:52 【问题描述】:

简介

我先介绍一下我要做的事情的目标。

我之前有一个文件被分成两部分

这两个文件的总大小可能超过 50 MB(作为长期目标)。由于UrlFetchApp.fetch() 对请求的大小有限制,我想单独上传它们,每个文件将小于 50 MB,然后合并它们。目前(尝试 Drive API),我使用的是小文件。

第一个文件是 640000 bytes256 的倍数) 524288 bytes。我意识到我之前犯了一个错误,即我使用文件的大小作为 256 的倍数,但它应该是 256*1024

的倍数

第二个文件是 47626 bytes 163339 bytes.

我已使用 curl 拆分文件并将它们上传到我的驱动器(正常的网络上传)。

我的意图是使用Resumable Uploadpartial files 一张一张地上传到Google Drive,使用Google Apps Script 中的Google Drive API 将它们合并到一个文件中。

到目前为止我尝试了什么?

昨天,我在这里问了question。我试图使用Drive.Files.insert 执行resumable upload,但用户指出使用下面引用的Drive.Files.insert 是不可能的。

很遗憾,在当前阶段,使用 Drive.Files.insert 无法实现可恢复上传。这似乎是谷歌方面目前的规范

我现在正在尝试使用Google Drive API。下面附上代码。
function myFunction() 
    var token = ScriptApp.getOAuthToken();

    var f1_id = '1HkBDHV1oXXXXXXXXXXXXXXXXXXXXXXXX';
    var f2_id = '1twuaKTCFTXXXXXXXXXXXXXXXXXXXX';
    
    var putUrl = 'https://www.googleapis.com/drive/v3/files?uploadType=resumable';
  
    var fileData = 
        name : 'Merged-file-from-GAS',
        file : DriveApp.getFileById(f1_id).getBlob()
    
    
    var options = 
      method : 'put',
      contentType:"application/json",
      headers : 
        Authorization: 'Bearer ' + token,
        'X-Upload-Content-Type' : 'application/octet-stream',
        'Content-Type' : 'application/json; charset=UTF-8'
      ,
      muteHttpExceptions: true,
      payload : fileData
    ;
  
    var response = UrlFetchApp.fetch(putUrl, options);
    Logger.log(response.getResponseCode());
    Logger.log(response.getAllHeaders()); 


我也试过把方法改成patch

我在headers 中添加了Content-Length : 640000,在这种情况下,我收到如下所示的错误。

例外:提供的属性值无效:Header:Content-Length

我尝试使用空白resource 创建一个使用Drive.Files.insert(resource) 的文件。然后我尝试使用UrlFetchApp(patchUrl,options) 更新它,同时拥有变量 var patchUrl = 'https://www.googleapis.com/upload/drive/v3/files/' + fileId + '?uploadType=resumable';

结果

它不会创建任何文件。 以下提供了附加代码(初始代码)结果的记录器日志:

[20-05-12 21:05:37:726 IST] 404.0

[20-05-12 21:05:37:736 IST] X-Frame-Options=SAMEORIGIN, Content-Security-Policy=frame-ancestors 'self', Transfer-Encoding=chunked, alt-svc= h3-27=":443"; ma=2592000,h3-25=":443"; ma=2592000,h3-Q050=":443"; ma=2592000,h3-Q049=":443"; ma=2592000,h3-Q048=":443"; ma=2592000,h3-Q046=":443"; ma=2592000,h3-Q043=":443"; ma=2592000,quic=":443";马=2592000; v="46,43", X-Content-Type-Options=nosniff, Date=Tue, 2020 年 5 月 12 日 15:35:37 GMT, Expires=Mon, 1990 年 1 月 1 日 00:00:00 GMT, X-XSS-保护=1; mode=block, Content-Encoding=gzip, Pragma=no-cache, Cache-Control=no-cache, no-store, max-age=0, must-revalidate, Vary=[Origin, X-Origin], Server= GSE,内容类型=文本/html;字符集=UTF-8

问题

在将upload type 保持为resumable 的同时,使用Apps 脚本中的Drive API 将文件initiating a upload 的正确方法是什么?

后续请求应该是什么样的?这样 50 MB 以上的文件可以随后上传到合并文件中吗?

编辑 1

使用更正的文件块大小再次尝试。同样的问题仍然存在。

编辑 2

为了理解答案中的代码,我单独使用了Tanaike的代码// 2中的代码来了解如何检索Location

function understanding() 
  var token = ScriptApp.getOAuthToken();
  const filename = 'understanding.pdf';
  const mimeType = MimeType.PDF;

  const url = 'https://www.googleapis.com/drive/v3/files?uploadType=resumable';
  
  const res1 = UrlFetchApp.fetch(url, 
    method: "post",
    contentType: "application/json",
    payload: JSON.stringify(name: filename, mimeType: mimeType),
    headers: authorization: "Bearer " + ScriptApp.getOAuthToken()
  );
  const location = res1.getHeaders().Location;
  Logger.log(location);

这将创建一个大小为0 bytes 的文件understanding.pdf。但是,Logger.log(location) 会记录 null

为什么会这样?

错误出现在终点。将其设置为 https://www.googleapis.com/upload/drive/v3/files?uploadType=resumable 有效。它检索位置。

【问题讨论】:

在您的情况下,您可以调整每个文件的初始大小吗?因为关于First file is of 640000 bytes (a multiple of 256).,这种情况下文件不能直接用于续传。因为在可恢复上传时,每个块都需要是 256 KB(256 x 1024 字节)的倍数。这个怎么样? Ref如果每个文件的大小(最后一个文件除外)都是262144字节的倍数,并且所有文件小于52428800字节,那么恢复上传的脚本可以更简单。 @Tanaike 我使用256x1024x2 = 524288 bytes 的文件再次尝试了它。还是没有上传。使用新文件大小编辑问题 感谢您的回复。我想确认一下您目前的情况。 1. 在您的示例文件中,“文件 A”和“文件 B”分别为 524,288 bytes163,339 bytes。 2.您测试的脚本是您问题中显示的脚本。 3. 您想使用可恢复上传来合并文件。我的理解正确吗?顺便问一下,合并文件的mimeType是什么? @Tanaike 1. 是的 2. 是的 3. 是的。 By the way, what is the mimeType of the merged file?PDF 【参考方案1】:

从您的问题和回复中,我可以理解您的情况和目标,如下所示。

在您的示例文件中,“文件 A”和“文件 B”分别为 524,288 字节和 163,339 字节。 您测试的脚本就是您的问题中显示的脚本。 您想使用可恢复上传合并文件。 合并文件的 mimeType 为 PDF。

对于这个,这个答案怎么样?

修改点:

很遗憾,您的脚本不完整,无法实现可恢复上传。 Google Drive API 的可恢复上传流程如下。 Ref

    请求检索用作上传数据端点的位置。 在您的情况下,新文件已创建。所以需要使用POST方式。 通过包含数据请求检索到的位置(在您的情况下,它是每个文件。)。 在这种情况下,需要使用循环上传数据。并且使用了PUT方法。 在这里,每个文件大小都是最重要的。如果最后一个文件以外的文件大小不是262,144字节的倍数,则无法进行续传。请注意这一点。

对于上述流程,当示例脚本准备好后,如下所示。

用法:

1。启用 Drive API。

在这种情况下,使用 Drive API。因此,请在 Advanced Google Services 中启用 Drive API。这样,Drive API 会在 API 控制台自动启用。

示例脚本的流程如下。

    创建一个在可恢复上传时使用的对象。 检索“位置”以开始可恢复上传。 上传每个文件并合并它们。

2。示例脚本。

请复制并粘贴以下脚本。请设置文件ID。在这种情况下,请设置它们以便合并。请注意这一点。

function myFunction() 
  const fileIds = ["###", "###"];  // Please set the file IDs of the file "A" and "B" in order.
  const filename = "sample.pdf";
  const mimeType = MimeType.PDF;

  // 1. Create an object for using at the resumable upload.
  const unitSize = 262144;
  const fileObj = fileIds.reduce((o, id, i, a) => 
    const file = DriveApp.getFileById(id);
    const size = file.getSize();
    if (i != a.length - 1 && (size % unitSize != 0 || size > 52428800)) 
      throw new Error("Size of each file is required to be the multiples of 262,144 bytes and less than 52,428,800 bytes.");
    
    o.files.push(data: file.getBlob().getBytes(), range: `bytes $o.size-$o.size + size - 1\/`, size: size.toString());
    o.size += size;
    return o;
  , size: 0, files: []);

  // 2. Retrieve "location" for starting the resumable upload.
  const url = "https://www.googleapis.com/upload/drive/v3/files?uploadType=resumable";
  const res1 = UrlFetchApp.fetch(url, 
    method: "post",
    contentType: "application/json",
    payload: JSON.stringify(name: filename, mimeType: mimeType),
    headers: authorization: "Bearer " + ScriptApp.getOAuthToken()
  );
  const location = res1.getHeaders().Location;

  // 3. Upload each file and merge them.
  fileObj.files.forEach((e, i) => 
    const params = 
      method: "put",
      headers: "Content-Range": e.range + fileObj.size,
      payload: e.data,
      muteHttpExceptions: true,
    ;
    const res = UrlFetchApp.fetch(location, params);
    const status = res.getResponseCode();
    if (status != 308 && status != 200) 
      throw new Error(res.getContentText());
    
    if (status == 200) 
      console.log(res.getContentText())
    
  );

  // DriveApp.createFile()  // This comment line is used for automatically detecting the scope of "https://www.googleapis.com/auth/drive" by the script editor. So please don't remove this line.

结果:

可恢复上传完成后,可以在日志中看到以下结果。并且可以在根文件夹看到合并后的文件。


 "kind": "drive#file",
 "id": "###",
 "name": "sample.pdf",
 "mimeType": "application/pdf"

注意:

这是一个简单的示例脚本。所以请根据您的实际情况进行修改。 我针对您的示例情况测试了上述脚本,即“文件 A”和“文件 B”分别为 524,288 字节和 163,339 字节。因此,当使用此脚本合并几个大小约为 50 MB 的文件时,会出现错误。 如果使用大文件时出现内存错误,目前阶段,似乎这是Google方面的规范。所以请注意这一点。

参考:

Perform a resumable upload

【讨论】:

@Tyler Rake fileObj// 1. 创建,用于// 3.。所以即使// 1.// 3. 被删除,// 2. 仍然有效。您也可以确认这一点。所以我无法理解Block 1 means // 1. Create an object for using at the resumable upload. When I tried to run Block 2 alone, it does not retrieve the Location. 的逻辑。这是因为我的技术不好。我对此深表歉意。为了正确理解您的新问题,您能否将复制问题的脚本添加到您的问题中?借此,我想确认一下。 @Tyler Rake 感谢您的回复。不幸的是,我无法从您的新问题Why is it that when I try to retrieve the Location in step 1 it returns null? 中复制您的情况。因为在我的环境中,脚本有效。这是因为我的技术不好。我对此深表歉意。为了正确理解您的新问题,您能否将复制问题的脚本添加到您的问题中?借此,我想确认一下。 @Tyler Rake 添加脚本时,请将其添加到您的问题中。因为评论里的脚本很难看懂。如果您能合作解决您的问题,我很高兴。 @Tyler Rake 顺便说一句,从您评论中的附加脚本来看,我认为端点不正确。请再次确认。 @Tyler Rake 从您更新的问题中,我可以清楚地了解您脚本的修改点。请将端点从https://www.googleapis.com/drive/v3/files?uploadType=resumable 修改为https://www.googleapis.com/upload/drive/v3/files?uploadType=resumable 并再次测试。这在官方文档中也可以看到。 Ref这个请小心。【参考方案2】:

Tanaike 的回答非常完美。它很优雅,甚至帮助我了解了array.reduce 函数。在我问这个问题之前,我对 javascript 的了解很少,使用Google Drive API 的知识几乎为零。

我的目的是使用Google Apps Script作为语言逐步学习resumable upload的整个过程。 使用 Tanaike 的代码作为参考 我编写了一个脚本,它不是高效、易于管理和优雅的,而是让我自己(至少)了解 resumable upload 的工作原理。我没有使用循环,没有对象,甚至没有使用数组。

第 1 步(声明必要的变量)

  var fileId1 = "XXXXXXXXXXX"; //id of the first file
  var fileId2 = "YYYYYYYYYYY"; //id of the second file
  var filename = "merged.pdf"; //name of the final merged file
  var mimeType = MimeType.PDF; //Mime type of the merged file

第 2 步(启动可恢复上传)

//declare the end point
const url = "https://www.googleapis.com/upload/drive/v3/files?uploadType=resumable";

//Send the request
//Method to be used is Post during initiation
//No file is to be sent during initiation
//The file name and the mime type are sent
const res1 = UrlFetchApp.fetch(url, 
    method: "post",
    contentType: "application/json",
    payload: JSON.stringify(name: filename, mimeType: mimeType),
    headers: authorization: "Bearer " + ScriptApp.getOAuthToken()
  );

第 3 步(保存可恢复会话 URI)

const location = res1.getHeaders().Location;

步骤 4 (a)(上传文件 1)

注意:步骤 4 (a) 和 (b) 可以使用循环来执行。就我而言,我使用了两次没有循环

  var file = DriveApp.getFileById(fileId1); //get the first file
  var data = file.getBlob().getBytes(); //get its contents in bytes array

//Method used is PUT not POST
//Content-Range will contain the range from starting byte to ending byte, then a slash
//and then file size
//bytes array of file's blob is put in data
  var params = 
    method : "put",
    headers : 
      'Content-Range' : `bytes 0-524287/687627`
    ,
    payload : data,
    muteHttpExceptions: true
  ; 

//Request using Resumable session URI, and above params as parameter

  var result = UrlFetchApp.fetch(location,params);

步骤 4 (b)(上传第二个文件)

//Almost same as Step 4 (a)
//The thing that changes is Content Range
file = DriveApp.getFileById(fileId2);
  data = file.getBlob().getBytes();

  params = 
    method : "put",
    headers : 
      'Content-Range' : `bytes 524288-687626/687627`
    ,
    payload : data,
    muteHttpExceptions : true
  ;

  result = UrlFetchApp.fetch(location, params);

现在,与其重复第 4 步n 次数,不如使用循环。

此外,此代码不会检查在此过程中可能发生的错误。

希望这段代码对某人有所帮助,尽管它更像是一个自学实验。 :)

【讨论】:

以上是关于如何使用 Apps 脚本将 Drive 中的现有文件上传到 Drive的主要内容,如果未能解决你的问题,请参考以下文章

用于复制整个 Google Drive 文件结构的 Google Apps 脚本;如何避免超时?

如何在 Google Apps 脚本中使用 UrlFetchApp 发出 Drive API 批处理请求

用于将 google drive 中的文件名列表与 google sheet 列中的名称列表进行比较的 Apps 脚本,以将新文件从驱动器添加到工作表

使用 AdWords 脚本,如何将 Merchant Center 中的 Feed 替换为 Google Drive 中的 .txt 文件?

Google Apps 脚本 - 将 gmail 中的数据提取到电子表格中

BigQuery AppsScript 客户端的默认范围不包括 Google Drive