You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Java Google Drive API V3大文件多部分可恢复上传技术求助

我来帮你一步步搞定Google Drive大文件的可恢复分片上传问题,你的核心需求是处理暂停、网络故障甚至重启应用后的断点续传,咱们逐个拆解疑问,再给出完整的改造代码:

1. 先搞懂可恢复上传的核心逻辑

Google Drive的可恢复上传是基于会话URI的——当你初始化上传后,服务端会返回一个专属的会话地址,后续所有上传请求都要绑定这个地址。想要断点续传,最关键的就是把这个会话URI和文件的关键信息(本地路径、总大小)持久化存起来(比如本地JSON文件、SQLite数据库),重启应用后靠它找回上传进度。

2. 解决你的三个核心问题

2.1 如何获取已上传到Drive的总字节数

当你需要恢复上传时,先给之前保存的会话URI发一个HEAD请求,服务端的响应头里会有Range字段,格式是bytes=0-XXXX,这里的XXXX就是已上传的最后一个字节的索引,总上传字节数就是XXXX + 1(因为是左闭右开区间)。

举个Java代码示例(用原生HttpURLConnection):

String resumeUri = "你之前保存的会话URI";
HttpURLConnection connection = (HttpURLConnection) new URL(resumeUri).openConnection();
connection.setRequestMethod("HEAD");
connection.connect();

String rangeHeader = connection.getHeaderField("Range");
if (rangeHeader != null) {
    // 解析Range: bytes=0-2097151
    String[] parts = rangeHeader.split("=")[1].split("-");
    long uploadedBytes = Long.parseLong(parts[1]) + 1;
    System.out.println("已上传字节数: " + uploadedBytes);
}
connection.disconnect();

2.2 如何用Content-Range请求头恢复上传

其实Google的Java客户端已经帮你封装了这个逻辑,不用手动拼接Content-Range头。你只需要在恢复上传时,指定本地文件的起始读取位置,客户端会自动处理请求头的设置。

如果要手动实现的话,格式是Content-Range: bytes start-end/total,比如已上传2MB,本次传下一个2MB,文件总大小是60MB,那就是Content-Range: bytes 2097152-4194303/62914560。不过用官方SDK的话完全不用自己写这个,省心很多。

2.3 实现用户暂停上传 & 重启应用恢复

  • 用户暂停上传:MediaHttpUploader没有直接的pause()方法,但你可以通过两种方式实现:

    1. 设置一个全局的布尔标志位(比如isPaused),在进度监听里检查,如果标志位为true,就调用uploader.abort()终止当前上传——这个方法会保留会话URI,不会让之前的上传进度丢失。
    2. 如果是异步上传,可以中断执行上传的线程(注意要安全中断,避免资源泄漏)。
      另外你提到的executeAsInputStream()是用于下载的,上传用不上,上传用execute()或者异步的executeAsync()就行。
  • 重启应用后恢复:关键是持久化以下信息:

    • 可恢复会话URI(resumeUri)
    • 本地文件的绝对路径
    • 文件的总字节数(可选,也可以从本地文件重新获取)
      下次启动应用时,先读取这些信息,然后基于会话URI恢复上传流程。
3. 完整改造后的代码示例

下面是整合了暂停、恢复、持久化功能的代码,我会加上详细注释:

首先,先写一个简单的工具类来持久化会话信息(用JSON格式存到本地文件):

import com.google.gson.Gson;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;

// 用于保存上传会话的信息
class UploadSession {
    String resumeUri;
    String localFilePath;
    long totalFileSize;
}

// 持久化工具类
class SessionPersistence {
    private static final String SESSION_FILE = "upload-session.json";
    private static final Gson gson = new Gson();

    // 保存会话信息
    public static void saveSession(UploadSession session) throws IOException {
        try (FileWriter writer = new FileWriter(SESSION_FILE)) {
            gson.toJson(session, writer);
        }
    }

    // 加载会话信息
    public static UploadSession loadSession() throws IOException {
        try (FileReader reader = new FileReader(SESSION_FILE)) {
            return gson.fromJson(reader, UploadSession.class);
        }
    }

    // 删除会话信息(上传完成后调用)
    public static void deleteSession() {
        new java.io.File(SESSION_FILE).delete();
    }
}

然后是核心的上传/恢复逻辑:

import com.google.api.client.googleapis.media.MediaHttpUploader;
import com.google.api.client.googleapis.media.MediaHttpUploaderProgressListener;
import com.google.api.services.drive.Drive;
import com.google.api.services.drive.model.File;
import com.google.api.client.http.FileContent;

import java.io.IOException;
import java.io.RandomAccessFile;
import java.nio.channels.Channels;

public class ResumableUploader {
    private Drive service;
    private volatile boolean isPaused = false; // 暂停标志位

    public ResumableUploader(Drive service) {
        this.service = service;
    }

    // 初始化新上传或恢复已有上传
    public void startOrResumeUpload() throws IOException {
        UploadSession session = null;
        try {
            // 尝试加载已保存的会话
            session = SessionPersistence.loadSession();
        } catch (IOException e) {
            // 没有会话文件,说明是新上传
            System.out.println("没有找到已保存的上传会话,开始新上传");
            startNewUpload();
            return;
        }

        // 恢复已有上传
        System.out.println("找到已保存的会话,恢复上传");
        resumeExistingUpload(session);
    }

