@@ -44,8 +44,8 @@ const (
4444 uriQuota = "/api/quota"
4545
4646 //
47- chunkSize = 4 * 1024 * 1024 //分片大小4M
48- rapidUploadThreshold = 256 * 1024 //秒传阈值256KB
47+ chunkSize = 4 * 1024 * 1024 // 分片大小锁定为 4M (官方黄金标准,普通用户与会员通用)
48+ rapidUploadThreshold = 256 * 1024 // 秒传阈值 256KB
4949)
5050
5151// Options defines the configuration for this backend
@@ -390,6 +390,7 @@ func (f *Fs) listDirFile(ctx context.Context, dir string, start, limit int) ([]F
390390 "web" : {"1" },
391391 "folder" : {"0" },
392392 "showempty" : {"1" },
393+ "openapi" : {"xpansdk" },
393394 },
394395 }
395396 resp := & FileListOut {}
@@ -615,17 +616,20 @@ func (f *Fs) Purge(ctx context.Context, dir string) error {
615616// About gets quota information
616617func (f * Fs ) About (ctx context.Context ) (usage * fs.Usage , err error ) {
617618 opts := & rest.Opts {
618- Method : "GET" ,
619- RootURL : rootURL ,
620- Path : uriQuota ,
621- Parameters : map [string ][]string {},
619+ Method : "GET" ,
620+ RootURL : rootURL ,
621+ Path : uriQuota ,
622+ Parameters : map [string ][]string {
623+ "openapi" : {"xpansdk" },
624+ },
622625 }
623626 resp := QuotaOut {}
624- if err = f .call (ctx , opts , resp ); err != nil {
627+ if err = f .call (ctx , opts , & resp ); err != nil {
625628 return nil , err
626629 }
630+ free := resp .Total - resp .Used
627631 usage = & fs.Usage {
628- Free : & resp . Free ,
632+ Free : & free , // 百度接口返回 Free 常为 0,在此手动计算以满足 rclone 展示
629633 Total : & resp .Total ,
630634 Used : & resp .Used ,
631635 }
@@ -761,52 +765,167 @@ func (o *Object) upload(ctx context.Context, in io.Reader, size int64) error {
761765 }
762766 }
763767
764- // 2. 对于 2GB 以下文件,使用单次上传 (Simple Upload) 绕过分片上传 Bug (31064)
765- if size <= 2 * 1024 * 1024 * 1024 {
768+ // 2. 对于 4MB 以下文件,使用单次上传 (Simple Upload)
769+ // 根据官方文档,4MB 以上必须分片,PCS 的简单上传通道在超限时会不稳定 (易中断)
770+ if size <= 4 * 1024 * 1024 && size >= 0 {
766771 return o .simpleUpload (ctx , in , size )
767772 }
768773
769- // 3. 超过 2GB 或流式大数据量时,使用 XPAN 三阶段分片上传
770- // 注意:分片上传目前在部分账户/应用下可能报 31064 错误
774+ // 3. 超过 4MB 使用 XPAN 分片上传 (superfile2)
775+ // 3. 超过 4MB 使用 XPAN 分片上传 (superfile2)
776+ // 注意:由于 rclone 内部可能使用 AsyncReader 包装,无法可靠检测 Seek 支持
777+ // 因此统一使用 Disk Spooling 模式(写盘缓存)处理大文件,避免内存 OOM
778+
771779 var md5s []string
772- var chunks [][]byte
773- buf := make ([]byte , chunkSize )
780+ var uploadSource io.ReaderAt
781+ // 如果是 Seeker(本地文件),可以直接使用
782+ if seeker , ok := in .(io.ReadSeeker ); ok {
783+ // 再次尝试 Seek Detect,排除 AsyncReader 的假实现
784+ if _ , err := seeker .Seek (0 , io .SeekCurrent ); err == nil {
785+ fs .Debugf (o , "检测到本地文件流,开启双读流式上传模式" )
786+ uploadSource = seeker .(io.ReaderAt ) // os.File implements ReaderAt
787+
788+ // 第一遍:计算所有分片的 MD5
789+ buf := make ([]byte , chunkSize )
790+ for {
791+ n , err := io .ReadFull (seeker , buf )
792+ if n > 0 {
793+ h := md5 .New ()
794+ h .Write (buf [:n ])
795+ md5s = append (md5s , hex .EncodeToString (h .Sum (nil )))
796+ }
797+ if err == io .EOF || err == io .ErrUnexpectedEOF {
798+ break
799+ }
800+ if err != nil {
801+ return fmt .Errorf ("扫描文件 MD5 失败: %w" , err )
802+ }
803+ }
774804
775- for {
776- n , err := io .ReadFull (in , buf )
777- if n > 0 {
778- chunkCopy := make ([]byte , n )
779- copy (chunkCopy , buf [:n ])
780- chunks = append (chunks , chunkCopy )
781-
782- h := md5 .New ()
783- h .Write (chunkCopy )
784- md5s = append (md5s , hex .EncodeToString (h .Sum (nil )))
805+ // 复位偏移量
806+ if _ , err := seeker .Seek (0 , io .SeekStart ); err != nil {
807+ return fmt .Errorf ("复位文件指针失败: %w" , err )
808+ }
809+
810+ goto DoUpload
785811 }
786- if err == io .EOF || err == io .ErrUnexpectedEOF {
787- break
812+ }
813+
814+ // 如果非 Seeker 且文件较大 (>128MB),使用临时文件缓存,避免内存溢出
815+ if size > 128 * 1024 * 1024 {
816+ fs .Debugf (o , "大文件上传 (>128MB) 且无法 Seek,启用磁盘缓存模式..." )
817+ tempFile , err := os .CreateTemp ("" , "rclone-baidu-upload-*" )
818+ if err != nil {
819+ return fmt .Errorf ("创建临时缓存文件失败: %w" , err )
788820 }
821+ defer func () {
822+ tempFile .Close ()
823+ os .Remove (tempFile .Name ())
824+ }()
825+
826+ uploadSource = tempFile // os.File implements ReaderAt
827+
828+ buf := make ([]byte , chunkSize )
829+ for {
830+ n , err := io .ReadFull (in , buf )
831+ if n > 0 {
832+ // 写入临时文件
833+ if _ , err := tempFile .Write (buf [:n ]); err != nil {
834+ return fmt .Errorf ("写入临时缓存文件失败: %w" , err )
835+ }
836+
837+ // 计算 MD5
838+ h := md5 .New ()
839+ h .Write (buf [:n ])
840+ md5s = append (md5s , hex .EncodeToString (h .Sum (nil )))
841+ }
842+ if err == io .EOF || err == io .ErrUnexpectedEOF {
843+ break
844+ }
845+ if err != nil {
846+ return err
847+ }
848+ }
849+
850+ // 确保数据落盘
851+ if err := tempFile .Sync (); err != nil {
852+ return fmt .Errorf ("同步临时文件失败: %w" , err )
853+ }
854+
855+ goto DoUpload
856+ }
857+
858+ // 4. 对于较小的流式输入 (<=128MB),缓冲至内存 (速度快)
859+ {
860+ fs .Debugf (o , "小文件流式上传 (<=128MB),全量缓存至内存处理..." )
861+ var chunks [][]byte
862+ buf := make ([]byte , chunkSize )
863+
864+ for {
865+ n , err := io .ReadFull (in , buf )
866+ if n > 0 {
867+ chunkCopy := make ([]byte , n )
868+ copy (chunkCopy , buf [:n ])
869+ chunks = append (chunks , chunkCopy )
870+
871+ h := md5 .New ()
872+ h .Write (chunkCopy )
873+ md5s = append (md5s , hex .EncodeToString (h .Sum (nil )))
874+ }
875+ if err == io .EOF || err == io .ErrUnexpectedEOF {
876+ break
877+ }
878+ if err != nil {
879+ return err
880+ }
881+ }
882+
883+ preResp , err := o .precreate (ctx , remote , size , md5s )
789884 if err != nil {
790- return err
885+ return fmt . Errorf ( "precreate 失败: %w" , err )
791886 }
887+ uploadID := preResp .UploadID
888+
889+ for i , chunkData := range chunks {
890+ _ , err := o .sliceUpload (ctx , remote , uploadID , i , bytes .NewReader (chunkData ), int64 (len (chunkData )))
891+ if err != nil {
892+ return fmt .Errorf ("分片 %d 上传失败: %w" , i , err )
893+ }
894+ }
895+
896+ md5ListBytes , _ := json .Marshal (md5s )
897+ file , err := o .complete (ctx , remote , uploadID , size , string (md5ListBytes ))
898+ if err != nil {
899+ return fmt .Errorf ("合并文件失败: %w" , err )
900+ }
901+ o .id = strconv .FormatUint (file .FsID , 10 )
902+ o .path = file .Path
903+ return nil
792904 }
793905
794- // 预上传 (Precreate)
906+ DoUpload:
795907 preResp , err := o .precreate (ctx , remote , size , md5s )
796908 if err != nil {
797909 return fmt .Errorf ("precreate 失败: %w" , err )
798910 }
799911 uploadID := preResp .UploadID
800912
801- // 分片上传 (SliceUpload)
802- for i , chunkData := range chunks {
803- _ , err := o .sliceUpload (ctx , remote , uploadID , i , bytes .NewReader (chunkData ), int64 (len (chunkData )))
804- if err != nil {
913+ for i := 0 ; i < len (md5s ); i ++ {
914+ var currentChunkSize int64
915+ if i == len (md5s )- 1 {
916+ currentChunkSize = size - int64 (i )* int64 (chunkSize )
917+ } else {
918+ currentChunkSize = int64 (chunkSize )
919+ }
920+
921+ // 使用 SectionReader 读取指定分片
922+ // uploadSource 必须是 io.ReaderAt (os.File 满足)
923+ sectionReader := io .NewSectionReader (uploadSource , int64 (i )* int64 (chunkSize ), currentChunkSize )
924+ if _ , err := o .sliceUpload (ctx , remote , uploadID , i , sectionReader , currentChunkSize ); err != nil {
805925 return fmt .Errorf ("分片 %d 上传失败: %w" , i , err )
806926 }
807927 }
808928
809- // 调用 Create 提交
810929 md5ListBytes , _ := json .Marshal (md5s )
811930 file , err := o .complete (ctx , remote , uploadID , size , string (md5ListBytes ))
812931 if err != nil {
@@ -826,6 +945,7 @@ func (o *Object) precreate(ctx context.Context, remote string, size int64, md5s
826945 v .Set ("autoinit" , "1" )
827946 v .Set ("rtype" , "3" )
828947 v .Set ("block_list" , string (md5ListJSON ))
948+ v .Set ("openapi" , "xpansdk" )
829949
830950 opts := & rest.Opts {
831951 Method : "POST" ,
@@ -892,6 +1012,7 @@ func (o *Object) rapidUpload(ctx context.Context, remote, contentMD5, sliceMD5 s
8921012 "slice-md5" : {sliceMD5 },
8931013 "content-crc32" : {fmt .Sprintf ("%d" , crc32Val )},
8941014 "ondup" : {"overwrite" },
1015+ "openapi" : {"xpansdk" },
8951016 },
8961017 }
8971018 resp := RapidUploadOut {}
@@ -917,9 +1038,10 @@ func (o *Object) simpleUpload(ctx context.Context, in io.Reader, size int64) err
9171038 "User-Agent" : "netdisk;P2SP;8.3.1.2;PC;PC-Windows;10.0.19042;WindowsBaiduYunGuanJia" ,
9181039 },
9191040 Parameters : map [string ][]string {
920- "method" : {"upload" },
921- "path" : {remote },
922- "ondup" : {"overwrite" },
1041+ "method" : {"upload" },
1042+ "path" : {remote },
1043+ "ondup" : {"overwrite" },
1044+ "openapi" : {"xpansdk" },
9231045 },
9241046 Body : formReader ,
9251047 }
@@ -943,7 +1065,7 @@ func (o *Object) sliceUpload(ctx context.Context, remote, uploadID string, partS
9431065 opts := & rest.Opts {
9441066 Method : "POST" ,
9451067 RootURL : uploadURL ,
946- Path : uriPCSFile ,
1068+ Path : uriSuperFile ,
9471069 ContentType : contentType ,
9481070 ContentLength : & contentLength ,
9491071 ExtraHeaders : map [string ]string {
@@ -955,6 +1077,7 @@ func (o *Object) sliceUpload(ctx context.Context, remote, uploadID string, partS
9551077 "path" : {remote },
9561078 "uploadid" : {uploadID },
9571079 "partseq" : {strconv .Itoa (partSeq )},
1080+ "openapi" : {"xpansdk" },
9581081 },
9591082 Body : formReader ,
9601083 }
@@ -974,6 +1097,7 @@ func (o *Object) complete(ctx context.Context, remote, uploadID string, size int
9741097 v .Set ("uploadid" , uploadID )
9751098 // block_list 需要是 ["...", "..."] 的 JSON 数组格式
9761099 v .Set ("block_list" , md5ListJSON )
1100+ v .Set ("openapi" , "xpansdk" )
9771101
9781102 opts := & rest.Opts {
9791103 Method : "POST" ,
0 commit comments