Drive Rest API V3中的可恢复上传

我正在尝试使用Android中的驱动器API创建可恢复的上传会话。

根据文档,需要遵循的3个步骤是

  1. 开始一个可恢复的会话
  2. 保存可恢复的会话URI
  3. 上传文件

第1步:我使用以下代码启动可恢复会话。

File body = new File(); body.setName(fileName); body.setMimeType(mimeType); body.setCreatedTime(modifiedDate); body.setModifiedTime(modifiedDate); body.setParents(Collections.singletonList(parentId)); HttpHeaders header = new HttpHeaders(); header.setContentLength(0L); header.setContentType("application/json; charset=UTF-8"); header.set("X-Upload-Content-Type","image/jpeg"); HttpResponse response= driveObject .files() .create(body) .setRequestHeaders(header) .set("uploadType","resumable") .buildHttpRequest() .execute(); 

第2步:执行完成后,我打印请求的响应头以查看位置URI

 System.out.println(response.getHeader().toString()); 

输出如下

 { cache-control=[no-cache, no-store, max-age=0, must-revalidate], content-encoding=[gzip], content-type=[application/json; charset=UTF-8], date=[Thu, 06 Oct 2016 02:20:18 GMT], expires=[Mon, 01 Jan 1990 00:00:00 GMT], alt-svc=[quic=":443"; ma=2592000; v="36,35,34,33,32"], pragma=[no-cache], server=[GSE], transfer-encoding=[chunked], vary=[Origin, X-Origin], x-android-received-millis=[1475720421761], x-android-response-source=[NETWORK 200], x-android-sent-millis=[1475720420804], x-content-type-options=[nosniff], x-frame-options=[SAMEORIGIN], x-xss-protection=[1; mode=block] } 

我没有在响应头中findLocation URI来开始上传文件中指定的filedata,也没有find任何Java样本来执行可恢复上传。

如何检索文档中指定的位置URI?

我现在正在尝试一周的大部分时间,我终于得到了可恢复的上传。 它不会像我预期的那样工作,但确实有效。

不要将Drive REST API用于所有内容

我所学到的是,就我所知,Google Drive REST API并不能真正实现分块上传。 这可能是一个错误,也可能是设计错误。 我也可能太傻了。

但让我思考的是我无法在任何地方看到代码示例。 每个人都在谈论Http标题。 所以这就是我们下面要做的。 我们只使用标题。

以下是使用Google Drive REST API和Android进行可恢复,分块上传的方法:

0)初始化

 String accountName = "account_name"; GoogleAccountCredential credential = GoogleAccountCredential.usingOAuth2(context, Arrays.asList(SCOPES)).setBackOff(new ExponentialBackOff()).setSelectedAccountName(accountName); 

1)开始一个可恢复的会话

请遵循本文档中 Google概述的规则:

 POST /upload/drive/v3/files?uploadType=resumable HTTP/1.1 Host: www.googleapis.com Authorization: Bearer your_auth_token Content-Length: 38 Content-Type: application/json; charset=UTF-8 X-Upload-Content-Type: image/jpeg X-Upload-Content-Length: 2000000 { "name": "My File" } 

设置所有标题字段就像在Google的示例中一样。 将其作为POST请求发送。 使用您的credentialvariables获取授权令牌。 X-Upload-Content-Type的mimetypes并不那么重要,它也可以不用它( 这个SO答案提供了一个很好的函数来从路径中检索它)。 将X-Upload-Content-Length设置为文件的总长度。 将Content-Type设置为JSON格式,因为我们的主体将以JSON格式为Google提供元数据。

