使用 Http 将可恢复和多部分文件上传到 google 驱动器。最后一个块溢出错误

Resumable & multipart file upload to google drive using Http. last chunk overflow error

所以我根据 google 文档 here

的规范使用 resumable/multipart 将文件上传到 google 驱动器的指南

一切顺利我很久以前就写了这段代码并且它已经为这么多文件工作了很长时间直到昨天我尝试使用相同的代码上传一个不是 256KB 倍数的示例文件[再次从 google 文档中指定]

这是相关的 classes

1) URLMaker :- HttpUrlFactory 用于持续块上传

 /*A Utility Class for constantly creating URL connections for each chunk upload as an instance can be used to send only one request */
 final class URLMaker 
 {
     static HttpURLConnection openConnection(String session, String method, String[] params, String[] values, boolean input, boolean output)throws IOException
     {
      HttpURLConnection request = (HttpURLConnection)new URL(session).openConnection();

      request.setRequestMethod(method);
      request.setDoInput(input);
      request.setDoOutput(output);
      request.setConnectTimeout(10000);

     //params & values array are of equal length so just one->one key:value pass it as headers
      for(int i=0;i<params.length;i++){request.setRequestProperty(params[i],values[i]);}

      return request;
   }
 }

2) 会话:- 用于检索可恢复的 uri 会话,我将其保存到文件并稍后读取

final class Session
{
 private static final File SESSION=new File("E:/Session.txt");

 private static String read()
 {
  try(ObjectInputStream ois=new ObjectInputStream(new FileInputStream(SESSION)))
  {
   URISession session=(URISession)ois.readObject();

   return session.uri;
  }
  catch(Exception ex){return null;}
 }

 static String makeSession(String token,File file)throws Exception
 {
  if(SESSION.exists()){return read();} //If I Previously saved an resumable URI reload that from file

  String body = "{\"name\": \"" + file.getName() + "\"}";

  String fields[]=new String[5];
  fields[0]="Authorization";
  fields[1]="X-Upload-Content-Type";
  fields[2]="X-Upload-Content-Length";
  fields[3]="Content-Type";
  fields[4]="Content-Length";

  String values[]=new String[5];
  values[0]="Bearer "+token;
  values[1]="*audio/mp3*";           //generic octet stream
  values[2]=String.format(Locale.ENGLISH, "%d",file.length());
  values[3]="application/json; charset=UTF-8";
  values[4]=String.format(Locale.ENGLISH, "%d", body.getBytes().length);

  HttpURLConnection request =URLMaker.openConnection("https://www.googleapis.com/upload/drive/v3/files?uploadType=resumable"
                                                    ,"POST"
                                                    ,fields,values, true, true);
  try(OutputStream outputStream = request.getOutputStream()){outputStream.write(body.getBytes());}

  request.connect();

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

   SESSION.createNewFile();

   write(uri);

   return uri;
  }
  else
  {
   System.err.println(request.getResponseCode()+"/"+request.getResponseMessage());

   System.out.println(new String(request.getErrorStream().readAllBytes()));
  }
  return null;
 }

 private static void write(String uri)
 {
  try(ObjectOutputStream oos=new ObjectOutputStream(new FileOutputStream(SESSION)))
  {
   oos.writeObject(new URISession(uri));
  }
  catch(Exception ex){ex.printStackTrace(System.out);}
 }

 private static class URISession implements Serializable
 {
  private final String uri;

  private URISession(String uri){this.uri=uri;}
 }
}

我的问题所在 class

3)Uploader :- 将数据从文件上传到uri

final class Uploader 
{
 private static final int
 KB=1024,
 _256KB=256*KB; 

