Compare commits

..

16 Commits

Author SHA1 Message Date
renovate[bot] b2870dda3c fix(deps): update module github.com/fclairamb/ftpserverlib to v0.32.4 2026-10-05 18:39:31 +00:00
naiy_ 4c39bbe9c2 fix(drivers/189pc): align login with the official PC client (#3105)
QR code scans were authorized on the phone but the storage stayed stuck on
the QR page, and token-only setups failed with "params is null".

The driver used appId 8025431004 while the official PC client uses
9317140619. Tokens, sessions and QR sessions are all scoped to an appId, so
the mismatch meant the QR poll never saw status:0 and an accessToken could
not be exchanged for a sessionSecret.

- Use appId 9317140619 and version 7.2.4.0. QR state polling uses
  clientType=1; password login keeps 10020.
- Send the QR poll parameters the official client sends (cb_SaveName,
  isOauth2, state, user-finger header, logbox Referer) and poll locally
  instead of only checking once per save.
- Parse lt/reqId from the logbox redirect and paramId from appConf.do. The
  new login page no longer embeds them as inline variables; the old inline
  format is still supported.
- Implement the -133 second device verification via
  sendSmsCodeForSecondAuth/submitForSecondAuth. That endpoint has no
  dedicated SMS field: the code goes into epd, encrypted with the same
  public key used for the password. Persist the DEVICEID cookie so the
  verification only happens once.
- Detect refreshToken.do failures. It reports them as HTTP 200 with a
  result field, so SetError never fired and a failed refresh was treated as
  success, surfacing later as a misleading "params is null".
- username/password are no longer required, so token-only storages save
  without placeholders. clientSn/jgOpenId are optional and only sent when
  configured; user-finger is generated once per storage.
- Return named errors instead of panicking when the login page changes shape.

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-10-03 15:45:29 +08:00
帅丘 e1d88b071f fix(drivers/lanzou): support new /fn download page structure (#3157)
Lanzou recently changed the file share page: download params moved
into an iframe (/fn?<token>) inner page, and the download API is now
an absolute URL (https://apifile.woozooo.com/ajaxfile.php?file=N)
with new sign params (wp_sign/ajaxdata, action=downprocess).

The old findFileIDReg ('/ajaxm.php?file=N' relative path) no longer
matches, causing 'failed link: failed get link: not find file id'.

Fall back to parsing the new /fn page structure when the legacy
regex does not match. Legacy flow is kept untouched.

Signed-off-by: rzsgsfm <rzsgsfm@users.noreply.github.com>
Co-authored-by: rzsgsfm <rzsgsfm@users.noreply.github.com>
Co-authored-by: GLM (ZCode CLI) <noreply@z.ai>
2026-10-03 14:50:51 +08:00
ILoveScratch ea10624fb6 fix: harden request handling for files, search, proxy and SSO
Co-authored-by: PIKACHUIM <PIKACHUIM@users.noreply.github.com>
2026-09-29 19:05:50 +08:00
ILoveScratch 54ae9d7451 fix(internal/db/searchnode): fix index update path (#3128) 2026-09-24 19:25:29 +08:00
Yinan Qin c16701b94b fix(build): update cgo-actions version to v1.3.0 and fallback FreeBSD version to 14.4 (#3130) 2026-09-24 19:23:21 +08:00
Nostalgia 893457cd50 fix(aliyundrive): limit callback concurrency (#3071)
* fix(aliyundrive): limit callback concurrency

- Share proxy callback admission by Aliyun user identity and hold permits for complete response-body lifetimes.
- Retry only verified callback-capacity rejections while preserving direct redirects and server download limiting.
- Map exhausted temporary capacity to S3 SlowDown through the merged OpenListTeam gofakes3 module.
- Cover shared limits, lifecycle release, cancellation, retry classification, and the S3 HTTP response.

Co-authored-by: Codex <267193182+codex@users.noreply.github.com>

# Conflicts:
#	go.mod
#	go.sum
#	server/s3/pager.go

* fix(op): separate redirect and proxy link cache entries

- Include redirect mode in the link cache key for all drivers.

- Cover both redirect-to-proxy and proxy-to-redirect cache reuse.

Co-authored-by: Codex <267193182+codex@users.noreply.github.com>

* fix(proxy): close range bodies before opening next

- make ServeHTTP own each range body and preserve cleanup failures
- remove the aggregate range closer and pass range readers directly
- replace the obsolete callback transport test with focused lifecycle coverage

Co-authored-by: Codex <267193182+codex@users.noreply.github.com>

---------

Co-authored-by: nostalume <nostalucent@gmail.com>
Co-authored-by: Codex <267193182+codex@users.noreply.github.com>
2026-09-24 12:01:50 +08:00
Nostalgia 90acfa18e4 refactor(context): centralize request origin access (#3100) 2026-09-24 01:57:21 +08:00
Nostalgia 1462d63a48 fix(net): preserve cancellation errors during partitioned downloads (#3090) 2026-09-24 01:47:30 +08:00
spark cadbf87246 fix(teldrive): use a valid root path for listings (#3108) 2026-09-24 01:46:35 +08:00
ZRHan 9de3f69b8f fix(fs): only list parent dir for related objs when file is video (#3113) 2026-09-24 01:46:14 +08:00
Wray e73a80c78c feat(drivers/pikpak): auto re-login with username/password when tokens expire (#3001) 2026-09-24 01:29:56 +08:00
ZRHan 6c6009109f fix(fs): return FOLDER type for directories in fs/get responses (#3114) 2026-09-24 01:26:42 +08:00
Nostalgia b51d1c8284 fix(s3): close ranged response bodies (#3095) 2026-09-24 01:26:26 +08:00
ILoveScratch f286862c61 refactor(stream): index parked readers by offset instead of full scans (#3048) 2026-09-24 01:25:57 +08:00
awaae 40f4f6546f fix(ftp): handle EOF and detect content type from read bytes (#3125) 2026-09-24 01:20:20 +08:00
95 changed files with 3778 additions and 5447 deletions
+1 -1
View File
@@ -124,7 +124,7 @@ jobs:
FRONTEND_REPO: ${{ vars.FRONTEND_REPO }}
- name: Build
uses: OpenListTeam/cgo-actions@6fcace5934c36d70503dba06e2396bb58b766130 # v1.2.5
uses: OpenListTeam/cgo-actions@d760a8ec1a6be1f8ec181229e11cb2671797f9e0 # v1.3.0
with:
targets: ${{ matrix.target }}
flags: ${{ matrix.flags || '-ldflags=' }}
+1 -1
View File
@@ -42,7 +42,7 @@ jobs:
FRONTEND_REPO: ${{ vars.FRONTEND_REPO }}
- name: Build
uses: OpenListTeam/cgo-actions@6fcace5934c36d70503dba06e2396bb58b766130 # v1.2.5
uses: OpenListTeam/cgo-actions@d760a8ec1a6be1f8ec181229e11cb2671797f9e0 # v1.3.0
with:
targets: ${{ matrix.target }}
flags: ${{ contains(matrix.target, '-musl') && '-ldflags=-linkmode external -extldflags ''-static -fpic''' || '-ldflags=' }}
+2 -2
View File
@@ -531,8 +531,8 @@ BuildReleaseFreeBSD() {
sed 's/\.0$//')
if [ -z "$freebsd_version" ]; then
echo "Failed to get FreeBSD version, falling back to 14.3"
freebsd_version="14.3"
echo "Failed to get FreeBSD version, falling back to 14.4"
freebsd_version="14.4"
fi
echo "Using FreeBSD version: $freebsd_version"
-74
View File
@@ -1,74 +0,0 @@
package _115
import (
"context"
"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"
)
// RapidHashAlgos 返回 115 支持的秒传哈希算法(SHA1)
func (d *Pan115) RapidHashAlgos() []utils.HashType {
return []utils.HashType{*utils.SHA1}
}
// RapidHashNeedsPieces 115 不需要分片哈希
func (d *Pan115) RapidHashNeedsPieces() bool {
return false
}
// 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
}
-11
View File
@@ -171,22 +171,11 @@ func (d *Open115) Get(ctx context.Context, path string) (model.Obj, error) {
path = stdpath.Join(d.parentPath, path)
resp, err := d.client.GetFolderInfoByPath(ctx, path)
if err != nil {
// SDK-level "object not found" (empty array response from API)
if errors.Is(err, sdk.ErrObjectNotFound) {
return d.getFromParent(ctx, path, "")
}
// API-level error response (State=false), treat as not found
// since this is a path-lookup that can't resolve the target
var apiErr *sdk.Error
if errors.As(err, &apiErr) {
return nil, errs.ObjectNotFound
}
return nil, err
}
// Handle null/empty response (e.g., API returns null data for non-existent path)
if resp.FileID == "" {
return nil, errs.ObjectNotFound
}
obj := &Obj{
Fid: resp.FileID,
Fn: resp.FileName,
-73
View File
@@ -1,73 +0,0 @@
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 云盘支持的秒传哈希算法(MD5)
func (d *Pan123) RapidHashAlgos() []utils.HashType {
return []utils.HashType{*utils.MD5}
}
// RapidHashNeedsPieces 123 云盘不需要分片哈希
func (d *Pan123) RapidHashNeedsPieces() bool {
return false
}
// 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
}
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,
}
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
}
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
}
-55
View File
@@ -1,55 +0,0 @@
package _123_open
import (
"context"
"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"
)
// 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.ErrUnavailableHash
}
parentID, err := strconv.ParseInt(dstDir.GetID(), 10, 64)
if err != nil {
return nil, err
}
resp, err := d.sha1Reuse(parentID, req.Name, sha1Hash, req.Size, 1)
if err != nil {
return nil, err
}
if !resp.Data.Reuse {
return nil, errs.ErrHashMismatch
}
return &model.ObjThumb{
Object: model.Object{
ID: strconv.FormatInt(resp.Data.FileID, 10),
Name: req.Name,
Size: req.Size,
IsFolder: false,
Path: path.Join(dstDir.GetPath(), req.Name),
Modified: time.Now(),
},
}, nil
}
+9 -4
View File
@@ -6,6 +6,7 @@ import (
"encoding/hex"
"fmt"
"io"
"strings"
"github.com/OpenListTeam/OpenList/v4/internal/model"
"github.com/OpenListTeam/OpenList/v4/pkg/torrent"
@@ -14,8 +15,12 @@ import (
// GenerateTorrent 根据上传过程中收集的哈希信息生成包含 CAS 扩展的 torrent 文件
func GenerateTorrent(fileName string, fileSize int64, fileMD5 string, sliceMD5s []string, sliceSize int64, pieceHashes []byte) ([]byte, error) {
// 计算 sliceMD5(统一走规范实现)
sliceMD5 := torrent.SliceMD5FromPieces(sliceMD5s, fileMD5)
// 计算 sliceMD5
sliceMD5 := fileMD5
if len(sliceMD5s) > 1 {
joined := strings.Join(sliceMD5s, "\n")
sliceMD5 = strings.ToUpper(torrent.GetMD5Str(joined))
}
t := torrent.NewTorrent(fileName, fileSize, fileMD5)
t.Info.PieceLength = sliceSize
@@ -25,7 +30,7 @@ func GenerateTorrent(fileName string, fileSize int64, fileMD5 string, sliceMD5s
SliceMD5: sliceMD5,
SliceMD5s: sliceMD5s,
SliceSize: sliceSize,
Cloud: torrent.Cloud189,
Cloud: "189",
})
return t.Encode()
@@ -90,7 +95,7 @@ func ComputeTorrentFromReader(reader io.Reader, fileName string, fileSize int64,
sliceSize = torrent.DefaultPieceSize
}
hw := torrent.NewHashWriter(sliceSize, sliceSize, fileSize)
hw := torrent.NewHashWriter(sliceSize, sliceSize)
buf := make([]byte, 32*1024)
for {
-35
View File
@@ -1,35 +0,0 @@
package _189_tv
import (
"context"
"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"
)
// RapidHashAlgos 返回 189 电视支持的秒传哈希算法(MD5)
func (d *Cloud189TV) RapidHashAlgos() []utils.HashType {
return []utils.HashType{*utils.MD5}
}
// RapidHashNeedsPieces 189 电视不需要分片哈希
func (d *Cloud189TV) RapidHashNeedsPieces() bool {
return false
}
// RapidUploadByHashes 使用种子中的 MD5 哈希尝试秒传
func (d *Cloud189TV) 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.ErrUnavailableHash
}
stream := driver.NewSeedHashStream(req)
obj, err := d.RapidUpload(ctx, dstDir, stream, d.isFamily(), overwrite)
if err != nil {
return nil, err
}
return obj, nil
}
+7 -2
View File
@@ -7,11 +7,16 @@ import (
type Addition struct {
LoginType string `json:"login_type" type:"select" options:"password,qrcode" default:"password" required:"true"`
Username string `json:"username" required:"true"`
Password string `json:"password" required:"true"`
Username string `json:"username" help:"Not needed when an access token or refresh token is provided"`
Password string `json:"password" help:"Not needed when an access token or refresh token is provided"`
VCode string `json:"validate_code"`
SmsCode string `json:"sms_code" help:"SMS code for the second device verification, fill it in and save again when login asks for it"`
AccessToken string `json:"access_token" required:"false"`
RefreshToken string `json:"refresh_token" help:"To switch accounts, please clear this field"`
DeviceID string `json:"device_id" help:"DEVICEID cookie issued after the second device verification, keep it to avoid verifying again"`
ClientSn string `json:"client_sn" help:"Device serial number captured from the official client, leave it empty if you do not have one"`
JgOpenId string `json:"jg_open_id" help:"Optional push id reported by the official client"`
UserFinger string `json:"user_finger" help:"Device fingerprint sent with login requests, generated and kept automatically when empty"`
driver.RootID
OrderBy string `json:"order_by" type:"select" options:"filename,filesize,lastOpTime" default:"filename"`
OrderDirection string `json:"order_direction" type:"select" options:"asc,desc" default:"asc"`
-35
View File
@@ -1,35 +0,0 @@
package _189pc
import (
"context"
"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"
)
// 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.ErrUnavailableHash
}
stream := driver.NewSeedHashStream(req)
obj, err := d.RapidUpload(ctx, dstDir, stream, d.isFamily(), overwrite)
if err != nil {
return nil, err
}
return obj, nil
}
+109 -17
View File
@@ -7,6 +7,7 @@ import (
"encoding/hex"
"fmt"
"io"
"net/url"
"strings"
"time"
@@ -27,8 +28,12 @@ import (
// fileName: 文件名
// fileSize: 文件大小
func GenerateTorrent(fileName string, fileSize int64, fileMD5 string, sliceMD5s []string, sliceSize int64, pieceHashes []byte) ([]byte, error) {
// 计算 sliceMD5(统一走规范实现)
sliceMD5 := torrent.SliceMD5FromPieces(sliceMD5s, fileMD5)
// 计算 sliceMD5
sliceMD5 := fileMD5
if len(sliceMD5s) > 1 {
joined := strings.Join(sliceMD5s, "\n")
sliceMD5 = strings.ToUpper(torrent.GetMD5Str(joined))
}
t := torrent.NewTorrent(fileName, fileSize, fileMD5)
t.Info.PieceLength = sliceSize
@@ -38,7 +43,7 @@ func GenerateTorrent(fileName string, fileSize int64, fileMD5 string, sliceMD5s
SliceMD5: sliceMD5,
SliceMD5s: sliceMD5s,
SliceSize: sliceSize,
Cloud: torrent.Cloud189,
Cloud: "189",
})
return t.Encode()
@@ -64,18 +69,101 @@ func (y *Cloud189PC) RapidUploadFromTorrent(ctx context.Context, dstDir model.Ob
fileName := t.Info.Name
fileSize := t.GetTotalSize()
// 优先使用 torrent 中嵌入的分片 MD5 与大写整文件 MD5
fileMD5 := strings.ToUpper(cas.FileMD5)
sliceMD5s := make([]string, len(cas.SliceMD5s))
for i, s := range cas.SliceMD5s {
sliceMD5s[i] = strings.ToUpper(s)
// 统一 MD5 为大写(与正常上传保持一致,天翼云盘要求大写)
fileMD5Upper := strings.ToUpper(cas.FileMD5)
// 优先使用 torrent 中嵌入的分片大小,与生成时保持一致
sliceSize := cas.SliceSize
if sliceSize <= 0 {
sliceSize = partSize(fileSize)
}
// 复用统一的 CAS 秒传核心实现
respObj, err := y.rapidUploadByCAS(ctx, dstDir, fileName, fileSize, fileMD5, sliceMD5s, cas.SliceSize, overwrite)
// 计算 sliceMd5(与上传时一致的算法)
// 优先使用 torrent 中已有的 SliceMD5;仅当有多分片列表时才重新计算
sliceMd5Hex := strings.ToUpper(cas.SliceMD5)
if sliceMd5Hex == "" {
sliceMd5Hex = fileMD5Upper
}
if len(cas.SliceMD5s) > 1 {
// 分片 MD5 也需要统一大写后再拼接计算
upperSliceMD5s := make([]string, len(cas.SliceMD5s))
for i, s := range cas.SliceMD5s {
upperSliceMD5s[i] = strings.ToUpper(s)
}
sliceMd5Hex = strings.ToUpper(utils.GetMD5EncodeStr(strings.Join(upperSliceMD5s, "\n")))
}
// 使用与 Web 端一致的三步秒传流程
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
_, err = y.request(fullUrl+"/initMultiUpload", "GET", func(req *resty.Request) {
req.SetContext(ctx)
}, initParams, &uploadInfo, isFamily)
if err != nil {
utils.Log.Errorf("[RapidUpload] 秒传失败: fileMD5=%s, err=%v", fileMD5, err)
return nil, err
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"`
}
_, err = y.request(fullUrl+"/checkTransSecond", "GET", func(req *resty.Request) {
req.SetContext(ctx)
}, checkParams, &checkResp, isFamily)
if err != nil {
utils.Log.Errorf("[RapidUpload] checkTransSecond 失败: uploadFileId=%s, err=%v", uploadFileId, err)
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)
var resp CommitMultiUploadFileResp
commitParams := Params{
"uploadFileId": uploadFileId,
"fileMd5": fileMD5Upper,
"sliceMd5": sliceMd5Hex,
"lazyCheck": "1",
"opertype": IF(overwrite, "3", "1"),
}
_, err = y.request(fullUrl+"/commitMultiUploadFile", "GET", func(req *resty.Request) {
req.SetContext(ctx)
}, commitParams, &resp, isFamily)
if err != nil {
utils.Log.Errorf("[RapidUpload] commitMultiUploadFile 失败: uploadFileId=%s, err=%v", uploadFileId, err)
return nil, fmt.Errorf("提交上传失败: %w", err)
}
// 秒传成功后,将 torrent 文件上传到目标目录(异步,不影响秒传结果)
@@ -108,7 +196,7 @@ func (y *Cloud189PC) RapidUploadFromTorrent(ctx context.Context, dstDir model.Ob
}()
}
return respObj, nil
return resp.toFile(), nil
}
// ComputeTorrentFromReader 从 io.Reader 计算并生成 torrent 文件
@@ -118,7 +206,7 @@ func ComputeTorrentFromReader(reader io.Reader, fileName string, fileSize int64,
sliceSize = torrent.DefaultPieceSize
}
hw := torrent.NewHashWriter(sliceSize, sliceSize, fileSize)
hw := torrent.NewHashWriter(sliceSize, sliceSize)
buf := make([]byte, 32*1024)
for {
@@ -171,8 +259,12 @@ func InjectCASIntoTorrent(torrentData []byte, fileMD5 string, sliceMD5s []string
return nil, fmt.Errorf("解析 torrent 失败: %w", err)
}
// 计算 sliceMD5(统一走规范实现)
sliceMD5 := torrent.SliceMD5FromPieces(sliceMD5s, fileMD5)
// 计算 sliceMD5
sliceMD5 := fileMD5
if len(sliceMD5s) > 1 {
joined := strings.Join(sliceMD5s, "\n")
sliceMD5 = strings.ToUpper(torrent.GetMD5Str(joined))
}
// 注入 CAS 信息
t.SetCASInfo(&torrent.CASInfo{
@@ -180,7 +272,7 @@ func InjectCASIntoTorrent(torrentData []byte, fileMD5 string, sliceMD5s []string
SliceMD5: sliceMD5,
SliceMD5s: sliceMD5s,
SliceSize: sliceSize,
Cloud: torrent.Cloud189,
Cloud: "189",
})
// 同时更新 info 中的 md5sum 字段
+62
View File
@@ -72,6 +72,8 @@ type BaseLoginParam struct {
// 请求头参数
Lt string
ReqId string
// logbox页面地址,作为后续请求的Referer,缺失会被判定为陌生设备
Referer string
// 表单参数
ParamId string
@@ -97,10 +99,20 @@ type LoginParam struct {
// rsa密钥
jRsaKey string
// 加密字段的前缀,服务端下发(如 {NRP})
rsaPrefix string
// 设备二次校验时服务端返回的加密手机号
SecondAuthMobile string
BaseLoginParam
}
// encryptSecret 用登陆时拿到的公钥加密敏感值,格式与userName/epd一致
func (p *LoginParam) encryptSecret(value string) string {
return p.rsaPrefix + RsaEncrypt(p.jRsaKey, value)
}
// 登陆加密相关
type EncryptConfResp struct {
Result int `json:"result"`
@@ -116,6 +128,35 @@ type LoginResp struct {
Msg string `json:"msg"`
Result int `json:"result"`
ToUrl string `json:"toUrl"`
// 设备二次校验时返回的加密手机号
Mobile string `json:"mobile"`
}
// 登陆页配置,新版登陆页的paramId由该接口下发
// 该接口的result可能是数字也可能是字符串
type AppConfResp struct {
Result any `json:"result"`
Msg string `json:"msg"`
Data struct {
ParamId string `json:"paramId"`
AccountType string `json:"accountType"`
ReturnUrl string `json:"returnUrl"`
MailSuffix string `json:"mailSuffix"`
} `json:"data"`
}
func (r *AppConfResp) Succeeded() bool {
switch v := r.Result.(type) {
case nil:
return true
case string:
return v == "0" || v == ""
case float64:
return v == 0
case int:
return v == 0
}
return false
}
// 刷新session返回
@@ -149,6 +190,27 @@ type AppSessionResp struct {
RefreshToken string `json:"refreshToken"`
}
// 刷新token返回,失败时以HTTP 200返回result/msg,需要单独判断
type RefreshTokenResp struct {
AccessToken string `json:"accessToken"`
RefreshToken string `json:"refreshToken"`
ExpiresIn int `json:"expiresIn"`
Result int `json:"result"`
Msg string `json:"msg"`
}
func (r *RefreshTokenResp) HasError() bool {
return r.Result != 0 || r.AccessToken == ""
}
func (r *RefreshTokenResp) Error() string {
if r.Msg != "" {
return fmt.Sprintf("refresh token failed, result: %d, msg: %s", r.Result, r.Msg)
}
return fmt.Sprintf("refresh token failed, result: %d", r.Result)
}
// 家庭云账户
type FamilyInfoListResp struct {
FamilyInfoResp []FamilyInfoResp `json:"familyInfoResp"`
+355 -173
View File
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+18 -4
View File
@@ -11,6 +11,7 @@ import (
"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/internal/stream"
"github.com/OpenListTeam/OpenList/v4/pkg/utils"
"github.com/go-resty/resty/v2"
log "github.com/sirupsen/logrus"
@@ -22,8 +23,9 @@ type AliyundriveOpen struct {
DriveId string
limiter *limiter
ref *AliyundriveOpen
limiter *limiter
ref *AliyundriveOpen
callback *callbackRegistration
}
func (d *AliyundriveOpen) Config() driver.Config {
@@ -35,6 +37,7 @@ func (d *AliyundriveOpen) GetAddition() driver.Additional {
}
func (d *AliyundriveOpen) Init(ctx context.Context) error {
d.CallbackConcurrency = normalizeCallbackConcurrency(d.CallbackConcurrency)
d.limiter = getLimiterForUser(globalLimiterUserID) // First create a globally shared limiter to limit the initial requests.
if d.LIVPDownloadFormat == "" {
d.LIVPDownloadFormat = "jpeg"
@@ -52,6 +55,7 @@ func (d *AliyundriveOpen) Init(ctx context.Context) error {
userid := utils.Json.Get(res, "user_id").ToString()
d.limiter.free()
d.limiter = getLimiterForUser(userid) // Allocate a corresponding limiter for each user.
d.callback = registerCallbackLimiter(userid, d.CallbackConcurrency)
return nil
}
@@ -65,6 +69,10 @@ func (d *AliyundriveOpen) InitReference(storage driver.Driver) error {
}
func (d *AliyundriveOpen) Drop(ctx context.Context) error {
if d.callback != nil {
d.callback.unregister()
d.callback = nil
}
d.limiter.free()
d.limiter = nil
d.ref = nil
@@ -119,10 +127,16 @@ func (d *AliyundriveOpen) Link(ctx context.Context, file model.Obj, args model.L
url = utils.Json.Get(res, "streamsUrl", d.LIVPDownloadFormat).ToString()
}
exp := time.Minute
return &model.Link{
link := &model.Link{
URL: url,
Expiration: &exp,
}, nil
}
if args.Redirect {
return link, nil
}
link.URL = ""
link.RangeReader = stream.RateLimitRangeReaderFunc(d.callbackRangeReader(url, file.GetSize()))
return link, nil
}
func (d *AliyundriveOpen) MakeDir(ctx context.Context, parentDir model.Obj, dirName string) (model.Obj, error) {
+14 -13
View File
@@ -8,19 +8,20 @@ import (
type Addition struct {
DriveType string `json:"drive_type" type:"select" options:"default,resource,backup" default:"resource"`
driver.RootID
RefreshToken string `json:"refresh_token" required:"true"`
OrderBy string `json:"order_by" type:"select" options:"name,size,updated_at,created_at"`
OrderDirection string `json:"order_direction" type:"select" options:"ASC,DESC"`
UseOnlineAPI bool `json:"use_online_api" default:"true"`
AlipanType string `json:"alipan_type" required:"true" type:"select" default:"default" options:"default,alipanTV"`
APIAddress string `json:"api_url_address" default:"https://api.oplist.org/alicloud/renewapi"`
ClientID string `json:"client_id" help:"Keep it empty if you don't have one"`
ClientSecret string `json:"client_secret" help:"Keep it empty if you don't have one"`
RemoveWay string `json:"remove_way" required:"true" type:"select" options:"trash,delete"`
RapidUpload bool `json:"rapid_upload" help:"If you enable this option, the file will be uploaded to the server first, so the progress will be incorrect"`
InternalUpload bool `json:"internal_upload" help:"If you are using Aliyun ECS is located in Beijing, you can turn it on to boost the upload speed"`
LIVPDownloadFormat string `json:"livp_download_format" type:"select" options:"jpeg,mov" default:"jpeg"`
AccessToken string
RefreshToken string `json:"refresh_token" required:"true"`
OrderBy string `json:"order_by" type:"select" options:"name,size,updated_at,created_at"`
OrderDirection string `json:"order_direction" type:"select" options:"ASC,DESC"`
UseOnlineAPI bool `json:"use_online_api" default:"true"`
AlipanType string `json:"alipan_type" required:"true" type:"select" default:"default" options:"default,alipanTV"`
APIAddress string `json:"api_url_address" default:"https://api.oplist.org/alicloud/renewapi"`
ClientID string `json:"client_id" help:"Keep it empty if you don't have one"`
ClientSecret string `json:"client_secret" help:"Keep it empty if you don't have one"`
RemoveWay string `json:"remove_way" required:"true" type:"select" options:"trash,delete"`
RapidUpload bool `json:"rapid_upload" help:"If you enable this option, the file will be uploaded to the server first, so the progress will be incorrect"`
InternalUpload bool `json:"internal_upload" help:"If you are using Aliyun ECS is located in Beijing, you can turn it on to boost the upload speed"`
LIVPDownloadFormat string `json:"livp_download_format" type:"select" options:"jpeg,mov" default:"jpeg"`
CallbackConcurrency int `json:"callback_concurrency" type:"number" default:"1" help:"Maximum active proxied downloads shared by Aliyun user"`
AccessToken string
}
var config = driver.Config{
-88
View File
@@ -1,88 +0,0 @@
package aliyundrive_open
import (
"context"
"net/http"
"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 返回阿里云盘支持的秒传哈希算法(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.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(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_code": proofCode,
}).SetResult(&resp)
})
if err != nil {
return nil, err
}
if !resp.RapidUpload {
return nil, errs.ErrHashMismatch
}
if resp.FileId != "" {
obj, err := d.completeUpload(ctx, resp.FileId, resp.UploadId)
if err != nil {
return nil, err
}
return obj, nil
}
return &model.ObjThumb{
Object: model.Object{
Name: req.Name,
Size: req.Size,
Modified: time.Now(),
IsFolder: false,
},
}, nil
}
-35
View File
@@ -1,35 +0,0 @@
package baidu_netdisk
import (
"context"
"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"
)
// 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.ErrUnavailableHash
}
stream := driver.NewSeedHashStream(req)
obj, err := d.PutRapid(ctx, dstDir, stream)
if err != nil {
return nil, err
}
return obj, nil
}

Some files were not shown because too many files have changed in this diff Show More