现在创建元数据主体。 我输入了文件名和父母。 将Content-Length设置为您的body长度(以字节为单位)。 然后将您的正文写入request.getOutputStream()输出流。

 URL url = new URL("https://www.googleapis.com/upload/drive/v3/files?uploadType=resumable"); HttpURLConnection request = (HttpURLConnection) url.openConnection(); request.setRequestMethod("POST"); request.setDoInput(true); request.setDoOutput(true); request.setRequestProperty("Authorization", "Bearer " + credential.getToken()); request.setRequestProperty("X-Upload-Content-Type", getMimeType(file.getPath())); request.setRequestProperty("X-Upload-Content-Length", String.format(Locale.ENGLISH, "%d", file.length())); request.setRequestProperty("Content-Type", "application/json; charset=UTF-8"); String body = "{\"name\": \"" + file.getName() + "\", \"parents\": [\"" + parentId + "\"]}"; request.setRequestProperty("Content-Length", String.format(Locale.ENGLISH, "%d", body.getBytes().length)); OutputStream outputStream = request.getOutputStream(); outputStream.write(body.getBytes()); outputStream.close(); request.connect(); 

2)保存可恢复会话URI

最后, connect()并等待响应。 如果响应代码为200 ,则表示您已成功启动了分块,可恢复的上载。 现在将location标头URI保存在某处(数据库,文本文件,等等)。 你以后会需要的。

 if (request.getResponseCode() == HttpURLConnection.HTTP_OK) { String sessionUri = request.getHeaderField("location"); } 

3)上传文件

 PUT {session_uri} HTTP/1.1 Host: www.googleapis.com Content-Length: 524288 Content-Type: image/jpeg Content-Range: bytes 0-524287/2000000 bytes 0-524288 

将以下代码放在循环中,直到整个文件上载。 在每个块之后,您将得到代码308range标题的响应。 从此range标题中,您可以读取下一个块开始(参见(4))。

Content-Type将再次成为mimetypes。 Content-Length是您在此块中上传的字节数。 Content-Range的格式必须为bytes startByte-EndByte/BytesTotal 。 你把它放在PUT请求中。

然后创建一个FileInputStream并将位置设置为起始字节(从上一个响应range标头获得)并将另一个块读入缓冲区。 然后将此缓冲区写入连接输出流。 最后, connect()

 URL url = new URL(sessionUri); HttpURLConnection request = (HttpURLConnection) url.openConnection(); request.setRequestMethod("PUT"); request.setDoOutput(true); request.setConnectTimeout(10000); request.setRequestProperty("Content-Type", getMimeType(file.getPath())); long uploadedBytes = chunkSizeInMb * 1024 * 1024; if (chunkStart + uploadedBytes > file.length()) { uploadedBytes = (int) file.length() - chunkStart; } request.setRequestProperty("Content-Length", String.format(Locale.ENGLISH, "%d", uploadedBytes)); request.setRequestProperty("Content-Range", "bytes " + chunkStart + "-" + (chunkStart + uploadedBytes - 1) + "/" + file.length()); byte[] buffer = new byte[(int) uploadedBytes]; FileInputStream fileInputStream = new FileInputStream(file); fileInputStream.getChannel().position(chunkStart); if (fileInputStream.read(buffer, 0, (int) uploadedBytes) == -1) { /* break, return, exit*/ } fileInputStream.close(); OutputStream outputStream = request.getOutputStream(); outputStream.write(buffer); outputStream.close(); request.connect(); 

4)处理响应

在此之后,您将获得代码308的响应(如果成功)。 此响应包含range标题(提及)。

 HTTP/1.1 308 Resume Incomplete Content-Length: 0 Range: bytes=0-524287 

您将其拆分并获取新的块开始字节。

  String range = chunkUploadConnection.getHeaderField("range"); int chunkPosition = Long.parseLong(range.substring(range.lastIndexOf("-") + 1, range.length())) + 1; 

5)响应代码不是308?!

您可能会收到5xx响应。 您的互联网连接可能会失败,文件可能会在上传过程中被删除/重命名等。别担心。 只要保存会话URI和块开始字节,就可以随时恢复上载。

为此,请发送以下表单的标头:

 PUT {session_uri} HTTP/1.1 Content-Length: 0 Content-Range: bytes */TotalFileLength URL url = new URL(sessionUri); HttpURLConnection request = (HttpURLConnection) url.openConnection(); request.setRequestMethod("PUT"); request.setDoOutput(true); request.setConnectTimeout(10000); request.setRequestProperty("Content-Length", "0"); request.setRequestProperty("Content-Range", "bytes */" + file.length()); request.connect(); 