 public static void uploadFile(String session,File file)throws IOException
 {
  long uploadPosition,bytesDone,workLeft;
  if((uploadPosition=getLastUploadedByte(session,file.length()))==-1){return;} //read the last uploaded byte position[0-length-1 value] if this upload previously failed due to network/server issues
  bytesDone=uploadPosition;  //if +ve value it means my work is a little less

  String
  fields[]={"Content-Type","Content-Length","Content-Range"},
  values[]=new String[]{"audio/mp3*","",""}; 

  try(RandomAccessFile raFile = new RandomAccessFile(file,"r"))
  {
   raFile.seek(uploadPosition); 

   byte buffer[]=new byte[_256KB];
   while((workLeft=raFile.read(buffer))!=-1)  //read certain amount of work/or bytes to be uploaded
   {
    int code=0;
    while(workLeft>0)// it is possible that google might not have uploaded all my bytes I have sent so I keep trying until what I have read has been completely uploaded
    {
     values[1]=String.format(Locale.ENGLISH, "%d",workLeft);
     values[2]="bytes "+uploadPosition+"-"+(uploadPosition + workLeft - 1)+"/"+file.length();

     System.out.println("Specified Range="+values[1]+"/"+values[2]);

     HttpURLConnection request=URLMaker.openConnection(session,"PUT",fields,values, true, true);
     try(OutputStream bytes=request.getOutputStream()){bytes.write(buffer,0,(int)workLeft);}  
     request.connect();

     code=request.getResponseCode();
     if(code==308)   //Means successful but more data needs to be uploaded
     {
      String range = request.getHeaderField("range");
      int bytesSent=Integer.parseInt(range.substring(range.lastIndexOf("-") + 1, range.length())) + 1
     System.out.println("Sent Range="+range);

      workLeft-=bytesSent;  //Work Reduced By this many bytes
      bytesDone+=bytesSent; //Progress increased by this many bytes

      System.out.println("Bytes="+bytesDone+"/"+file.length());
     }
     else if(code!=200)
     {
      System.err.println("Upload Error:"+request.getResponseMessage());
      System.err.println("Upload Error Msg:"+new String(request.getErrorStream().readAllBytes()));

      code=-1;
      break;
     }

     request.disconnect();
    }

    if(code==-1){return;}
    else if(code==200 || code==201){System.out.println("Upload Completed");}
   } 
  }
 }

 private static long getLastUploadedByte(String session,long fileLength)throws IOException
 {
  HttpURLConnection request = URLMaker.openConnection(session
                                                     ,"PUT"
                                                     ,new String[]{"Content-Length","Content-Range"}
                                                     ,new String[]{"0","bytes */" + fileLength},true,true);

  try(OutputStream output=request.getOutputStream()){output.flush();}

  request.connect();

  long chunkPosition=-1;
  if(request.getResponseCode()==308 || request.getResponseCode()==200)
  {
   String range = request.getHeaderField("range"); //previously sent byte position[ it's a 0-length-1 value]
   if(range==null){return 0;} //suppose it's my first time ever using this uri then there would have been no bytes uploaded and hence no range header
   chunkPosition = Long.parseLong(range.substring(range.lastIndexOf("-") + 1, range.length())) + 1;//format[0-value]
  }
  else
  {
   System.err.println("Seek Error:"+request.getResponseCode()+"/"+request.getResponseMessage());
   System.err.println("Seek Error Msg:"+new String(request.getErrorStream().readAllBytes()));
  }

  request.disconnect();

  return chunkPosition;
 }
}

还有我的主Class

class FileUpload 
{
 private static final Credential getCredentials(NetHttpTransport HTTP_TRANSPORT,String authorize,Collection<String> SCOPES,boolean clearTokens)throws IOException,GeneralSecurityException
 {
  // Load client secrets.    
  InputStream in = GDrive.class.getResourceAsStream(CREDENTIALS_FILE_PATH);
  if (in == null){throw new FileNotFoundException("Resource not found: " + CREDENTIALS_FILE_PATH);}
  GoogleClientSecrets clientSecrets = GoogleClientSecrets.load(JSON_FACTORY, new InputStreamReader(in));

  // Build flow and trigger user authorization request.
  GoogleAuthorizationCodeFlow flow = new GoogleAuthorizationCodeFlow.Builder(HTTP_TRANSPORT,JSON_FACTORY,clientSecrets,SCOPES)
          .setDataStoreFactory(new FileDataStoreFactory(new File("tokens")))
          .setAccessType("offline")
          .build();

  LocalServerReceiver receiver = new LocalServerReceiver.Builder().setPort(8888).build();
  return new AuthorizationCodeInstalledApp(flow,receiver).authorize(authorize);
 }

 public static Credential makeCredentials(String authorize,Collection<String> SCOPES,boolean clearTokens)throws IOException,GeneralSecurityException
 {
  NetHttpTransport HTTP_TRANSPORT = GoogleNetHttpTransport.newTrustedTransport();

  return getCredentials(HTTP_TRANSPORT,authorize,SCOPES,clearTokens);
 }