    // 开始新上传
    private void startNewUpload() throws IOException {
        // 你的原始初始化代码
        File fileMetadata = new File();
        fileMetadata.setName("Video.mp4");
        fileMetadata.setMimeType("application/vnd.google-apps.video");
        java.io.File filePath = new java.io.File("E:\\large-file-60MB.mp4");
        FileContent mediaContent = new FileContent("video/mp4", filePath);

        Drive.Files.Create create = service.files().create(fileMetadata, mediaContent);
        MediaHttpUploader uploader = create.getMediaHttpUploader();
        uploader.setDirectUploadEnabled(false);
        uploader.setChunkSize(2 * MediaHttpUploader.MINIMUM_CHUNK_SIZE);

        // 设置进度监听,同时检查暂停标志
        uploader.setProgressListener(getProgressListener(uploader, filePath.length()));

        // 初始化上传,获取会话URI并保存
        uploader.setInitiationCallback(initiation -> {
            try {
                UploadSession session = new UploadSession();
                session.resumeUri = uploader.getResumeUri().toString();
                session.localFilePath = filePath.getAbsolutePath();
                session.totalFileSize = filePath.length();
                SessionPersistence.saveSession(session);
                System.out.println("上传会话已保存: " + session.resumeUri);
            } catch (IOException e) {
                e.printStackTrace();
            }
        });

        // 执行上传
        try {
            create.execute();
            // 上传完成,删除会话文件
            SessionPersistence.deleteSession();
            System.out.println("上传完成!");
        } catch (IOException e) {
            // 网络故障等异常,会话已经保存,下次可以恢复
            System.out.println("上传中断,可下次恢复");
            e.printStackTrace();
        }
    }

    // 恢复已有上传
    private void resumeExistingUpload(UploadSession session) throws IOException {
        java.io.File filePath = new java.io.File(session.localFilePath);
        // 先获取已上传的字节数
        long uploadedBytes = getUploadedBytes(session.resumeUri);

        // 创建RandomAccessFile,从已上传位置开始读取
        RandomAccessFile raf = new RandomAccessFile(filePath, "r");
        raf.seek(uploadedBytes);

        // 创建FileContent,使用RandomAccessFile的通道
        FileContent mediaContent = new FileContent("video/mp4", Channels.newInputStream(raf.getChannel()));
        // 设置内容长度为剩余字节数
        mediaContent.setLength(session.totalFileSize - uploadedBytes);

        // 基于会话URI创建上传请求
        Drive.Files.Create create = service.files().create(null, mediaContent);
        MediaHttpUploader uploader = create.getMediaHttpUploader();
        uploader.setDirectUploadEnabled(false);
        uploader.setChunkSize(2 * MediaHttpUploader.MINIMUM_CHUNK_SIZE);
        // 设置恢复URI
        uploader.setResumeUri(session.resumeUri);

        // 设置进度监听,同时检查暂停标志
        uploader.setProgressListener(getProgressListener(uploader, session.totalFileSize));

        // 执行恢复上传
        try {
            create.execute();
            SessionPersistence.deleteSession();
            System.out.println("恢复上传完成!");
        } catch (IOException e) {
            System.out.println("恢复上传中断,可再次恢复");
            e.printStackTrace();
        } finally {
            raf.close();
        }
    }

    // 获取已上传字节数
    private long getUploadedBytes(String resumeUri) throws IOException {
        java.net.HttpURLConnection connection = (java.net.HttpURLConnection) new java.net.URL(resumeUri).openConnection();
        connection.setRequestMethod("HEAD");
        connection.connect();

        String rangeHeader = connection.getHeaderField("Range");
        connection.disconnect();

        if (rangeHeader == null) {
            return 0; // 没有上传任何内容
        }

        String[] parts = rangeHeader.split("=")[1].split("-");
        return Long.parseLong(parts[1]) + 1;
    }

    // 进度监听器,包含暂停检查
    private MediaHttpUploaderProgressListener getProgressListener(MediaHttpUploader uploader, long totalSize) {
        return uploading -> {
            // 检查是否需要暂停
            if (isPaused) {
                uploader.abort();
                System.out.println("上传已暂停");
                return;
            }

            switch (uploading.getUploadState()) {
                case INITIATION_STARTED:
                    System.out.println("初始化上传会话...");
                    break;
                case INITIATION_COMPLETE:
                    System.out.println("上传会话初始化完成");
                    break;
                case MEDIA_IN_PROGRESS:
                    long uploaded = uploading.getNumBytesUploaded();
                    double progress = (double) uploaded / totalSize * 100;
                    System.out.printf("上传进度: %.2f%% | 已上传: %d MB%n", progress, uploaded / (1024*1024));
                    break;
                case MEDIA_COMPLETE:
                    System.out.println("上传完成!");
                    break;
            }
        };
    }

    // 外部调用暂停上传
    public void pauseUpload() {
        isPaused = true;
    }

    public static void main(String[] args) throws IOException {
        Drive service = GDrive.getService(); // 你的GDrive初始化代码
        ResumableUploader uploader = new ResumableUploader(service);

        // 示例:启动上传,5秒后暂停(实际可以绑定按钮点击等用户交互)
        new Thread(() -> {
            try {
                uploader.startOrResumeUpload();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }).start();

        // 模拟用户暂停
        try {
            Thread.sleep(5000);
            uploader.pauseUpload();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}
4. 关键注意事项
  • 会话URI有效期:Google Drive的可恢复会话URI一般有效期为7天,如果超过时间,需要重新初始化上传。
  • 分片大小:你设置的2MB是官方推荐的最小分片大小,也可以根据网络情况调整到更大(比如10MB),减少请求次数。
  • 持久化可靠性:如果是生产环境,建议用数据库存储会话信息,而不是本地文件,避免文件丢失或损坏。
  • 异常处理:要捕获各种IO异常、网络异常,确保会话信息在异常发生后依然能正确保存。

内容的提问来源于stack exchange,提问作者Sync it

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.07 19:32:52