Java Google Drive API V3大文件多部分可恢复上传技术求助
我来帮你一步步搞定Google Drive大文件的可恢复分片上传问题,你的核心需求是处理暂停、网络故障甚至重启应用后的断点续传,咱们逐个拆解疑问,再给出完整的改造代码:
Google Drive的可恢复上传是基于会话URI的——当你初始化上传后,服务端会返回一个专属的会话地址,后续所有上传请求都要绑定这个地址。想要断点续传,最关键的就是把这个会话URI和文件的关键信息(本地路径、总大小)持久化存起来(比如本地JSON文件、SQLite数据库),重启应用后靠它找回上传进度。
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()方法,但你可以通过两种方式实现:- 设置一个全局的布尔标志位(比如
isPaused),在进度监听里检查,如果标志位为true,就调用uploader.abort()终止当前上传——这个方法会保留会话URI,不会让之前的上传进度丢失。 - 如果是异步上传,可以中断执行上传的线程(注意要安全中断,避免资源泄漏)。
另外你提到的executeAsInputStream()是用于下载的,上传用不上,上传用execute()或者异步的executeAsync()就行。
- 设置一个全局的布尔标志位(比如
重启应用后恢复:关键是持久化以下信息:
- 可恢复会话URI(resumeUri)
- 本地文件的绝对路径
- 文件的总字节数(可选,也可以从本地文件重新获取)
下次启动应用时,先读取这些信息,然后基于会话URI恢复上传流程。
下面是整合了暂停、恢复、持久化功能的代码,我会加上详细注释:
首先,先写一个简单的工具类来持久化会话信息(用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(); } } }
- 会话URI有效期:Google Drive的可恢复会话URI一般有效期为7天,如果超过时间,需要重新初始化上传。
- 分片大小:你设置的2MB是官方推荐的最小分片大小,也可以根据网络情况调整到更大(比如10MB),减少请求次数。
- 持久化可靠性:如果是生产环境,建议用数据库存储会话信息,而不是本地文件,避免文件丢失或损坏。
- 异常处理:要捕获各种IO异常、网络异常,确保会话信息在异常发生后依然能正确保存。
内容的提问来源于stack exchange,提问作者Sync it

