mirror of
https://github.com/OpenListTeam/OpenList.git
synced 2026-10-10 04:53:09 +08:00
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:
+60
-34
@@ -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
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
@@ -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 }
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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 }
|
||||
|
||||
@@ -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 }
|
||||
|
||||
@@ -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 }
|
||||
|
||||
@@ -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 }
|
||||
|
||||
@@ -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 }
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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")
|
||||
)
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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{
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user