 public static void main(String args[])throws Exception
 {
  Credential credentials=makeCredentials("user",List.of(DriveScopes.DRIVE_FILE));//authorize,scopes

  File media=new File("E:\Music\sample.mp3");

  String session=Session.makeSession(credentials.getAccessToken(),media);

  System.out.println("Session="+session);
  if(session==null){return;}

  Uploader.uploadFile(session,media);
 }
} 

问题来了,我有一个 2.01 MB(21,13,939 字节)的文件,您甚至可以从 here 下载该文件并自行测试,或使用任何类似文件

这是日志文件

Specified Range=262144/bytes 0-262143/2113939 Ok so I start of good uploading the first 256KB of data at every step
Sent Range=bytes=0-262143
Bytes=262144/2113939
Specified Range=262144/bytes 0-262143/2113939
Sent Range=bytes=0-262143
Bytes=524288/2113939
Specified Range=262144/bytes 0-262143/2113939   //No Problems here still an multiple of 256KB
Sent Range=bytes=0-262143
Bytes=786432/2113939
Specified Range=262144/bytes 0-262143/2113939
Sent Range=bytes=0-262143
Bytes=1048576/2113939
Specified Range=262144/bytes 0-262143/2113939
Sent Range=bytes=0-262143
Bytes=1310720/2113939
Specified Range=262144/bytes 0-262143/2113939
Sent Range=bytes=0-262143
Bytes=1572864/2113939
Specified Range=262144/bytes 0-262143/2113939
Sent Range=bytes=0-262143
Bytes=1835008/2113939
Specified Range=262144/bytes 0-262143/2113939
Sent Range=bytes=0-262143
Bytes=2097152/2113939

//       HERE IS WHERE LOGIC IS THROWN OUT THE WINDOW  THE LAST CHUNK   //

Specified Range=16787/bytes 0-16786/2113939  <---- I am clearly telling google that this is the last 16786 bytes of data of my file 
Sent Range=bytes=0-262143         <---- But google dosen't seem to care and uploads the whole 256KB byte array along with the garbage data from the previous read
Bytes=2359296/2113939     <--- And now I have uploaded more bytes than the actual file itself [format is bytesDone/fileSize]

我在应用程序无错误退出之前得到的最后一个响应代码是 308 我希望看到消息 "Upload Completed" 但它从来没有看到 文件从不显示在我的 google 驱动器上。

因为您已经猜到此代码适用于小于 256KB 或其倍数的文件,这就是为什么我直到现在才发现此代码有任何问题的原因

我发布了所有 classes 以便任何人都可以自己复制和测试它,但主要问题 我怀疑出在上传者 class a这就是我需要你帮助的地方。

我终于从你的输出中找到了你的问题:

Specified Range=262144/bytes 0-262143/2113939 Ok so I start of good uploading the first 256KB of data at every step
Sent Range=bytes=0-262143
Bytes=262144/2113939
Specified Range=262144/bytes 0-262143/2113939
Sent Range=bytes=0-262143
Bytes=524288/2113939
Specified Range=262144/bytes 0-262143/2113939   //No Problems here still an multiple of 256KB
Sent Range=bytes=0-262143
Bytes=786432/2113939
Specified Range=262144/bytes 0-262143/2113939

查看您的代码,实际进入 Content-Range 的部分是 values 数组的第三个位置:

values[2]="bytes "+uploadPosition+"-"+(uploadPosition + workLeft - 1)+"/"+file.length();

您似乎没有更新变量 uploadPoistion 所以您总是一遍又一遍地发送相同的范围。您甚至可以从驱动器的响应中看到:

String range = request.getHeaderField("range"); // This is always the same
int bytesSent=Integer.parseInt(range.substring(range.lastIndexOf("-") + 1, range.length())) + 1
System.out.println("Sent Range="+range);
Sent Range=bytes=0-262143

这个值在您的日志中一遍又一遍地重复,所以您只是在通知前 262144 个字节的信息。

您应该尝试每次增加 Content-Range 以覆盖文件的所有字节:

bytes 0-262143/2113939
bytes 262144-524287/2113939
bytes 524288- ....

这是对 @Raserhin 对我有用的答案的跟进。我只是在这里发布完整的代码。

变化

1) 将 uploadPosition 重命名为 driveIndex

2) 发送范围不是 return 每个块上传的字节数,而是 return 每个块上传的文件的 EOF 指针 [0- 上传文件的最后索引位置]

3) driveIndex 应分配范围 returned

4) 应该在while循环中引入一个localPosition,以便从上一个上传成功的地方读取字节

