Drive Rest API V3中的可恢复上传

时间:2016-10-06 04:01:18

标签: java android google-drive-api

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

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

  1. 启动可恢复的会话
  2. 保存可恢复的会话URI
  3. 上传文件
  4. 步骤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]
    }
    

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

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

4 个答案:

答案 0 :(得分:5)

我正在尝试一周的大部分时间,我终于获得了可恢复的上传。它没有按照我预期的方式工作,但确实有效。

不要使用Drive REST API for Everything

据我所知,据我所知,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在this document中列出的规则:

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请求发送。使用credential变量获取授权令牌。 X-Upload-Content-Type的mime类型并不那么重要,它也可以不用它(this SO answer提供了一个很好的函数来从路径中检索它)。将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将再次成为mime类型。 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();

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

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

答案 1 :(得分:4)

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

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

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类中 所以我并没有真正回答这个问题(在哪里找到"位置")但我指出这一事实在使用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();

答案 2 :(得分:1)

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

嗯,在收到评论后,让我加上一些额外的描述。

然而," 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) throws MalformedURLException, IOException
        {
                URL url = new URL("https://www.googleapis.com/upload/drive/v3/files?uploadType=resumable");
                HttpURLConnection req = (HttpURLConnection) url.openConnection();
                req.setRequestMethod("POST");
                req.setDoInput(true);
                req.setDoOutput(true);
                req.setRequestProperty("Authorization", "Bearer " + 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 request, HttpServletResponse response, String sessionUri, com.google.api.services.drive.model.File jsonStructure, java.io.File file, long chunkStart, long uploadBytes) throws MalformedURLException, IOException
        {
            URL url1 = new URL(sessionUri);
            HttpURLConnection req1 = (HttpURLConnection) url1.openConnection();

            req1.setRequestMethod("PUT");
            req1.setDoOutput(true);
            req1.setDoInput(true);
            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().position(chunkStart);
            if (fileInputStream.read(buffer, 0, (int) uploadBytes) == -1);
            fileInputStream.close();

            outstream.write(buffer);
            outstream.close();

            req1.connect();

            return req1.getResponseCode();
        }

以下方法上传文件,将其分成块。


    public void uploadFile(HttpServletRequest request, HttpServletResponse response, Credential credential, com.google.api.services.drive.model.File jsonStructure, java.io.File file) throws 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)) throw new UploadFileException(responseCode);
            }
        }

这就是全部。

答案 3 :(得分:0)

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

如果是这种情况,我建议首先确定您是否获得了200 OK Http状态代码,并循环getAllheaders以确定是否列出了Location标头。< / p>

希望这有帮助!