fix(seed): repair broken build of hash-driven rapid upload across 12 drivers

The previous commit (624fdd24) introduced the authoritative
driver.SeedRapidUploader interface, but left every driver's seed_rapid.go
on the older, incompatible API, so the branch did not compile at all.

Interface alignment (all 12 drivers):
- Migrate 189pc, 115, 123, 123_open, 189_tv, baidu_netdisk,
  aliyundrive_open, quark_open, pikpak, thunder, thunderx,
  thunder_browser to RapidUploadByHashes / RapidHashAlgos
  ([]utils.HashType, no longer []*utils.HashType) / RapidHashNeedsPieces
- Add shared driver.SeedHashStream as a complete model.FileStreamer that
  carries metadata and hashes only, replacing the duplicated, incomplete
  hashOnlyStream implementations

Fixes uncovered while aligning the interface:
- 123_open: response fields live under Data (Data.Reuse / Data.FileID)
- quark_open: pre.Data.FID -> pre.Data.Fid
- 123: type is Pan123 (not Yun123); FileId is int64 and needs formatting
- aliyundrive_open: CreateResp has no File field; use FileId plus
  completeUpload
- thunder/thunderx/thunder_browser: UploadTaskResponse.File is a Files
  value type, return &resp.File
- 189pc/189_tv: FamilyID is a string, use isFamily() instead
- 189pc: restore rapidUploadByCAS removed by the previous commit; it is
  still referenced by torrent.go. Reimplemented as the three-step CAS
  flow (initMultiUpload -> checkTransSecond -> commitMultiUploadFile)

Build and hashing fixes:
- hash_writer.go: HashType exposes NewFunc; GCID.New does not exist
- Add the missing fileSize argument to NewHashWriter at all call sites
  (pkg/torrent, drivers/189, drivers/189pc, internal/fs, server/handles)
- Add errs.ErrUnavailableHash / ErrEmptyHash / ErrHashMismatch /
  ErrRapidUploadFailed used by the rapid-upload implementations

Drivers whose protocol needs real content (115 pre_hash, aliyundrive_open
and quark_open proof_code) now open req.Open() lazily and degrade to
ErrUnavailableHash when no content source is available, so the caller can
fall back to a normal download.

