From 448b7161d1ab4e353e4b759badbb09641c69057f Mon Sep 17 00:00:00 2001 From: pikachuim Date: Tue, 26 May 2026 14:45:28 +0800 Subject: [PATCH 1/2] fix(torrent): sync .torrent file when copy/move/rename/upload --- drivers/189pc/driver.go | 31 +++++++- drivers/189pc/torrent.go | 148 +++++++++++++++++++++++++++++++++++++++ drivers/189pc/types.go | 9 +-- drivers/189pc/utils.go | 61 +++++++++++++++- 4 files changed, 243 insertions(+), 6 deletions(-) diff --git a/drivers/189pc/driver.go b/drivers/189pc/driver.go index 82aa1c1af..84024f0f1 100644 --- a/drivers/189pc/driver.go +++ b/drivers/189pc/driver.go @@ -262,6 +262,16 @@ func (y *Cloud189PC) Move(ctx context.Context, srcObj, dstDir model.Obj) (model. if err = y.WaitBatchTask("MOVE", resp.TaskID, time.Millisecond*400); err != nil { return nil, err } + + // 跟随移动 torrent 文件 + if !srcObj.IsDir() { + var srcFolderId string + if f, ok := srcObj.(*Cloud189File); ok { + srcFolderId = f.ParentId + } + y.torrentFollowMove(srcFolderId, srcObj.GetName(), dstDir) + } + return srcObj, nil } @@ -298,6 +308,12 @@ func (y *Cloud189PC) Rename(ctx context.Context, srcObj model.Obj, newName strin } return nil, err } + + // 跟随重命名 torrent 文件 + if f, ok := srcObj.(*Cloud189File); ok { + y.torrentFollowRename(f.ParentId, srcObj.GetName(), newName) + } + switch f := srcObj.(type) { case *Cloud189File: return resp.toFile(f), nil @@ -319,7 +335,20 @@ func (y *Cloud189PC) Copy(ctx context.Context, srcObj, dstDir model.Obj) error { if err != nil { return err } - return y.WaitBatchTask("COPY", resp.TaskID, time.Second) + if err = y.WaitBatchTask("COPY", resp.TaskID, time.Second); err != nil { + return err + } + + // 跟随复制 torrent 文件 + if !srcObj.IsDir() { + var srcFolderId string + if f, ok := srcObj.(*Cloud189File); ok { + srcFolderId = f.ParentId + } + y.torrentFollowCopy(srcFolderId, srcObj.GetName(), dstDir) + } + + return nil } func (y *Cloud189PC) Remove(ctx context.Context, obj model.Obj) error { diff --git a/drivers/189pc/torrent.go b/drivers/189pc/torrent.go index 3068a0f7b..152d902c4 100644 --- a/drivers/189pc/torrent.go +++ b/drivers/189pc/torrent.go @@ -1,6 +1,7 @@ package _189pc import ( + "bytes" "context" "crypto/sha1" "encoding/hex" @@ -8,10 +9,13 @@ import ( "io" "net/url" "strings" + "time" "github.com/go-resty/resty/v2" "github.com/OpenListTeam/OpenList/v4/internal/model" + "github.com/OpenListTeam/OpenList/v4/internal/op" + "github.com/OpenListTeam/OpenList/v4/internal/stream" "github.com/OpenListTeam/OpenList/v4/pkg/torrent" "github.com/OpenListTeam/OpenList/v4/pkg/utils" ) @@ -167,6 +171,36 @@ func (y *Cloud189PC) RapidUploadFromTorrent(ctx context.Context, dstDir model.Ob return nil, fmt.Errorf("提交上传失败: %w", err) } + // 秒传成功后,将 torrent 文件上传到目标目录(异步,不影响秒传结果) + if y.Addition.GenerateTorrent { + capturedDstDir := dstDir + capturedIsFamily := isFamily + go func() { + torrentName := fileName + ".cas.torrent" + infoHash, _ := GetInfoHashHex(torrentData) + utils.Log.Infof("秒传成功,上传 torrent: %s (info_hash: %s, size: %d bytes)", + torrentName, infoHash, len(torrentData)) + + torrentFileStream := &stream.FileStream{ + Ctx: context.Background(), + Obj: &model.Object{ + Name: torrentName, + Size: int64(len(torrentData)), + IsFolder: false, + }, + Reader: bytes.NewReader(torrentData), + Mimetype: "application/x-bittorrent", + } + _, uploadErr := y.FastUpload(context.Background(), capturedDstDir, torrentFileStream, func(p float64) {}, capturedIsFamily, false) + if uploadErr != nil { + utils.Log.Warnf("上传 torrent 文件失败: %v", uploadErr) + } else { + utils.Log.Infof("torrent 文件已上传: %s", torrentName) + op.Cache.DeleteDirectory(y, capturedDstDir.GetPath()) + } + }() + } + return resp.toFile(), nil } @@ -294,3 +328,117 @@ func ComputeSliceMD5sFromReader(reader io.Reader, sliceSize int64) (string, []st fileMD5Hex := strings.ToUpper(hex.EncodeToString(fileMD5Hash.Sum(nil))) return fileMD5Hex, sliceMD5s, nil } + +// torrentFollowCopy 跟随复制 torrent 文件(异步,不影响主操作) +// srcFolderId: 源文件所在目录 ID +// srcFileName: 源文件名 +// dstDir: 目标目录 +func (y *Cloud189PC) torrentFollowCopy(srcFolderId string, srcFileName string, dstDir model.Obj) { + if !y.Addition.GenerateTorrent { + return + } + if srcFolderId == "" { + return + } + torrentName := srcFileName + ".cas.torrent" + isFamily := y.isFamily() + + go func() { + torrentFile, err := y.findFileByName(context.Background(), torrentName, srcFolderId, isFamily) + if err != nil { + // torrent 文件不存在,忽略 + return + } + // 复制 torrent 文件到目标目录 + _, copyErr := y.CreateBatchTask("COPY", IF(isFamily, y.FamilyID, ""), dstDir.GetID(), + map[string]string{"targetFileName": dstDir.GetName()}, + BatchTaskInfo{ + FileId: torrentFile.GetID(), + FileName: torrentFile.GetName(), + IsFolder: 0, + }) + if copyErr != nil { + utils.Log.Warnf("跟随复制 torrent 文件失败: %v", copyErr) + } + }() +} + +// torrentFollowMove 跟随移动 torrent 文件(异步,不影响主操作) +// srcFolderId: 源文件所在目录 ID +// srcFileName: 源文件名 +// dstDir: 目标目录 +func (y *Cloud189PC) torrentFollowMove(srcFolderId string, srcFileName string, dstDir model.Obj) { + if !y.Addition.GenerateTorrent { + return + } + if srcFolderId == "" { + return + } + torrentName := srcFileName + ".cas.torrent" + isFamily := y.isFamily() + + go func() { + torrentFile, err := y.findFileByName(context.Background(), torrentName, srcFolderId, isFamily) + if err != nil { + // torrent 文件不存在,忽略 + return + } + // 移动 torrent 文件到目标目录 + resp, moveErr := y.CreateBatchTask("MOVE", IF(isFamily, y.FamilyID, ""), dstDir.GetID(), + map[string]string{"targetFileName": dstDir.GetName()}, + BatchTaskInfo{ + FileId: torrentFile.GetID(), + FileName: torrentFile.GetName(), + IsFolder: 0, + }) + if moveErr != nil { + utils.Log.Warnf("跟随移动 torrent 文件失败: %v", moveErr) + return + } + _ = y.WaitBatchTask("MOVE", resp.TaskID, time.Millisecond*400) + }() +} + +// torrentFollowRename 跟随重命名 torrent 文件(异步,不影响主操作) +// folderId: 文件所在目录 ID +// oldFileName: 原文件名 +// newFileName: 新文件名 +func (y *Cloud189PC) torrentFollowRename(folderId string, oldFileName string, newFileName string) { + if !y.Addition.GenerateTorrent { + return + } + if folderId == "" { + return + } + oldTorrentName := oldFileName + ".cas.torrent" + newTorrentName := newFileName + ".cas.torrent" + isFamily := y.isFamily() + + go func() { + torrentFile, err := y.findFileByName(context.Background(), oldTorrentName, folderId, isFamily) + if err != nil { + // torrent 文件不存在,忽略 + return + } + + // 重命名 torrent 文件 + queryParam := make(map[string]string) + fullUrl := API_URL + method := "POST" + if isFamily { + fullUrl += "/family/file" + method = "GET" + queryParam["familyId"] = y.FamilyID + } + fullUrl += "/renameFile.action" + queryParam["fileId"] = torrentFile.GetID() + queryParam["destFileName"] = newTorrentName + + _, renameErr := y.request(fullUrl, method, func(req *resty.Request) { + req.SetContext(context.Background()).SetQueryParams(queryParam) + }, nil, &RenameResp{}, isFamily) + if renameErr != nil { + utils.Log.Warnf("跟随重命名 torrent 文件失败: %v", renameErr) + } + }() +} diff --git a/drivers/189pc/types.go b/drivers/189pc/types.go index c05e3867f..c2f7d5be5 100644 --- a/drivers/189pc/types.go +++ b/drivers/189pc/types.go @@ -166,10 +166,11 @@ type FamilyInfoResp struct { /*文件部分*/ // 文件 type Cloud189File struct { - ID String `json:"id"` - Name string `json:"name"` - Size int64 `json:"size"` - Md5 string `json:"md5"` + ID String `json:"id"` + Name string `json:"name"` + Size int64 `json:"size"` + Md5 string `json:"md5"` + ParentId string `json:"-"` // 由 getFiles 设置,不从 JSON 解析 LastOpTime Time `json:"lastOpTime"` CreateDate Time `json:"createDate"` diff --git a/drivers/189pc/utils.go b/drivers/189pc/utils.go index e863060af..e00918a50 100644 --- a/drivers/189pc/utils.go +++ b/drivers/189pc/utils.go @@ -210,6 +210,7 @@ func (y *Cloud189PC) getFiles(ctx context.Context, fileId string, isFamily bool) res = append(res, &resp.FileListAO.FolderList[i]) } for i := 0; i < len(resp.FileListAO.FileList); i++ { + resp.FileListAO.FileList[i].ParentId = fileId res = append(res, &resp.FileListAO.FileList[i]) } } @@ -947,6 +948,11 @@ func (y *Cloud189PC) FastUpload(ctx context.Context, dstDir model.Obj, file mode if tmpF != nil { writers = append(writers, tmpF) } + + // 如果启用了 torrent 生成,额外计算 SHA-1 piece hash + generateTorrent := y.Addition.GenerateTorrent + pieceSHA1Hashes := make([]byte, 0, count*20) + written := int64(0) for i := 1; i <= count; i++ { if utils.IsCanceled(ctx) { @@ -957,7 +963,17 @@ func (y *Cloud189PC) FastUpload(ctx context.Context, dstDir model.Obj, file mode byteSize = lastSliceSize } - n, err := utils.CopyWithBufferN(io.MultiWriter(writers...), file, byteSize) + // 如果需要生成 torrent,同时计算 SHA-1 + var sha1Writer hash.Hash + var multiWriter io.Writer + if generateTorrent { + sha1Writer = sha1Pkg.New() + multiWriter = io.MultiWriter(append(writers, sha1Writer)...) + } else { + multiWriter = io.MultiWriter(writers...) + } + + n, err := utils.CopyWithBufferN(multiWriter, file, byteSize) written += n if err != nil && err != io.EOF { return nil, err @@ -966,6 +982,11 @@ func (y *Cloud189PC) FastUpload(ctx context.Context, dstDir model.Obj, file mode sliceMd5Hexs = append(sliceMd5Hexs, strings.ToUpper(hex.EncodeToString(md5Byte))) partInfos = append(partInfos, fmt.Sprint(i, "-", base64.StdEncoding.EncodeToString(md5Byte))) sliceMd5.Reset() + + // 收集 SHA-1 piece hash + if generateTorrent && sha1Writer != nil { + pieceSHA1Hashes = append(pieceSHA1Hashes, sha1Writer.Sum(nil)...) + } } if tmpF != nil { @@ -1081,6 +1102,44 @@ func (y *Cloud189PC) FastUpload(ctx context.Context, dstDir model.Obj, file mode if err != nil { return nil, err } + + // 生成 torrent 文件(异步,不影响上传结果) + if generateTorrent && len(pieceSHA1Hashes) > 0 { + capturedDstDir := dstDir + capturedIsFamily := isFamily + capturedFileName := file.GetName() + go func() { + torrentData, err := GenerateTorrent(capturedFileName, size, fileMd5Hex, sliceMd5Hexs, sliceSize, pieceSHA1Hashes) + if err != nil { + utils.Log.Warnf("生成 torrent 失败: %v", err) + return + } + infoHash, _ := GetInfoHashHex(torrentData) + torrentName := capturedFileName + ".cas.torrent" + utils.Log.Infof("已生成 torrent: %s (info_hash: %s, size: %d bytes)", + torrentName, infoHash, len(torrentData)) + + // 将 torrent 文件上传到同一目录 + torrentFileStream := &stream.FileStream{ + Ctx: context.Background(), + Obj: &model.Object{ + Name: torrentName, + Size: int64(len(torrentData)), + IsFolder: false, + }, + Reader: bytes.NewReader(torrentData), + Mimetype: "application/x-bittorrent", + } + _, uploadErr := y.FastUpload(context.Background(), capturedDstDir, torrentFileStream, func(p float64) {}, capturedIsFamily, false) + if uploadErr != nil { + utils.Log.Warnf("上传 torrent 文件失败: %v", uploadErr) + } else { + utils.Log.Infof("torrent 文件已上传: %s", torrentName) + op.Cache.DeleteDirectory(y, capturedDstDir.GetPath()) + } + }() + } + return resp.toFile(), nil } From 5964b704701970356ed3ce874b4a67fe6d8d8acd Mon Sep 17 00:00:00 2001 From: Suyunmeng Date: Sat, 30 May 2026 11:09:50 +0800 Subject: [PATCH 2/2] fix(torrent): prevent recursive torrent generation --- drivers/189pc/torrent.go | 17 ++++++++++------- drivers/189pc/utils.go | 9 ++++++--- 2 files changed, 16 insertions(+), 10 deletions(-) diff --git a/drivers/189pc/torrent.go b/drivers/189pc/torrent.go index 152d902c4..052317d0a 100644 --- a/drivers/189pc/torrent.go +++ b/drivers/189pc/torrent.go @@ -93,7 +93,6 @@ func (y *Cloud189PC) RapidUploadFromTorrent(ctx context.Context, dstDir model.Ob sliceMd5Hex = strings.ToUpper(utils.GetMD5EncodeStr(strings.Join(upperSliceMD5s, "\n"))) } - // 使用与 Web 端一致的三步秒传流程 fullUrl := "https://upload.cloud.189.cn" if isFamily { @@ -114,7 +113,6 @@ func (y *Cloud189PC) RapidUploadFromTorrent(ctx context.Context, dstDir model.Ob initParams.Set("familyId", y.FamilyID) } - var uploadInfo InitMultiUploadResp _, err = y.request(fullUrl+"/initMultiUpload", "GET", func(req *resty.Request) { req.SetContext(ctx) @@ -123,7 +121,6 @@ func (y *Cloud189PC) RapidUploadFromTorrent(ctx context.Context, dstDir model.Ob return nil, fmt.Errorf("initMultiUpload 失败: %w", err) } - uploadFileId := uploadInfo.Data.UploadFileID // Step 2: checkTransSecond(用 fileMd5 + sliceMd5 + uploadFileId 检查秒传) @@ -133,7 +130,6 @@ func (y *Cloud189PC) RapidUploadFromTorrent(ctx context.Context, dstDir model.Ob "uploadFileId": uploadFileId, } - var checkResp struct { Data struct { FileDataExists int `json:"fileDataExists"` @@ -147,7 +143,6 @@ func (y *Cloud189PC) RapidUploadFromTorrent(ctx context.Context, dstDir model.Ob return nil, fmt.Errorf("秒传检查失败: %w", err) } - if checkResp.Data.FileDataExists != 1 { return nil, fmt.Errorf("秒传失败:云端不存在该文件(fileMD5=%s, sliceMD5=%s, size=%d)", fileMD5Upper, sliceMd5Hex, fileSize) } @@ -191,7 +186,7 @@ func (y *Cloud189PC) RapidUploadFromTorrent(ctx context.Context, dstDir model.Ob Reader: bytes.NewReader(torrentData), Mimetype: "application/x-bittorrent", } - _, uploadErr := y.FastUpload(context.Background(), capturedDstDir, torrentFileStream, func(p float64) {}, capturedIsFamily, false) + _, uploadErr := y.fastUpload(context.Background(), capturedDstDir, torrentFileStream, func(p float64) {}, capturedIsFamily, false, false) if uploadErr != nil { utils.Log.Warnf("上传 torrent 文件失败: %v", uploadErr) } else { @@ -297,6 +292,10 @@ func GetInfoHashHex(torrentData []byte) (string, error) { return hex.EncodeToString(t.InfoHash), nil } +func isCASTorrentFile(fileName string) bool { + return strings.HasSuffix(fileName, ".cas.torrent") +} + // ComputeSliceMD5sFromReader 从 reader 中计算每个 10MB 分片的 MD5 // 返回:整文件 MD5、分片 MD5 列表 func ComputeSliceMD5sFromReader(reader io.Reader, sliceSize int64) (string, []string, error) { @@ -350,7 +349,7 @@ func (y *Cloud189PC) torrentFollowCopy(srcFolderId string, srcFileName string, d return } // 复制 torrent 文件到目标目录 - _, copyErr := y.CreateBatchTask("COPY", IF(isFamily, y.FamilyID, ""), dstDir.GetID(), + resp, copyErr := y.CreateBatchTask("COPY", IF(isFamily, y.FamilyID, ""), dstDir.GetID(), map[string]string{"targetFileName": dstDir.GetName()}, BatchTaskInfo{ FileId: torrentFile.GetID(), @@ -359,6 +358,10 @@ func (y *Cloud189PC) torrentFollowCopy(srcFolderId string, srcFileName string, d }) if copyErr != nil { utils.Log.Warnf("跟随复制 torrent 文件失败: %v", copyErr) + return + } + if err = y.WaitBatchTask("COPY", resp.TaskID, time.Second); err != nil { + utils.Log.Warnf("等待跟随复制 torrent 文件失败: %v", err) } }() } diff --git a/drivers/189pc/utils.go b/drivers/189pc/utils.go index e00918a50..3f3824d8a 100644 --- a/drivers/189pc/utils.go +++ b/drivers/189pc/utils.go @@ -911,6 +911,11 @@ func (y *Cloud189PC) RapidUpload(ctx context.Context, dstDir model.Obj, stream m // 快传 func (y *Cloud189PC) FastUpload(ctx context.Context, dstDir model.Obj, file model.FileStreamer, up driver.UpdateProgress, isFamily bool, overwrite bool) (model.Obj, error) { + generateTorrent := y.Addition.GenerateTorrent && !isCASTorrentFile(file.GetName()) + return y.fastUpload(ctx, dstDir, file, up, isFamily, overwrite, generateTorrent) +} + +func (y *Cloud189PC) fastUpload(ctx context.Context, dstDir model.Obj, file model.FileStreamer, up driver.UpdateProgress, isFamily bool, overwrite bool, generateTorrent bool) (model.Obj, error) { var ( cache = file.GetFile() tmpF *os.File @@ -949,8 +954,6 @@ func (y *Cloud189PC) FastUpload(ctx context.Context, dstDir model.Obj, file mode writers = append(writers, tmpF) } - // 如果启用了 torrent 生成,额外计算 SHA-1 piece hash - generateTorrent := y.Addition.GenerateTorrent pieceSHA1Hashes := make([]byte, 0, count*20) written := int64(0) @@ -1130,7 +1133,7 @@ func (y *Cloud189PC) FastUpload(ctx context.Context, dstDir model.Obj, file mode Reader: bytes.NewReader(torrentData), Mimetype: "application/x-bittorrent", } - _, uploadErr := y.FastUpload(context.Background(), capturedDstDir, torrentFileStream, func(p float64) {}, capturedIsFamily, false) + _, uploadErr := y.fastUpload(context.Background(), capturedDstDir, torrentFileStream, func(p float64) {}, capturedIsFamily, false, false) if uploadErr != nil { utils.Log.Warnf("上传 torrent 文件失败: %v", uploadErr) } else {