代码

public static void uploadFile(String session,File file)throws IOException
 {
  long driveIndex;
  if((driveIndex=getLastUploadedByte(session,file.length()))==-1){return;}

  String
  fields[]={"Content-Type","Content-Length","Content-Range"},
  values[]=new String[]{"audio/mp3","",""}; 

  long fileIndex;
  int localPos,workLeft;
  int bytesLeft,bytesUploaded;

  try(RandomAccessFile raFile = new RandomAccessFile(file,"r"))
  {
   raFile.seek(driveIndex); 

   byte buffer[]=new byte[_256KB];
   while((workLeft=raFile.read(buffer))!=-1)
   {
    int code=0;
    localPos=0;
    fileIndex=raFile.getFilePointer();

    while(workLeft>0)
    {
     values[1]=String.format(Locale.ENGLISH, "%d",0);
     values[2]="bytes "+driveIndex+"-"+(driveIndex + workLeft - 1)+"/"+file.length();

     HttpURLConnection request=URLMaker.openConnection(session,"PUT",fields,values, true, true);
     try(OutputStream bytes=request.getOutputStream()){bytes.write(buffer,localPos,workLeft);}
     request.connect();

     code=request.getResponseCode();
     if(code==308)
     {
      String range = request.getHeaderField("range");
      driveIndex=Long.parseLong(range.substring(range.lastIndexOf("-") + 1, range.length())) + 1;

      bytesLeft=(int)(fileIndex-driveIndex);
      bytesUploaded=workLeft-bytesLeft;

      workLeft-=bytesUploaded;           //bytes left to uploaded decreases
      localPos+=(int)bytesUploaded;      //next read start's from the next chunk 

      System.out.println(driveIndex+"/"+(file.length()-1));
     }
     else if(code==200)
     {
      System.out.println("Upload Completed");
      break;
     }
     else
     {
      System.err.println("Upload Error:"+request.getResponseMessage());
      System.err.println("Upload Error Msg:"+new String(request.getErrorStream().readAllBytes()));

      code=-1;
      break;
     }

     request.disconnect();
    } 
    if(code==-1 || code==200){break;}
   } 
   System.out.println("File Closed");
  }
  System.out.println("End");
 }

对于任何想要解释逻辑的人来说,这里是一个粗略的示例

Current=last uploaded byte index to drive [Assume finished at index 2]
FilePos=is the Filepointer after reading from Current[Here for example purpose we assume we read 5 bytes]


   [2]       [7]
0,1,2,3,4,5,6,7,8,9 <--------- File=10
    ^         ^
    |         |
 Current    FilePos

Bytes read                                              = 2,3,4,5,6,7
Local pos                                               = 0

while(workLeft>0)
{
 Cycle 1
 Bytes read                                              = 2,3,4,5,6,7[6 Bytes,Last Index=7]
 Work left                                               = 6
 Bytes uploaded                                          = 2,3[Assume only 2 bytes were uploaded for explanation sake,Last Index=3]                                     


 Num of bytes left[FilePos-UploadPos]                    = 7-3=>4
 Number of bytes uploaded[Work Left-Num of bytes left]   = 6-4=>2

 Work left-=Number of bytes uploaded                     = 6-2=>4
 Local pos+=Number of bytes uploaded                     = 0+2=>2

 Cycle 2
 Bytes Read                                              = 2,3,4,5,6,7[Last Index=7]
 Work left                                               = 4
 Bytes uploaded                                          = -,-,4,5,6[Assume Only 3 bytes were uploaded,Last Index=6]

 Num of bytes left[FilePos-UploadPos]                    = 7-6=>1
 Number of bytes uploaded[Work Left-Num of bytes left]   = 4-1=>3

 Work left-=Number of bytes uploaded                     = 4-3=>1
 Local Pos+=Number of bytes uploaded                     = 2+3=>5

 Cycle 3
 Bytes Read                                              = 2,3,4,5,6,7[Last Index=7]
 Work left                                               = 1
 Bytes uploaded                                          = -,-,-,-,-,7[Last Byte Uploaded,Last Index=7]

 Num of bytes left[FilePos-UploadPos]                    = 7-7=>0
 Number of bytes uploaded[Work Left-Num of bytes left]   = 1-0=>1

 Work left-=Number of bytes uploaded                     = 1-2=>0
 Local Pos+=Number of bytes uploaded                     = 5+1=>6
}
//when workleft becomes 0 while loop break's