Note: go vet warnings for non-constant format strings in 189pc/utils.go
are pre-existing and intentionally left untouched.
This commit is contained in:
PIKACHUIM
2026-09-12 21:56:42 +08:00
parent 79c1d9d721
commit f933d59ec4
22 changed files with 580 additions and 429 deletions
+60 -34
View File
@@ -2,47 +2,73 @@ package _115
import (
"context"
"io"
"os"
"time"
"strings"
"github.com/OpenListTeam/OpenList/v4/internal/driver"
"github.com/OpenListTeam/OpenList/v4/internal/errs"
"github.com/OpenListTeam/OpenList/v4/internal/model"
"github.com/OpenListTeam/OpenList/v4/pkg/http_range"
"github.com/OpenListTeam/OpenList/v4/pkg/utils"
)
// SeedRapidUpload 使用种子的哈希信息进行秒传
func (d *Pan115) SeedRapidUpload(ctx context.Context, dstDir model.Obj, fileName string, fileSize int64, hashes utils.HashInfo) (model.Obj, error) {
// 115 使用 SHA1 秒传
sha1Hash := hashes.GetHash(utils.SHA1)
if len(sha1Hash) < utils.SHA1.Width {
return nil, errs.EmptyHash
}
// 使用已有的 rapidUpload 私有方法
stream := &hashOnlyStream{
name: fileName,
size: fileSize,
hashInfo: hashes,
}
// 调用内部秒传方法
return d.rapidUpload(fileSize, fileName, dstDir.GetID(), "", "", stream)
// RapidHashAlgos 返回 115 支持的秒传哈希算法(SHA1)
func (d *Pan115) RapidHashAlgos() []utils.HashType {
return []utils.HashType{*utils.SHA1}
}
// hashOnlyStream 仅包含哈希信息的 FileStream
type hashOnlyStream struct {
name string
size int64
hashInfo utils.HashInfo
// RapidHashNeedsPieces 115 不需要分片哈希
func (d *Pan115) RapidHashNeedsPieces() bool {
return false
}
func (s *hashOnlyStream) GetName() string { return s.name }
func (s *hashOnlyStream) GetSize() int64 { return s.size }
func (s *hashOnlyStream) GetHash() utils.HashInfo { return s.hashInfo }
func (s *hashOnlyStream) Read(p []byte) (n int, err error) { return 0, io.EOF }
func (s *hashOnlyStream) Close() error { return nil }
func (s *hashOnlyStream) GetMimetype() string { return "" }
func (s *hashOnlyStream) ModTime() time.Time { return time.Now() }
func (s *hashOnlyStream) CreateTime() time.Time { return time.Now() }
func (s *hashOnlyStream) GetFile() *os.File { return nil }
// RapidUploadByHashes 使用种子中的 SHA1 哈希尝试秒传。
//
// 115 的秒传协议除了整文件 SHA1,还需要文件头部 128KB 的 SHA1(pre_hash),
// 因此当内容源可用时会打开它来计算前置哈希;内容不可用时无法秒传。
func (d *Pan115) RapidUploadByHashes(ctx context.Context, dstDir model.Obj, req *driver.SeedRapidUploadRequest, overwrite bool) (model.Obj, error) {
fullHash := strings.ToUpper(req.Whole.GetHash(utils.SHA1))
if len(fullHash) != utils.SHA1.Width {
return nil, errs.ErrUnavailableHash
}
if req.Open == nil {
return nil, errs.ErrUnavailableHash
}
src, err := req.Open()
if err != nil {
return nil, err
}
defer src.Close()
const PreHashSize int64 = 128 * utils.KB
hashSize := PreHashSize
if req.Size < PreHashSize {
hashSize = req.Size
}
reader, err := src.RangeRead(http_range.Range{Start: 0, Length: hashSize})
if err != nil {
return nil, err
}
preHash, err := utils.HashReader(utils.SHA1, reader)
if err != nil {
return nil, err
}
preHash = strings.ToUpper(preHash)
fastInfo, err := d.rapidUpload(req.Size, req.Name, dstDir.GetID(), preHash, fullHash, src)
if err != nil {
return nil, err
}
matched, err := fastInfo.Ok()
if err != nil {
return nil, err
}
if !matched {
return nil, errs.ErrRapidUploadFailed
}
f, err := d.getNewFileByPickCode(fastInfo.PickCode)
if err != nil {
return nil, err
}
return f, nil
}
+54 -37
View File
@@ -2,55 +2,72 @@ package _123
import (
"context"
"net/http"
"strconv"
"strings"
"time"
"github.com/OpenListTeam/OpenList/v4/drivers/base"
"github.com/OpenListTeam/OpenList/v4/internal/driver"
"github.com/OpenListTeam/OpenList/v4/internal/errs"
"github.com/OpenListTeam/OpenList/v4/internal/model"
"github.com/OpenListTeam/OpenList/v4/pkg/utils"
"github.com/go-resty/resty/v2"
)
// RapidHashAlgos 返回123云盘支持的秒传哈希算法(SHA1/MD5)
func (d *Yun123) RapidHashAlgos() []*utils.HashType {
// 123 优先使用 SHA1(Etag),也支持 MD5
return []*utils.HashType{utils.SHA1, utils.MD5}
// RapidHashAlgos 返回 123 云盘支持的秒传哈希算法(MD5)
func (d *Pan123) RapidHashAlgos() []utils.HashType {
return []utils.HashType{*utils.MD5}
}
// RapidHashNeedsPieces 123云盘不需要分片哈希
func (d *Yun123) RapidHashNeedsPieces() bool {
// RapidHashNeedsPieces 123 云盘不需要分片哈希
func (d *Pan123) RapidHashNeedsPieces() bool {
return false
}
// RapidUploadByHashes 使用种子中的SHA1或MD5哈希尝试秒传
func (d *Yun123) RapidUploadByHashes(ctx context.Context, dstDir model.Obj, req *driver.SeedRapidUploadRequest, overwrite bool) (model.Obj, error) {
// 优先尝试 SHA1
sha1 := req.Whole.GetHash(utils.SHA1)
if sha1 == "" {
// 降级到 MD5
sha1 = req.Whole.GetHash(utils.MD5)
}
if sha1 == "" {
return nil, driver.ErrUnavailableHash
// RapidUploadByHashes 使用种子中的 MD5(Etag)哈希尝试秒传。
//
// 123 的秒传即「上传请求返回 reuse=true」,无需真正传输内容。
func (d *Pan123) RapidUploadByHashes(ctx context.Context, dstDir model.Obj, req *driver.SeedRapidUploadRequest, overwrite bool) (model.Obj, error) {
etag := req.Whole.GetHash(utils.MD5)
if len(etag) < utils.MD5.Width {
return nil, errs.ErrUnavailableHash
}
// 调用已有的 Put 方法,它会自动处理秒传
return d.Put(ctx, dstDir, &hashOnlyStream{
name: req.Name,
size: req.Size,
hashInfo: utils.NewHashInfo(utils.SHA1, sha1),
}, nil)
}
duplicate := 0
if overwrite {
duplicate = 2
}
data := base.Json{
"driveId": 0,
"duplicate": duplicate,
"etag": strings.ToLower(etag),
"fileName": req.Name,
"parentFileId": dstDir.GetID(),
"size": req.Size,
"type": 0,
}
// hashOnlyStream 仅提供文件元信息和哈希,不提供实际数据流
type hashOnlyStream struct {
name string
size int64
hashInfo utils.HashInfo
}
var resp UploadResp
_, err := d.Request(UploadRequest, http.MethodPost, func(r *resty.Request) {
r.SetBody(data).SetContext(ctx)
}, &resp)
if err != nil {
return nil, err
}
// reuse=true 或未返回上传 Key 均视为秒传成功
if !resp.Data.Reuse && resp.Data.Key != "" {
return nil, errs.ErrHashMismatch
}
func (s *hashOnlyStream) GetName() string { return s.name }
func (s *hashOnlyStream) GetSize() int64 { return s.size }
func (s *hashOnlyStream) Close() error { return nil }
func (s *hashOnlyStream) GetHash() utils.HashInfo { return s.hashInfo }
func (s *hashOnlyStream) GetMimetype() string { return "" }
func (s *hashOnlyStream) NeedStore() bool { return false }
func (s *hashOnlyStream) UseStreamer() bool { return true }
func (s *hashOnlyStream) GetReadCloser() model.ReadCloserProvider { return nil }
return &model.ObjThumb{
Object: model.Object{
ID: strconv.FormatInt(resp.Data.FileId, 10),
Name: req.Name,
Size: req.Size,
Modified: time.Now(),
IsFolder: false,
HashInfo: utils.NewHashInfo(utils.MD5, strings.ToLower(etag)),
},
}, nil
}
+22 -18
View File
@@ -2,49 +2,53 @@ package _123_open
import (
"context"
"io"
"os"
"path"
"strconv"
"time"
"github.com/OpenListTeam/OpenList/v4/internal/driver"
"github.com/OpenListTeam/OpenList/v4/internal/errs"
"github.com/OpenListTeam/OpenList/v4/internal/model"
"github.com/OpenListTeam/OpenList/v4/pkg/utils"
)
// SeedRapidUpload 使用种子的哈希信息进行秒传
func (d *Open123) SeedRapidUpload(ctx context.Context, dstDir model.Obj, fileName string, fileSize int64, hashes utils.HashInfo) (model.Obj, error) {
// 123云盘使用 SHA1 秒传
sha1Hash := hashes.GetHash(utils.SHA1)
// RapidHashAlgos 返回 123 开放平台支持的秒传哈希算法(SHA1)
func (d *Open123) RapidHashAlgos() []utils.HashType {
return []utils.HashType{*utils.SHA1}
}
// RapidHashNeedsPieces 123 开放平台不需要分片哈希
func (d *Open123) RapidHashNeedsPieces() bool {
return false
}
// RapidUploadByHashes 使用种子中的 SHA1 哈希尝试秒传
func (d *Open123) RapidUploadByHashes(ctx context.Context, dstDir model.Obj, req *driver.SeedRapidUploadRequest, overwrite bool) (model.Obj, error) {
sha1Hash := req.Whole.GetHash(utils.SHA1)
if len(sha1Hash) < utils.SHA1.Width {
return nil, errs.EmptyHash
return nil, errs.ErrUnavailableHash
}
// 获取父目录ID
parentID, err := strconv.ParseInt(dstDir.GetID(), 10, 64)
if err != nil {
return nil, err
}
// 调用已有的 sha1Reuse 方法
resp, err := d.sha1Reuse(parentID, fileName, sha1Hash, fileSize, 1)
resp, err := d.sha1Reuse(parentID, req.Name, sha1Hash, req.Size, 1)
if err != nil {
return nil, err
}
if !resp.Reuse {
return nil, errs.HashMismatch
if !resp.Data.Reuse {
return nil, errs.ErrHashMismatch
}
// 返回文件对象
return &model.ObjThumb{
Object: model.Object{
ID: strconv.FormatInt(resp.FileID, 10),
Name: fileName,
Size: fileSize,
ID: strconv.FormatInt(resp.Data.FileID, 10),
Name: req.Name,
Size: req.Size,
IsFolder: false,
Path: path.Join(dstDir.GetPath(), fileName),
Path: path.Join(dstDir.GetPath(), req.Name),
Modified: time.Now(),
},
}, nil
+1 -1
View File
@@ -95,7 +95,7 @@ func ComputeTorrentFromReader(reader io.Reader, fileName string, fileSize int64,
sliceSize = torrent.DefaultPieceSize
}
hw := torrent.NewHashWriter(sliceSize, sliceSize)
hw := torrent.NewHashWriter(sliceSize, sliceSize, fileSize)
buf := make([]byte, 32*1024)
for {
+10 -52
View File
@@ -2,9 +2,6 @@ package _189_tv
import (
"context"
"io"
"os"
"time"
"github.com/OpenListTeam/OpenList/v4/internal/driver"
"github.com/OpenListTeam/OpenList/v4/internal/errs"
@@ -12,66 +9,27 @@ import (
"github.com/OpenListTeam/OpenList/v4/pkg/utils"
)
// RapidHashAlgos 返回189电视支持的秒传哈希算法(MD5)
func (d *Cloud189TV) RapidHashAlgos() []*utils.HashType {
return []*utils.HashType{utils.MD5}
// RapidHashAlgos 返回 189 电视支持的秒传哈希算法(MD5)
func (d *Cloud189TV) RapidHashAlgos() []utils.HashType {
return []utils.HashType{*utils.MD5}
}
// RapidHashNeedsPieces 189电视不需要分片哈希
// RapidHashNeedsPieces 189 电视不需要分片哈希
func (d *Cloud189TV) RapidHashNeedsPieces() bool {
return false
}
// RapidUploadByHashes 使用种子中的MD5哈希尝试秒传
// RapidUploadByHashes 使用种子中的 MD5 哈希尝试秒传
func (d *Cloud189TV) RapidUploadByHashes(ctx context.Context, dstDir model.Obj, req *driver.SeedRapidUploadRequest, overwrite bool) (model.Obj, error) {
md5 := req.Whole.GetHash(utils.MD5)
if md5 == "" {
return nil, driver.ErrUnavailableHash
md5Hash := req.Whole.GetHash(utils.MD5)
if len(md5Hash) < utils.MD5.Width {
return nil, errs.ErrUnavailableHash
}
// 调用已有的 RapidUpload 方法
obj, err := d.RapidUpload(ctx, dstDir.GetPath(), req.Name, req.Size, md5, req.Open)
stream := driver.NewSeedHashStream(req)
obj, err := d.RapidUpload(ctx, dstDir, stream, d.isFamily(), overwrite)
if err != nil {
// 如果秒传失败,可能需要下载文件
if err == errs.RapidUploadFailed {
return nil, driver.ErrUnavailableHash
}
return nil, err
}
return obj, nil
}
// hashOnlyStream 仅提供文件元信息和哈希,不提供实际数据流
type hashOnlyStream struct {
name string
size int64
hashInfo utils.HashInfo
open func() (model.FileStreamer, error)
}
func (s *hashOnlyStream) GetName() string { return s.name }
func (s *hashOnlyStream) GetSize() int64 { return s.size }
func (s *hashOnlyStream) Close() error { return nil }
func (s *hashOnlyStream) GetHash() utils.HashInfo { return s.hashInfo }
func (s *hashOnlyStream) GetMimetype() string { return "" }
func (s *hashOnlyStream) NeedStore() bool { return false }
func (s *hashOnlyStream) UseStreamer() bool { return true }
func (s *hashOnlyStream) GetReadCloser() model.ReadCloserProvider {
if s.open == nil {
return nil
}
return func() (io.ReadCloser, error) {
fs, err := s.open()
if err != nil {
return nil, err
}
rcp := fs.GetReadCloser()
if rcp == nil {
_ = fs.Close()
return nil, os.ErrInvalid
}
return rcp()
}
}
func (s *hashOnlyStream) ModTime() time.Time { return time.Now() }
+20 -32
View File
@@ -2,46 +2,34 @@ package _189pc
import (
"context"
"io"
"os"
"time"
"github.com/OpenListTeam/OpenList/v4/internal/driver"
"github.com/OpenListTeam/OpenList/v4/internal/errs"
"github.com/OpenListTeam/OpenList/v4/internal/model"
"github.com/OpenListTeam/OpenList/v4/pkg/utils"
)
// SeedRapidUpload 使用种子的哈希信息进行秒传
func (d *Cloud189PC) SeedRapidUpload(ctx context.Context, dstDir model.Obj, fileName string, fileSize int64, hashes utils.HashInfo) (model.Obj, error) {
// 189pc 使用 MD5 秒传
md5Hash := hashes.GetHash(utils.MD5)
// RapidHashAlgos 返回 189pc 支持的秒传哈希算法(MD5)
func (d *Cloud189PC) RapidHashAlgos() []utils.HashType {
return []utils.HashType{*utils.MD5}
}
// RapidHashNeedsPieces 189pc 的 CAS 秒传依赖分片 MD5
func (d *Cloud189PC) RapidHashNeedsPieces() bool {
return true
}
// RapidUploadByHashes 使用种子中的 MD5 哈希尝试秒传
func (d *Cloud189PC) RapidUploadByHashes(ctx context.Context, dstDir model.Obj, req *driver.SeedRapidUploadRequest, overwrite bool) (model.Obj, error) {
md5Hash := req.Whole.GetHash(utils.MD5)
if len(md5Hash) < utils.MD5.Width {
return nil, errs.EmptyHash
return nil, errs.ErrUnavailableHash
}
// 使用已有的 RapidUpload 方法,传入构造的 FileStream
stream := &hashOnlyStream{
name: fileName,
size: fileSize,
hashInfo: hashes,
stream := driver.NewSeedHashStream(req)
obj, err := d.RapidUpload(ctx, dstDir, stream, d.isFamily(), overwrite)
if err != nil {
return nil, err
}
return d.RapidUpload(ctx, dstDir, stream, d.FamilyID != 0, false)
return obj, nil
}
// hashOnlyStream 仅包含哈希信息的 FileStream
type hashOnlyStream struct {
name string
size int64
hashInfo utils.HashInfo
}
func (s *hashOnlyStream) GetName() string { return s.name }
func (s *hashOnlyStream) GetSize() int64 { return s.size }
func (s *hashOnlyStream) GetHash() utils.HashInfo { return s.hashInfo }
func (s *hashOnlyStream) Read(p []byte) (n int, err error) { return 0, io.EOF }
func (s *hashOnlyStream) Close() error { return nil }
func (s *hashOnlyStream) GetMimetype() string { return "" }
func (s *hashOnlyStream) ModTime() time.Time { return time.Now() }
func (s *hashOnlyStream) CreateTime() time.Time { return time.Now() }
func (s *hashOnlyStream) GetFile() *os.File { return nil }
+1 -1
View File
@@ -122,7 +122,7 @@ func ComputeTorrentFromReader(reader io.Reader, fileName string, fileSize int64,
sliceSize = torrent.DefaultPieceSize
}
hw := torrent.NewHashWriter(sliceSize, sliceSize)
hw := torrent.NewHashWriter(sliceSize, sliceSize, fileSize)
buf := make([]byte, 32*1024)
for {
+96
View File
@@ -1329,6 +1329,102 @@ func (y *Cloud189PC) OldUploadCommit(ctx context.Context, fileCommitUrl string,
return resp.toFile(), nil
}
// rapidUploadByCAS 使用 MD5 + 分片 MD5(CAS)执行天翼云盘秒传。
//
// 流程与 Web 端一致:
// 1. initMultiUpload(仅传 lazyCheck=1)
// 2. checkTransSecond(用 fileMd5 + sliceMd5 检查云端是否已存在文件数据)
// 3. commitMultiUploadFile(提交并返回文件对象)
func (y *Cloud189PC) rapidUploadByCAS(ctx context.Context, dstDir model.Obj, fileName string, fileSize int64, fileMD5 string, sliceMD5s []string, sliceSize int64, overwrite bool) (model.Obj, error) {
isFamily := y.isFamily()
// 统一 MD5 为大写(天翼云盘要求大写)
fileMD5Upper := strings.ToUpper(fileMD5)
// 优先使用传入的分片大小,否则按文件大小推导
if sliceSize <= 0 {
sliceSize = partSize(fileSize)
}
// 计算 sliceMd5(与上传时一致的算法)
sliceMd5Hex := fileMD5Upper
if len(sliceMD5s) > 1 {
upperSliceMD5s := make([]string, len(sliceMD5s))
for i, s := range sliceMD5s {
upperSliceMD5s[i] = strings.ToUpper(s)
}
sliceMd5Hex = strings.ToUpper(utils.GetMD5EncodeStr(strings.Join(upperSliceMD5s, "\n")))
} else if len(sliceMD5s) == 1 {
sliceMd5Hex = strings.ToUpper(sliceMD5s[0])
}
fullUrl := "https://upload.cloud.189.cn"
if isFamily {
fullUrl += "/family"
} else {
fullUrl += "/person"
}
// Step 1: initMultiUpload(不传 fileMd5/sliceMd5,只传 lazyCheck)
initParams := Params{
"parentFolderId": dstDir.GetID(),
"fileName": url.QueryEscape(fileName),
"fileSize": fmt.Sprint(fileSize),
"sliceSize": fmt.Sprint(sliceSize),
"lazyCheck": "1",
}
if isFamily {
initParams.Set("familyId", y.FamilyID)
}
var uploadInfo InitMultiUploadResp
if _, err := y.request(fullUrl+"/initMultiUpload", "GET", func(req *resty.Request) {
req.SetContext(ctx)
}, initParams, &uploadInfo, isFamily); err != nil {
return nil, fmt.Errorf("initMultiUpload 失败: %w", err)
}
uploadFileId := uploadInfo.Data.UploadFileID
// Step 2: checkTransSecond(用 fileMd5 + sliceMd5 + uploadFileId 检查秒传)
checkParams := Params{
"fileMd5": fileMD5Upper,
"sliceMd5": sliceMd5Hex,
"uploadFileId": uploadFileId,
}
var checkResp struct {
Data struct {
FileDataExists int `json:"fileDataExists"`
} `json:"data"`
}
if _, err := y.request(fullUrl+"/checkTransSecond", "GET", func(req *resty.Request) {
req.SetContext(ctx)
}, checkParams, &checkResp, isFamily); err != nil {
return nil, fmt.Errorf("秒传检查失败: %w", err)
}
if checkResp.Data.FileDataExists != 1 {
return nil, fmt.Errorf("秒传失败:云端不存在该文件(fileMD5=%s, sliceMD5=%s, size=%d)", fileMD5Upper, sliceMd5Hex, fileSize)
}
// Step 3: commitMultiUploadFile(传 fileMd5 + sliceMd5)
commitParams := Params{
"uploadFileId": uploadFileId,
"fileMd5": fileMD5Upper,
"sliceMd5": sliceMd5Hex,
"lazyCheck": "1",
"opertype": IF(overwrite, "3", "1"),
}
var resp CommitMultiUploadFileResp
if _, err := y.request(fullUrl+"/commitMultiUploadFile", "GET", func(req *resty.Request) {
req.SetContext(ctx)
}, commitParams, &resp, isFamily); err != nil {
return nil, fmt.Errorf("提交上传失败: %w", err)
}
return resp.toFile(), nil
}
func (y *Cloud189PC) isFamily() bool {
return y.Type == "family"
}
+60 -45
View File
@@ -2,72 +2,87 @@ package aliyundrive_open
import (
"context"
"fmt"
"io"
"net/http"
"os"
"time"
"github.com/OpenListTeam/OpenList/v4/drivers/base"
"github.com/OpenListTeam/OpenList/v4/internal/driver"
"github.com/OpenListTeam/OpenList/v4/internal/errs"
"github.com/OpenListTeam/OpenList/v4/internal/model"
"github.com/OpenListTeam/OpenList/v4/pkg/utils"
"github.com/go-resty/resty/v2"
)
// SeedRapidUpload 使用种子的哈希信息进行秒传
func (d *AliyundriveOpen) SeedRapidUpload(ctx context.Context, dstDir model.Obj, fileName string, fileSize int64, hashes utils.HashInfo) (model.Obj, error) {
// 阿里云盘使用 SHA1 秒传
sha1Hash := hashes.GetHash(utils.SHA1)
// RapidHashAlgos 返回阿里云盘支持的秒传哈希算法(SHA1)
func (d *AliyundriveOpen) RapidHashAlgos() []utils.HashType {
return []utils.HashType{*utils.SHA1}
}
// RapidHashNeedsPieces 阿里云盘不需要分片哈希
func (d *AliyundriveOpen) RapidHashNeedsPieces() bool {
return false
}
// RapidUploadByHashes 使用种子中的 SHA1 哈希尝试秒传。
//
// 阿里云盘的秒传还需要 proof_code(按 proof range 读取的一段内容),
// 因此当内容源不可用时无法完成秒传。
func (d *AliyundriveOpen) RapidUploadByHashes(ctx context.Context, dstDir model.Obj, req *driver.SeedRapidUploadRequest, overwrite bool) (model.Obj, error) {
sha1Hash := req.Whole.GetHash(utils.SHA1)
if len(sha1Hash) < utils.SHA1.Width {
return nil, errs.EmptyHash
return nil, errs.ErrUnavailableHash
}
if req.Open == nil {
return nil, errs.ErrUnavailableHash
}
stream, err := req.Open()
if err != nil {
return nil, err
}
defer stream.Close()
proofCode, err := d.calProofCode(stream)
if err != nil {
return nil, err
}
// 调用创建文件接口,尝试秒传
var resp CreateResp
_, err := d.request(ctx, limiterOther, "/adrive/v1.0/openFile/create", http.MethodPost, func(req *resty.Request) {
req.SetBody(base.Json{
"drive_id": d.DriveId,
"parent_file_id": dstDir.GetID(),
"name": fileName,
"type": "file",
"check_name_mode": "auto_rename",
"size": fileSize,
"content_hash": sha1Hash,
_, err = d.request(ctx, limiterOther, "/adrive/v1.0/openFile/create", http.MethodPost, func(r *resty.Request) {
r.SetBody(base.Json{
"drive_id": d.DriveId,
"parent_file_id": dstDir.GetID(),
"name": req.Name,
"type": "file",
"check_name_mode": "auto_rename",
"size": req.Size,
"content_hash": sha1Hash,
"content_hash_name": "sha1",
"proof_version": "v1",
"proof_version": "v1",
"proof_code": proofCode,
}).SetResult(&resp)
})
if err != nil {
return nil, err
}
// 如果没有返回 UploadId,说明秒传成功
if resp.UploadId == "" {
if resp.RapidUpload {
return fileToObj(resp.File), nil
}
// 文件已存在但不是秒传
return fileToObj(resp.File), nil
if !resp.RapidUpload {
return nil, errs.ErrHashMismatch
}
// 秒传失败,需要分片上传
return nil, errs.HashMismatch
}
if resp.FileId != "" {
obj, err := d.completeUpload(ctx, resp.FileId, resp.UploadId)
if err != nil {
return nil, err
}
return obj, nil
}
// hashOnlyStream 仅包含哈希信息的 FileStream
type hashOnlyStream struct {
name string
size int64
hashInfo utils.HashInfo
return &model.ObjThumb{
Object: model.Object{
Name: req.Name,
Size: req.Size,
Modified: time.Now(),
IsFolder: false,
},
}, nil
}
func (s *hashOnlyStream) GetName() string { return s.name }
func (s *hashOnlyStream) GetSize() int64 { return s.size }
func (s *hashOnlyStream) GetHash() utils.HashInfo { return s.hashInfo }
func (s *hashOnlyStream) Read(p []byte) (n int, err error) { return 0, io.EOF }
func (s *hashOnlyStream) Close() error { return nil }
func (s *hashOnlyStream) GetMimetype() string { return "" }
func (s *hashOnlyStream) ModTime() time.Time { return time.Now() }
func (s *hashOnlyStream) CreateTime() time.Time { return time.Now() }
func (s *hashOnlyStream) GetFile() *os.File { return nil }
+20 -32
View File
@@ -2,46 +2,34 @@ package baidu_netdisk
import (
"context"
"io"
"os"
"time"
"github.com/OpenListTeam/OpenList/v4/internal/driver"
"github.com/OpenListTeam/OpenList/v4/internal/errs"
"github.com/OpenListTeam/OpenList/v4/internal/model"
"github.com/OpenListTeam/OpenList/v4/pkg/utils"
)
// SeedRapidUpload 使用种子的哈希信息进行秒传
func (d *BaiduNetdisk) SeedRapidUpload(ctx context.Context, dstDir model.Obj, fileName string, fileSize int64, hashes utils.HashInfo) (model.Obj, error) {
// 百度网盘使用 MD5 秒传
md5Hash := hashes.GetHash(utils.MD5)
// RapidHashAlgos 返回百度网盘支持的秒传哈希算法(MD5)
func (d *BaiduNetdisk) RapidHashAlgos() []utils.HashType {
return []utils.HashType{*utils.MD5}
}
// RapidHashNeedsPieces 百度网盘不需要分片哈希
func (d *BaiduNetdisk) RapidHashNeedsPieces() bool {
return false
}
// RapidUploadByHashes 使用种子中的 MD5 哈希尝试秒传
func (d *BaiduNetdisk) RapidUploadByHashes(ctx context.Context, dstDir model.Obj, req *driver.SeedRapidUploadRequest, overwrite bool) (model.Obj, error) {
md5Hash := req.Whole.GetHash(utils.MD5)
if len(md5Hash) < utils.MD5.Width {
return nil, errs.EmptyHash
return nil, errs.ErrUnavailableHash
}
// 使用已有的 PutRapid 方法
stream := &hashOnlyStream{
name: fileName,
size: fileSize,
hashInfo: hashes,
stream := driver.NewSeedHashStream(req)
obj, err := d.PutRapid(ctx, dstDir, stream)
if err != nil {
return nil, err
}
return d.PutRapid(ctx, dstDir, stream)
return obj, nil
}
// hashOnlyStream 仅包含哈希信息的 FileStream
type hashOnlyStream struct {
name string
size int64
hashInfo utils.HashInfo
}
func (s *hashOnlyStream) GetName() string { return s.name }
func (s *hashOnlyStream) GetSize() int64 { return s.size }
func (s *hashOnlyStream) GetHash() utils.HashInfo { return s.hashInfo }
func (s *hashOnlyStream) Read(p []byte) (n int, err error) { return 0, io.EOF }
func (s *hashOnlyStream) Close() error { return nil }
func (s *hashOnlyStream) GetMimetype() string { return "" }
func (s *hashOnlyStream) ModTime() time.Time { return time.Now() }
func (s *hashOnlyStream) CreateTime() time.Time { return time.Now() }
func (s *hashOnlyStream) GetFile() *os.File { return nil }
+22 -34
View File
@@ -2,34 +2,41 @@ package pikpak
import (
"context"
"io"
"net/http"
"os"
"strings"
"time"
"github.com/OpenListTeam/OpenList/v4/drivers/base"
"github.com/OpenListTeam/OpenList/v4/internal/driver"
"github.com/OpenListTeam/OpenList/v4/internal/errs"
"github.com/OpenListTeam/OpenList/v4/internal/model"
"github.com/OpenListTeam/OpenList/v4/pkg/hash_extend"
"github.com/OpenListTeam/OpenList/v4/pkg/utils"
hash_extend "github.com/OpenListTeam/OpenList/v4/pkg/utils/hash"
"github.com/go-resty/resty/v2"
)
// SeedRapidUpload 使用种子的哈希信息进行秒传
func (d *PikPak) SeedRapidUpload(ctx context.Context, dstDir model.Obj, fileName string, fileSize int64, hashes utils.HashInfo) (model.Obj, error) {
// PikPak 使用 GCID 秒传
gcid := hashes.GetHash(hash_extend.GCID)
// RapidHashAlgos 返回 PikPak 支持的秒传哈希算法(GCID)
func (d *PikPak) RapidHashAlgos() []utils.HashType {
return []utils.HashType{*hash_extend.GCID}
}
// RapidHashNeedsPieces PikPak 不需要分片哈希
func (d *PikPak) RapidHashNeedsPieces() bool {
return false
}
// RapidUploadByHashes 使用种子中的 GCID 哈希尝试秒传
func (d *PikPak) RapidUploadByHashes(ctx context.Context, dstDir model.Obj, req *driver.SeedRapidUploadRequest, overwrite bool) (model.Obj, error) {
gcid := req.Whole.GetHash(hash_extend.GCID)
if len(gcid) < hash_extend.GCID.Width {
return nil, errs.EmptyHash
return nil, errs.ErrUnavailableHash
}
var resp UploadTaskData
res, err := d.request("https://api-drive.mypikpak.net/drive/v1/files", http.MethodPost, func(req *resty.Request) {
req.SetBody(base.Json{
_, err := d.request("https://api-drive.mypikpak.net/drive/v1/files", http.MethodPost, func(r *resty.Request) {
r.SetContext(ctx).SetBody(base.Json{
"kind": "drive#file",
"name": fileName,
"size": fileSize,
"name": req.Name,
"size": req.Size,
"hash": strings.ToUpper(gcid),
"upload_type": "UPLOAD_TYPE_RESUMABLE",
"objProvider": base.Json{"provider": "UPLOAD_TYPE_UNKNOWN"},
@@ -41,30 +48,11 @@ func (d *PikPak) SeedRapidUpload(ctx context.Context, dstDir model.Obj, fileName
return nil, err
}
// 秒传成功(没有返回 Resumable)
// 秒传成功时不会返回 Resumable
if resp.Resumable == nil {
// 解析返回的文件信息
file := fileToObj(resp.File)
return file, nil
}
// 秒传失败
return nil, errs.HashMismatch
return nil, errs.ErrHashMismatch
}
// hashOnlyStream 仅包含哈希信息的 FileStream
type hashOnlyStream struct {
name string
size int64
hashInfo utils.HashInfo
}
func (s *hashOnlyStream) GetName() string { return s.name }
func (s *hashOnlyStream) GetSize() int64 { return s.size }
func (s *hashOnlyStream) GetHash() utils.HashInfo { return s.hashInfo }
func (s *hashOnlyStream) Read(p []byte) (n int, err error) { return 0, io.EOF }
func (s *hashOnlyStream) Close() error { return nil }
func (s *hashOnlyStream) GetMimetype() string { return "" }
func (s *hashOnlyStream) ModTime() time.Time { return time.Now() }
func (s *hashOnlyStream) CreateTime() time.Time { return time.Now() }
func (s *hashOnlyStream) GetFile() *os.File { return nil }
+39 -46
View File
@@ -2,66 +2,59 @@ package quark_open
import (
"context"
"io"
"os"
"time"
"github.com/OpenListTeam/OpenList/v4/internal/driver"
"github.com/OpenListTeam/OpenList/v4/internal/errs"
"github.com/OpenListTeam/OpenList/v4/internal/model"
"github.com/OpenListTeam/OpenList/v4/pkg/utils"
)
// SeedRapidUpload 使用种子的哈希信息进行秒传
func (d *QuarkOpen) SeedRapidUpload(ctx context.Context, dstDir model.Obj, fileName string, fileSize int64, hashes utils.HashInfo) (model.Obj, error) {
// 夸克网盘使用 MD5 + SHA1 秒传
md5Hash := hashes.GetHash(utils.MD5)
sha1Hash := hashes.GetHash(utils.SHA1)
// RapidHashAlgos 返回夸克网盘支持的秒传哈希算法(MD5 + SHA1)
func (d *QuarkOpen) RapidHashAlgos() []utils.HashType {
return []utils.HashType{*utils.MD5, *utils.SHA1}
}
// RapidHashNeedsPieces 夸克网盘不需要分片哈希
func (d *QuarkOpen) RapidHashNeedsPieces() bool {
return false
}
// RapidUploadByHashes 使用种子中的 MD5/SHA1 哈希尝试秒传。
//
// 夸克网盘的预上传需要 proof_code(按 proof range 读取的一段内容),
// 因此当内容源不可用时无法完成秒传。
func (d *QuarkOpen) RapidUploadByHashes(ctx context.Context, dstDir model.Obj, req *driver.SeedRapidUploadRequest, overwrite bool) (model.Obj, error) {
md5Hash := req.Whole.GetHash(utils.MD5)
sha1Hash := req.Whole.GetHash(utils.SHA1)
if len(md5Hash) < utils.MD5.Width || len(sha1Hash) < utils.SHA1.Width {
return nil, errs.EmptyHash
return nil, errs.ErrUnavailableHash
}
if req.Open == nil {
return nil, errs.ErrUnavailableHash
}
// 调用预上传接口
pre, err := d.upPre(ctx, &hashOnlyStream{
name: fileName,
size: fileSize,
hashInfo: hashes,
}, dstDir.GetID(), md5Hash, sha1Hash)
stream, err := req.Open()
if err != nil {
return nil, err
}
defer stream.Close()
// 如果预上传已经完成,说明秒传成功
if pre.Data.Finish {
// 返回文件对象
return &model.ObjThumb{
Object: model.Object{
ID: pre.Data.FID,
Name: fileName,
Size: fileSize,
IsFolder: false,
Modified: time.Now(),
},
}, nil
pre, err := d.upPre(ctx, stream, dstDir.GetID(), md5Hash, sha1Hash)
if err != nil {
return nil, err
}
if !pre.Data.Finish {
return nil, errs.ErrHashMismatch
}
// 秒传失败
return nil, errs.HashMismatch
return &model.ObjThumb{
Object: model.Object{
ID: pre.Data.Fid,
Name: req.Name,
Size: req.Size,
IsFolder: false,
Modified: time.Now(),
},
}, nil
}
// hashOnlyStream 仅包含哈希信息的 FileStream
type hashOnlyStream struct {
name string
size int64
hashInfo utils.HashInfo
}
func (s *hashOnlyStream) GetName() string { return s.name }
func (s *hashOnlyStream) GetSize() int64 { return s.size }
func (s *hashOnlyStream) GetHash() utils.HashInfo { return s.hashInfo }
func (s *hashOnlyStream) Read(p []byte) (n int, err error) { return 0, io.EOF }
func (s *hashOnlyStream) Close() error { return nil }
func (s *hashOnlyStream) GetMimetype() string { return "" }
func (s *hashOnlyStream) ModTime() time.Time { return time.Now() }
func (s *hashOnlyStream) CreateTime() time.Time { return time.Now() }
func (s *hashOnlyStream) GetFile() *os.File { return nil }
+20 -33
View File
@@ -2,25 +2,32 @@ package thunder
import (
"context"
"io"
"net/http"
"os"
"time"
"github.com/OpenListTeam/OpenList/v4/drivers/base"
"github.com/OpenListTeam/OpenList/v4/internal/driver"
"github.com/OpenListTeam/OpenList/v4/internal/errs"
"github.com/OpenListTeam/OpenList/v4/internal/model"
"github.com/OpenListTeam/OpenList/v4/pkg/hash_extend"
"github.com/OpenListTeam/OpenList/v4/pkg/utils"
hash_extend "github.com/OpenListTeam/OpenList/v4/pkg/utils/hash"
"github.com/go-resty/resty/v2"
)
// SeedRapidUpload 使用种子的哈希信息进行秒传
func (xc *XunLeiCommon) SeedRapidUpload(ctx context.Context, dstDir model.Obj, fileName string, fileSize int64, hashes utils.HashInfo) (model.Obj, error) {
// 迅雷使用 GCID 秒传
gcid := hashes.GetHash(hash_extend.GCID)
// RapidHashAlgos 返回迅雷支持的秒传哈希算法(GCID)
func (xc *XunLeiCommon) RapidHashAlgos() []utils.HashType {
return []utils.HashType{*hash_extend.GCID}
}
// RapidHashNeedsPieces 迅雷不需要分片哈希
func (xc *XunLeiCommon) RapidHashNeedsPieces() bool {
return false
}
// RapidUploadByHashes 使用种子中的 GCID 哈希尝试秒传
func (xc *XunLeiCommon) RapidUploadByHashes(ctx context.Context, dstDir model.Obj, req *driver.SeedRapidUploadRequest, overwrite bool) (model.Obj, error) {
gcid := req.Whole.GetHash(hash_extend.GCID)
if len(gcid) < hash_extend.GCID.Width {
return nil, errs.EmptyHash
return nil, errs.ErrUnavailableHash
}
var resp UploadTaskResponse
@@ -29,8 +36,8 @@ func (xc *XunLeiCommon) SeedRapidUpload(ctx context.Context, dstDir model.Obj, f
r.SetBody(&base.Json{
"kind": FILE,
"parent_id": dstDir.GetID(),
"name": fileName,
"size": fileSize,
"name": req.Name,
"size": req.Size,
"hash": gcid,
"upload_type": UPLOAD_TYPE_RESUMABLE,
"space": xc.Space,
@@ -42,28 +49,8 @@ func (xc *XunLeiCommon) SeedRapidUpload(ctx context.Context, dstDir model.Obj, f
// 秒传成功(UploadType != UPLOAD_TYPE_RESUMABLE)
if resp.UploadType != UPLOAD_TYPE_RESUMABLE {
// 解析返回的文件信息
file := fileToObj(resp.File)
return file, nil
return &resp.File, nil
}
// 秒传失败
return nil, errs.HashMismatch
return nil, errs.ErrHashMismatch
}
// hashOnlyStream 仅包含哈希信息的 FileStream
type hashOnlyStream struct {
name string
size int64
hashInfo utils.HashInfo
}
func (s *hashOnlyStream) GetName() string { return s.name }
func (s *hashOnlyStream) GetSize() int64 { return s.size }
func (s *hashOnlyStream) GetHash() utils.HashInfo { return s.hashInfo }
func (s *hashOnlyStream) Read(p []byte) (n int, err error) { return 0, io.EOF }
func (s *hashOnlyStream) Close() error { return nil }
func (s *hashOnlyStream) GetMimetype() string { return "" }
func (s *hashOnlyStream) ModTime() time.Time { return time.Now() }
func (s *hashOnlyStream) CreateTime() time.Time { return time.Now() }
func (s *hashOnlyStream) GetFile() *os.File { return nil }
+32 -28
View File
@@ -2,50 +2,54 @@ package thunder_browser
import (
"context"
"net/http"
"github.com/OpenListTeam/OpenList/v4/drivers/base"
"github.com/OpenListTeam/OpenList/v4/internal/driver"
"github.com/OpenListTeam/OpenList/v4/internal/errs"
"github.com/OpenListTeam/OpenList/v4/internal/model"
"github.com/OpenListTeam/OpenList/v4/pkg/utils"
hash_extend "github.com/OpenListTeam/OpenList/v4/pkg/utils/hash"
"github.com/go-resty/resty/v2"
)
// RapidHashAlgos 返回迅雷浏览器支持的秒传哈希算法(GCID)
func (d *ThunderBrowser) RapidHashAlgos() []*utils.HashType {
return []*utils.HashType{hash_extend.GCID}
func (xc *XunLeiBrowserCommon) RapidHashAlgos() []utils.HashType {
return []utils.HashType{*hash_extend.GCID}
}
// RapidHashNeedsPieces 迅雷浏览器不需要分片哈希
func (d *ThunderBrowser) RapidHashNeedsPieces() bool {
func (xc *XunLeiBrowserCommon) RapidHashNeedsPieces() bool {
return false
}
// RapidUploadByHashes 使用种子中的GCID哈希尝试秒传
func (d *ThunderBrowser) RapidUploadByHashes(ctx context.Context, dstDir model.Obj, req *driver.SeedRapidUploadRequest, overwrite bool) (model.Obj, error) {
// RapidUploadByHashes 使用种子中的 GCID 哈希尝试秒传
func (xc *XunLeiBrowserCommon) RapidUploadByHashes(ctx context.Context, dstDir model.Obj, req *driver.SeedRapidUploadRequest, overwrite bool) (model.Obj, error) {
gcid := req.Whole.GetHash(hash_extend.GCID)
if gcid == "" {
return nil, driver.ErrUnavailableHash
if len(gcid) < hash_extend.GCID.Width {
return nil, errs.ErrUnavailableHash
}
// 调用已有的 Put 方法,它会自动处理 GCID 秒传
return d.Put(ctx, dstDir, &hashOnlyStream{
name: req.Name,
size: req.Size,
hashInfo: utils.NewHashInfo(hash_extend.GCID, gcid),
}, nil)
}
var resp UploadTaskResponse
_, err := xc.Request(FILE_API_URL, http.MethodPost, func(r *resty.Request) {
r.SetContext(ctx)
r.SetBody(&base.Json{
"kind": FILE,
"parent_id": dstDir.GetID(),
"name": req.Name,
"size": req.Size,
"hash": gcid,
"upload_type": UPLOAD_TYPE_RESUMABLE,
})
}, &resp)
if err != nil {
return nil, err
}
// hashOnlyStream 仅提供文件元信息和哈希,不提供实际数据流
type hashOnlyStream struct {
name string
size int64
hashInfo utils.HashInfo
}
// 秒传成功(UploadType != UPLOAD_TYPE_RESUMABLE)
if resp.UploadType != UPLOAD_TYPE_RESUMABLE {
return &resp.File, nil
}
func (s *hashOnlyStream) GetName() string { return s.name }
func (s *hashOnlyStream) GetSize() int64 { return s.size }
func (s *hashOnlyStream) Close() error { return nil }
func (s *hashOnlyStream) GetHash() utils.HashInfo { return s.hashInfo }
func (s *hashOnlyStream) GetMimetype() string { return "" }
func (s *hashOnlyStream) NeedStore() bool { return false }
func (s *hashOnlyStream) UseStreamer() bool { return true }
func (s *hashOnlyStream) GetReadCloser() model.ReadCloserProvider { return nil }
return nil, errs.ErrHashMismatch
}
+32 -29
View File
@@ -2,51 +2,54 @@ package thunderx
import (
"context"
"net/http"
"github.com/OpenListTeam/OpenList/v4/drivers/base"
"github.com/OpenListTeam/OpenList/v4/internal/driver"
"github.com/OpenListTeam/OpenList/v4/internal/errs"
"github.com/OpenListTeam/OpenList/v4/internal/model"
"github.com/OpenListTeam/OpenList/v4/pkg/utils"
hash_extend "github.com/OpenListTeam/OpenList/v4/pkg/utils/hash"
"github.com/go-resty/resty/v2"
)
// RapidHashAlgos 返回迅雷X支持的秒传哈希算法(GCID)
func (d *ThunderX) RapidHashAlgos() []*utils.HashType {
return []*utils.HashType{hash_extend.GCID}
func (xc *XunLeiXCommon) RapidHashAlgos() []utils.HashType {
return []utils.HashType{*hash_extend.GCID}
}
// RapidHashNeedsPieces 迅雷X不需要分片哈希
func (d *ThunderX) RapidHashNeedsPieces() bool {
func (xc *XunLeiXCommon) RapidHashNeedsPieces() bool {
return false
}
// RapidUploadByHashes 使用种子中的GCID哈希尝试秒传
func (d *ThunderX) RapidUploadByHashes(ctx context.Context, dstDir model.Obj, req *driver.SeedRapidUploadRequest, overwrite bool) (model.Obj, error) {
// RapidUploadByHashes 使用种子中的 GCID 哈希尝试秒传
func (xc *XunLeiXCommon) RapidUploadByHashes(ctx context.Context, dstDir model.Obj, req *driver.SeedRapidUploadRequest, overwrite bool) (model.Obj, error) {
gcid := req.Whole.GetHash(hash_extend.GCID)
if gcid == "" {
return nil, driver.ErrUnavailableHash
if len(gcid) < hash_extend.GCID.Width {
return nil, errs.ErrUnavailableHash
}
// 调用已有的 Put 方法,它会自动处理 GCID 秒传
// ThunderX 的 Put 实现与 Thunder 类似,通过 GCID 秒传
return d.Put(ctx, dstDir, &hashOnlyStream{
name: req.Name,
size: req.Size,
hashInfo: utils.NewHashInfo(hash_extend.GCID, gcid),
}, nil)
}
var resp UploadTaskResponse
_, err := xc.Request(FILE_API_URL, http.MethodPost, func(r *resty.Request) {
r.SetContext(ctx)
r.SetBody(&base.Json{
"kind": FILE,
"parent_id": dstDir.GetID(),
"name": req.Name,
"size": req.Size,
"hash": gcid,
"upload_type": UPLOAD_TYPE_RESUMABLE,
})
}, &resp)
if err != nil {
return nil, err
}
// hashOnlyStream 仅提供文件元信息和哈希,不提供实际数据流
type hashOnlyStream struct {
name string
size int64
hashInfo utils.HashInfo
}
// 秒传成功(UploadType != UPLOAD_TYPE_RESUMABLE)
if resp.UploadType != UPLOAD_TYPE_RESUMABLE {
return &resp.File, nil
}
func (s *hashOnlyStream) GetName() string { return s.name }
func (s *hashOnlyStream) GetSize() int64 { return s.size }
func (s *hashOnlyStream) Close() error { return nil }
func (s *hashOnlyStream) GetHash() utils.HashInfo { return s.hashInfo }
func (s *hashOnlyStream) GetMimetype() string { return "" }
func (s *hashOnlyStream) NeedStore() bool { return false }
func (s *hashOnlyStream) UseStreamer() bool { return true }
func (s *hashOnlyStream) GetReadCloser() model.ReadCloserProvider { return nil }
return nil, errs.ErrHashMismatch
}
+72
View File
@@ -0,0 +1,72 @@
package driver
import (
"io"
"time"
"github.com/OpenListTeam/OpenList/v4/internal/model"
"github.com/OpenListTeam/OpenList/v4/pkg/http_range"
"github.com/OpenListTeam/OpenList/v4/pkg/utils"
)
// SeedHashStream is a model.FileStreamer that only carries file metadata and
// precomputed hashes; it never yields real content.
//
// It exists so that hash-driven rapid upload (秒传/CAS) implementations can reuse
// the drivers' existing Put/RapidUpload code paths, which expect a
// model.FileStreamer but only read the name/size/hash. Drivers that additionally
// need the real content (e.g. to compute a leading proof hash) should supply
// Source, which is exposed via GetReadCloser-like lazy opening.
type SeedHashStream struct {
name string
size int64
hashInfo utils.HashInfo
// Source lazily opens the underlying content streamer. May be nil.
Source func() (model.FileStreamer, error)
}
var (
_ model.FileStreamer = (*SeedHashStream)(nil)
_ utils.ClosersIF = (*SeedHashStream)(nil)
)
// NewSeedHashStream builds a hash-only streamer from a rapid-upload request.
func NewSeedHashStream(req *SeedRapidUploadRequest) *SeedHashStream {
s := &SeedHashStream{name: req.Name, size: req.Size, Source: req.Open}
if req.Whole != nil {
s.hashInfo = *req.Whole
}
return s
}
func (s *SeedHashStream) GetName() string { return s.name }
func (s *SeedHashStream) GetSize() int64 { return s.size }
func (s *SeedHashStream) GetHash() utils.HashInfo { return s.hashInfo }
func (s *SeedHashStream) GetMimetype() string { return "" }
func (s *SeedHashStream) ModTime() time.Time { return time.Now() }
func (s *SeedHashStream) CreateTime() time.Time { return time.Now() }
func (s *SeedHashStream) IsDir() bool { return false }
func (s *SeedHashStream) GetID() string { return "" }
func (s *SeedHashStream) GetPath() string { return "" }
func (s *SeedHashStream) NeedStore() bool { return false }
func (s *SeedHashStream) IsForceStreamUpload() bool { return true }
func (s *SeedHashStream) GetExist() model.Obj { return nil }
func (s *SeedHashStream) SetExist(model.Obj) {}
func (s *SeedHashStream) GetFile() model.File { return nil }
func (s *SeedHashStream) Add(io.Closer) {}
func (s *SeedHashStream) AddIfCloser(any) {}
func (s *SeedHashStream) Close() error { return nil }
// Read returns EOF: the stream carries hashes only, no content.
func (s *SeedHashStream) Read([]byte) (int, error) { return 0, io.EOF }
// RangeRead returns an empty reader, since no content is available.
func (s *SeedHashStream) RangeRead(http_range.Range) (io.Reader, error) {
return nil, io.EOF
}
// CacheFullAndWriter reports that the content cannot be materialized.
func (s *SeedHashStream) CacheFullAndWriter(*model.UpdateProgress, io.Writer) (model.File, error) {
return nil, io.EOF
}
+12
View File
@@ -4,4 +4,16 @@ import "errors"
var (
EmptyToken = errors.New("empty token")
// ErrUnavailableHash indicates the seed does not carry the hash algorithm
// required by the destination driver, so rapid upload cannot be attempted.
ErrUnavailableHash = errors.New("required hash is unavailable")
// ErrEmptyHash indicates a required hash exists but is empty/too short.
ErrEmptyHash = errors.New("empty hash")
// ErrHashMismatch indicates the remote side rejected the provided hash, so
// a full content transfer is required instead of a rapid upload.
ErrHashMismatch = errors.New("hash mismatch")
// ErrRapidUploadFailed indicates the driver attempted a rapid upload but
// could not confirm success.
ErrRapidUploadFailed = errors.New("rapid upload failed")
)
+2 -2
View File
@@ -367,7 +367,7 @@ func GenerateSeedArtifacts(ctx context.Context, user *model.User, params SeedGen
if (hasShare || hasDirect) && strings.TrimSpace(setting.GetStr(conf.SeedSiteURL)) == "" {
return nil, nil, fmt.Errorf("seed_site_url must be configured before embedding download sources")
}
globalHasher := torrent.NewHashWriter(pieceSize, pieceSize)
globalHasher := torrent.NewHashWriter(pieceSize, pieceSize, 0)
fullPaths := make([]string, 0, len(params.Paths))
var total int64
for _, requestedPath := range params.Paths {
@@ -439,7 +439,7 @@ func GenerateSeedArtifacts(ctx context.Context, user *model.User, params SeedGen
if err != nil {
return nil, nil, err
}
fileHasher := torrent.NewHashWriter(pieceSize, pieceSize)
fileHasher := torrent.NewHashWriter(pieceSize, pieceSize, 0)
n, copyErr := io.Copy(io.MultiWriter(globalHasher, fileHasher), rc)
_ = rc.Close()
if copyErr != nil {
+1 -1
View File
@@ -132,7 +132,7 @@ func GenerateSeedFromReader(reader io.Reader, filePath string, expectedSize, pie
if err := validateRelativeSeedPath(filePath); err != nil {
return nil, err
}
hw := NewHashWriter(pieceSize, pieceSize)
hw := NewHashWriter(pieceSize, pieceSize, expectedSize)
if _, err := CopyAndHash(nil, reader, hw); err != nil {
return nil, err
}
+1 -1
View File
@@ -69,7 +69,7 @@ func NewHashWriter(sliceSize, pieceSize, fileSize int64) *HashWriter {
fileMD5: md5.New(),
fileSHA1: sha1.New(),
fileSHA256: sha256.New(),
fileGCID: hash_extend.GCID.New(fileSize),
fileGCID: hash_extend.GCID.NewFunc(fileSize),
sliceMD5: md5.New(),
pieceMD5: md5.New(),
pieceSHA1: sha1.New(),
+2 -2
View File
@@ -111,7 +111,7 @@ func FsStream(c *gin.Context) {
var seedHasher *torrent.HashWriter
var uploadReader io.Reader = c.Request.Body
if generateSeed {
seedHasher = torrent.NewHashWriter(seedPieceSize(c), seedPieceSize(c))
seedHasher = torrent.NewHashWriter(seedPieceSize(c), seedPieceSize(c), 0)
uploadReader = io.TeeReader(c.Request.Body, seedHasher)
}
s := &stream.FileStream{
@@ -225,7 +225,7 @@ func FsForm(c *gin.Context) {
var seedHasher *torrent.HashWriter
var uploadReader io.Reader = f
if generateSeed {
seedHasher = torrent.NewHashWriter(seedPieceSize(c), seedPieceSize(c))
seedHasher = torrent.NewHashWriter(seedPieceSize(c), seedPieceSize(c), 0)
uploadReader = io.TeeReader(f, seedHasher)
}
s := &stream.FileStream{
+1 -1
View File
@@ -1224,7 +1224,7 @@ func rehashSeedFile(c *gin.Context, user *model.User, sourcePath string, pieceSi
return out, err
}
defer rc.Close()
hasher := torrent.NewHashWriter(pieceSize, pieceSize)
hasher := torrent.NewHashWriter(pieceSize, pieceSize, obj.GetSize())
n, copyErr := io.Copy(hasher, io.LimitReader(rc, obj.GetSize()+1))
if copyErr != nil {
return out, fmt.Errorf("read %s: %w", sourcePath, copyErr)