然后,您将收到一个带有range标题的308 ,您可以从中读取上次上传的字节(就像我们上面所做的那样)。 拿这个号码再开始循环。

我希望我可以帮助你们中的一些人。 如果您还有其他问题,请在评论中提问,我将编辑答案。

你不必关心所有这些逻辑。 文档确实解释了完成可恢复上传的流程,但如果“手动”完成则容易出错。
幸运的是,谷歌公开了一个专门的类来处理这种情况,即MediaHttpUploader

这段代码完成了驱动器上可恢复上传的工作(在GCS上可以实现相同的function):

 public class Main { private static final JacksonFactory JSON_FACTORY = new JacksonFactory(); private static final NetHttpTransport HTTP_TRANSPORT = new NetHttpTransport(); private static final MemoryDataStoreFactory DATA_STORE = new MemoryDataStoreFactory(); public static void main(String... args) throws IOException { Credential credential = authorize(); MediaHttpUploader mediaHttpUploader = new MediaHttpUploader(new FileContent("application/json", Paths.get("/path/to/foo.json").toFile()), HTTP_TRANSPORT, credential); mediaHttpUploader.setProgressListener(uploader -> System.out.println("progress: " + uploader.getProgress())); GenericUrl genericUrl = new GenericUrl(new URL("https://www.googleapis.com/upload/drive/v3/files?name=toto")); GenericJson data = new GenericJson(); data.put("name", "title"); JsonHttpContent jsonHttpContent = new JsonHttpContent(JSON_FACTORY, data); mediaHttpUploader.setMetadata(jsonHttpContent).upload(genericUrl); System.out.println("Finished"); } private static Credential authorize() throws IOException { // load client secrets try (BufferedReader br = Files.newBufferedReader(Paths.get(Resources.getResource("client_secret.json").getPath()))) { GoogleClientSecrets clientSecrets = GoogleClientSecrets.load(JSON_FACTORY, br); // set up authorization code flow GoogleAuthorizationCodeFlow flow = new GoogleAuthorizationCodeFlow.Builder( HTTP_TRANSPORT, JSON_FACTORY, clientSecrets, Collections.singleton(DriveScopes.DRIVE)) .setAccessType("offline") .setDataStoreFactory(DATA_STORE).build(); // authorize return new AuthorizationCodeInstalledApp(flow, new LocalServerReceiver()).authorize("user"); } } 

}

请注意,我们没有提到位置。 所有逻辑都隐藏在MediaHttpUploader类中。
所以我并没有真正回答这个问题(在哪里可以find“位置”)但是我指出这一事实在使用Google库中的类时并不是真的需要(而且我很确定其他第三方库存在同样的工作) 。

更新:mediaHttpUploader是Drive v3客户端在幕后使用的内容。 所以我们可以考虑这样的事情:

  File fileMetadata = new File(); fileMetadata.setName(UPLOAD_FILE.getName()); FileContent mediaContent = new FileContent("image/jpeg", UPLOAD_FILE); Drive.Files.Create insert = drive.files().create(fileMetadata, mediaContent); MediaHttpUploader uploader = insert.getMediaHttpUploader(); uploader.setDirectUploadEnabled(false); uploader.setProgressListener(new FileUploadProgressListener()); return insert.execute(); 

也许这个https://github.com/PiyushXCoder/google-drive-ResumableUpload/blob/master/ResumableUpload.java可以帮到你。 但是,它是为servlet编写的,但您可以轻松地为Android修改它。

好了,在收到评论后,让我提出一些额外的说明。

但是,“ResumableUpload.java”github repo链接评论很好,它足以让你清楚如何在谷歌驱动器上执行此上传。 并且,您实际上不需要阅读这个长描述。

正如谷歌的https://developers.google.com/drive/v3/web/resumable-upload中所描述的,如何执行可恢复的上传

  • 我们需要发出POST请求以通知服务器有关此上载的内容,并获取会话URI,我们将向其发送文件的数据块。 是的,我们需要访问令牌来执行此请求(这里,Credential的对象具有访问令牌,我们将使用它)。 此请求由此方法执行:

     public String requestUploadUrl(HttpServletRequest request,HttpServletResponse response,Credential credential,com.google.api.services.drive.model.File jsonStructure)抛出MalformedURLException,IOException
         {
                urlurl =新url(“https://www.googleapis.com/upload/drive/v3/files?uploadType=resumable”);
                 HttpURLConnection req =(HttpURLConnection)url.openConnection();
                 req.setRequestMethod( “POST”);
                 req.setDoInput(真);
                 req.setDoOutput(真);
                 req.setRequestProperty(“授权”,“承载”+ credential.getAccessToken());
                 req.setRequestProperty(“X-Upload-Content-Type”,jsonStructure.getMimeType());
                 req.setRequestProperty(“X-Upload-Content-Length”,String.valueOf(jsonStructure.getSize()));
                 req.setRequestProperty(“Content-Type”,“application / json; charset = UTF-8”);

                 String body =“{\”name \“:\”“+ jsonStructure.getName()+”\“}”;
                 req.setRequestProperty(“Content-Length”,String.format(Locale.ENGLISH,“%d”,body.getBytes()。length));
                 OutputStream outputStream = req.getOutputStream();
                 outputStream.write(body.getBytes());
                 outputStream.close();
                 req.connect();

                 String sessionUri = null;

                 if(req.getResponseCode()== HttpURLConnection.HTTP_OK){
                     sessionUri = req.getHeaderField(“location”);
                 }
                 return sessionUri; 
             }

  • 现在,当我们获得会话URI时,我们可以继续发送我们所请求文件的数据。 并且,让我们为每个块执行PUT请求。 每个夹头的尺寸应为256KB的倍数。 以下方法可用于每个块。

     public int uploadFilePacket(HttpServletRequest请求,HttpServletResponse响应,String sessionUri,com.google.api.services.drive.model.File jsonStructure,java.io.File文件,long chunkStart,long uploadBytes)抛出MalformedURLException,IOException
         {
             URL url1 =新url(sessionUri);
             HttpURLConnection req1 =(HttpURLConnection)url1.openConnection();

             req1.setRequestMethod( “PUT”);
             req1.setDoOutput(真);
             req1.setDoInput(真);
             req1.setConnectTimeout(10000);

             req1.setRequestProperty(“Content-Type”,jsonStructure.getMimeType());
             req1.setRequestProperty(“Content-Length”,String.valueOf(uploadBytes));
             req1.setRequestProperty(“Content-Range”,“bytes”+ chunkStart +“ - ”+(chunkStart + uploadBytes -1)+“/”+ jsonStructure.getSize());

             OutputStream outstream = req1.getOutputStream();

             byte [] buffer = new byte [(int)uploadBytes];
             FileInputStream fileInputStream = new FileInputStream(file);
             。fileInputStream.getChannel()位置(chunkStart);
             if(fileInputStream.read(buffer,0,(int)uploadBytes)== -1);
             fileInputStream.close();

             outstream.write(缓冲液);
             outstream.close();

             req1.connect();

             return req1.getResponseCode();
         }

以下方法上载将文件分成块的文件。


     public void uploadFile(HttpServletRequest请求,HttpServletResponse响应,Credential凭证,com.google.api.services.drive.model.File jsonStructure,java.io.File文件)抛出IOException,UploadFileException
         {
             String sessionUrl = requestUploadUrl(request,response,credential,jsonStructure);

             for(long i = 1,j = CHUNK_LIMIT; i = jsonStructure.getSize())
                 {
                     j = jsonStructure.getSize() -  i + 1;
                 }
                 int responseCode = uploadFilePacket(request,response,sessionUrl,jsonStructure,file,i-1,j);
                 if(!(responseCode == OK || responseCode == CREATED || responseCode == INCOMPLETE))抛出新的UploadFileException(responseCode);
             }
         }

就这样。

如果您能够获得200 Http状态,它将提供Location作为标题的一部分。 但是从我在你的System.print上看到的,没有HttpResponse.getHeader ,这可能只是一个错字,你指的是HttpResponse.getHeaders

如果是这种情况,我建议首先确定你是否有200 OK Http状态代码,并循环getAllheaders以确定是否列出了Location头。

希望这可以帮助!