From 06eef0996fffb2ab2a1aecacd22651f1595bad1b Mon Sep 17 00:00:00 2001 From: Har01d Date: Wed, 29 Jul 2026 12:47:55 +0800 Subject: [PATCH 1/6] =?UTF-8?q?123=E7=A7=92=E4=BC=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- drivers/115_share/driver.go | 29 ++- drivers/123_open/driver.go | 8 + drivers/123_open/meta.go | 6 + drivers/123_open/token.go | 62 ++++++ drivers/123_open/types.go | 10 + drivers/123_open/upload.go | 21 ++ drivers/123_open/util.go | 21 ++ drivers/123_rapid/anon.go | 199 ++++++++++++++++++ drivers/123_rapid/rapid.go | 155 ++++++++++++++ drivers/123_rapid/rapid_test.go | 62 ++++++ drivers/aliyundrive_share2_open/driver.go | 41 +++- .../aliyundrive_share2_open/driver_test.go | 6 + drivers/guangyapan/driver.go | 15 ++ drivers/guangyapan/types.go | 15 ++ drivers/guangyapan_share/driver.go | 38 +++- drivers/quark_uc_share/types.go | 1 + drivers/quark_uc_share/util.go | 42 ++++ internal/bootstrap/data/setting.go | 5 + internal/conf/const.go | 6 + 19 files changed, 737 insertions(+), 5 deletions(-) create mode 100644 drivers/123_rapid/anon.go create mode 100644 drivers/123_rapid/rapid.go create mode 100644 drivers/123_rapid/rapid_test.go diff --git a/drivers/115_share/driver.go b/drivers/115_share/driver.go index ab6384856..3f6c5389a 100644 --- a/drivers/115_share/driver.go +++ b/drivers/115_share/driver.go @@ -4,12 +4,13 @@ import ( "context" "errors" "fmt" - "net/http" _115 "github.com/OpenListTeam/OpenList/v4/drivers/115" + _123rapid "github.com/OpenListTeam/OpenList/v4/drivers/123_rapid" "github.com/OpenListTeam/OpenList/v4/internal/conf" "github.com/OpenListTeam/OpenList/v4/internal/op" "github.com/OpenListTeam/OpenList/v4/internal/setting" log "github.com/sirupsen/logrus" + "net/http" "strings" "time" @@ -103,6 +104,12 @@ func (d *Pan115Share) List(ctx context.Context, dir model.Obj, args model.ListAr } func (d *Pan115Share) Link(ctx context.Context, file model.Obj, args model.LinkArgs) (*model.Link, error) { + // 123 优先:开关开启时先按 SHA1 秒传到 123,失败/未命中回退现有「转存个人 115」取链。 + if setting.GetBool(conf.Pan115To123) { + if link := rapid115To123(ctx, file); link != nil { + return link, nil + } + } count := op.GetDriverCount("115 Cloud") var lastErr error for i := 0; i < count; i++ { @@ -115,6 +122,26 @@ func (d *Pan115Share) Link(ctx context.Context, file model.Obj, args model.LinkA return nil, lastErr } +// rapid115To123 把 115 分享文件按 SHA1 秒传到 123。声明为 var 便于单测替换。 +var rapid115To123 = func(ctx context.Context, file model.Obj) *model.Link { + parts := strings.Split(file.GetID(), "-") + if len(parts) < 2 { + return nil + } + link, err := _123rapid.RapidTo123(ctx, _123rapid.Source{ + HashType: utils.SHA1, + Hash: parts[1], + Name: file.GetName(), + Size: file.GetSize(), + }) + if err != nil || link == nil { + log.Debugf("[115-share] rapid to 123 skipped: %v", err) + return nil + } + log.Infof("[115-share] 使用123秒传直链: %s", file.GetName()) + return link +} + func (d *Pan115Share) link(ctx context.Context, file model.Obj, args model.LinkArgs) (*model.Link, error) { storage := op.Get115Driver(idx) idx++ diff --git a/drivers/123_open/driver.go b/drivers/123_open/driver.go index 78ff272b9..c9e860fa0 100644 --- a/drivers/123_open/driver.go +++ b/drivers/123_open/driver.go @@ -38,6 +38,14 @@ func (d *Open123) Init(ctx context.Context) error { // proactive refresh by renewapi or client credentials d.AccessToken = "" d.tm = &tokenManager{} + } else if d.OAuthProxy && d.RefreshToken != "" { + // oauth 代理(oauth.litepan.top):保留登录拿到的 access_token 先用, + // 到期(或接口返回 401 触发 Request 里的 forceRefresh)再走代理刷新, + // 避免一上来就刷新刚授权获取的 token(此时 refresh_token 可能尚未在代理侧确认)。 + d.tm = &tokenManager{} + if d.AccessToken != "" { + d.tm.expiredAt = time.Now().Add(time.Hour) + } } else { // 避免个人 token 刷新产生的多个登录,被动刷新 // 默认过期时间90天,jwt exp 不可靠 diff --git a/drivers/123_open/meta.go b/drivers/123_open/meta.go index d23f8eec6..ee27d7cb9 100644 --- a/drivers/123_open/meta.go +++ b/drivers/123_open/meta.go @@ -16,6 +16,12 @@ type Addition struct { // refresh_token方式的AccessToken 【对个人开发者暂未开放】 RefreshToken string `json:"RefreshToken" required:"false"` + // OAuthProxy:无自有 client_id 时,经 oauth 代理刷新(扫码授权拿到的 refresh_token)。 + OAuthProxy bool `json:"OAuthProxy" required:"false" default:"false"` + + // OAuthServer:oauth 代理地址(如 https://oauth.litepan.top),由消费端(alist-tvbox)传入。 + OAuthServer string `json:"OAuthServer" required:"false"` + // 使用在线API UseOnlineAPI bool `json:"use_online_api" default:"true"` APIAddress string `json:"api_url_address" default:"https://api.oplist.org/123cloud/renewapi"` diff --git a/drivers/123_open/token.go b/drivers/123_open/token.go index a628d22fe..e3e1da080 100644 --- a/drivers/123_open/token.go +++ b/drivers/123_open/token.go @@ -4,11 +4,15 @@ import ( "errors" "fmt" "net/http" + "strings" "sync" "time" "github.com/OpenListTeam/OpenList/v4/drivers/base" + "github.com/OpenListTeam/OpenList/v4/internal/conf" "github.com/OpenListTeam/OpenList/v4/internal/op" + "github.com/OpenListTeam/OpenList/v4/internal/token" + jsoniter "github.com/json-iterator/go" ) var ( @@ -48,6 +52,64 @@ func (d *Open123) getAccessToken(forceRefresh bool) (string, error) { } func (d *Open123) flushAccessToken() error { + // OAuthProxy:用户无自有 client_id,经 oauth 代理刷新(扫码授权的 refresh_token)。 + // 代理地址由消费端通过 OAuthServer 传入,留空时回退默认。 + if d.OAuthProxy && d.RefreshToken != "" { + oauthServer := strings.TrimSpace(d.OAuthServer) + if oauthServer == "" { + oauthServer = "https://oauth.litepan.top" + } + oauthRefreshURL := strings.TrimRight(oauthServer, "/") + "/api/oauth/refresh" + resp, err := base.RestyClient.R(). + SetHeader("Content-Type", "application/json"). + SetBody(base.Json{"driver_type": "123云盘Open", "refresh_token": d.RefreshToken}). + Post(oauthRefreshURL) + if err != nil { + return err + } + body := resp.Body() + at := jsoniter.Get(body, "data", "access_token").ToString() + if at == "" { + at = jsoniter.Get(body, "data", "token_data", "access_token").ToString() + } + if at == "" { + at = jsoniter.Get(body, "access_token").ToString() + } + if at == "" { + msg := jsoniter.Get(body, "message").ToString() + if msg == "" { + msg = jsoniter.Get(body, "data", "error").ToString() + } + if msg == "" { + msg = "empty access_token from oauth proxy" + } + return fmt.Errorf("123 open oauth refresh failed: %s", msg) + } + rt := jsoniter.Get(body, "data", "refresh_token").ToString() + if rt == "" { + rt = jsoniter.Get(body, "refresh_token").ToString() + } + if rt == "" { + rt = d.RefreshToken + } + ei := jsoniter.Get(body, "data", "expires_in").ToInt() + if ei <= 0 { + ei = jsoniter.Get(body, "expires_in").ToInt() + } + expiredAt, err := expiresInToExpiredAt(int64(ei)) + if err != nil { + return err + } + d.AccessToken = at + d.RefreshToken = rt + d.tm.expiredAt = expiredAt + op.MustSaveDriverStorage(d) + // 同步(可能轮换的)refresh_token 回 alist-tvbox,避免其重启后用旧 refresh_token。 + token.SaveAccountToken(conf.OPEN123, d.RefreshToken, int(d.ID)) + d.tm.blockRefresh = false + return nil + } + // Official app renewapi response contains access_token, refresh_token and expires_in. if d.UseOnlineAPI && d.RefreshToken != "" && len(d.APIAddress) > 0 { var resp RefreshTokenResp diff --git a/drivers/123_open/types.go b/drivers/123_open/types.go index 31dedf72b..b2fbaa458 100644 --- a/drivers/123_open/types.go +++ b/drivers/123_open/types.go @@ -223,3 +223,13 @@ type OfflineDownloadProcessResp struct { Status int `json:"status"` } `json:"data"` } + +// ShareCreateResp /api/v1/share/create 返回 +type ShareCreateResp struct { + BaseResp + Data struct { + ShareKey string `json:"shareKey"` + ShareID string `json:"shareID"` + SharePwd string `json:"sharePwd"` + } `json:"data"` +} diff --git a/drivers/123_open/upload.go b/drivers/123_open/upload.go index 0e03684e9..3511a4d4c 100644 --- a/drivers/123_open/upload.go +++ b/drivers/123_open/upload.go @@ -201,3 +201,24 @@ func (d *Open123) sha1Reuse(parentFileID int64, filename string, sha1Hash string } return &resp, nil } + +// Reuse 跨盘秒传入口(纯 hash,不下载/上传源文件),供 drivers/123_rapid 调用。 +// hashType=SHA1(115) 走 sha1_reuse;hashType=MD5(夸克/UC/光鸭)走 file/create(etag=md5)。 +// 返回 reuse=true 表示命中,fileID 为目标文件 id;reuse=false 表示未命中,调用方应回退。 +func (d *Open123) Reuse(hashType *utils.HashType, hash, filename string, size int64, duplicate int) (reuse bool, fileID int64, err error) { + if len(filename) == 0 { + return false, 0, fmt.Errorf("reuse: empty filename") + } + if hashType == utils.SHA1 { + resp, e := d.sha1Reuse(0, filename, hash, size, duplicate) + if e != nil { + return false, 0, e + } + return resp.Data.Reuse, resp.Data.FileID, nil + } + resp, e := d.create(0, filename, hash, size, duplicate, false) + if e != nil { + return false, 0, e + } + return resp.Data.Reuse, resp.Data.FileID, nil +} diff --git a/drivers/123_open/util.go b/drivers/123_open/util.go index 1b6eea2da..8f38f70e6 100644 --- a/drivers/123_open/util.go +++ b/drivers/123_open/util.go @@ -35,6 +35,7 @@ var ( // 不同情况下获取的AccessTokenQPS限制不同 如下模块化易 OfflineDownload = InitApiInfo(Api+"/api/v1/offline/download", 1) OfflineDownloadProcess = InitApiInfo(Api+"/api/v1/offline/download/process", 5) + ShareCreate = InitApiInfo(Api+"/api/v1/share/create", 1) ) func (d *Open123) Request(apiInfo *ApiInfo, method string, callback base.ReqCallback, resp interface{}) ([]byte, error) { @@ -246,6 +247,26 @@ func (d *Open123) trash(fileId int64) error { return nil } +// CreateShare 对指定文件创建公开分享,供后续免登录解析直链。 +// 123 Open 要求 fileIdList 为逗号分隔字符串(传数组会返回"分享ID非法")。shareExpire<=0 永久。 +func (d *Open123) CreateShare(fileID int64, shareName string, shareExpire int) (*ShareCreateResp, error) { + if shareExpire < 0 { + shareExpire = 0 + } + var resp ShareCreateResp + _, err := d.Request(ShareCreate, http.MethodPost, func(req *resty.Request) { + req.SetBody(base.Json{ + "fileIdList": strconv.FormatInt(fileID, 10), + "shareName": shareName, + "shareExpire": shareExpire, + }) + }, &resp) + if err != nil { + return nil, err + } + return &resp, nil +} + func (d *Open123) createOfflineDownloadTask(ctx context.Context, url string, dirID, callback string) (taskID int, err error) { body := base.Json{ "url": url, diff --git a/drivers/123_rapid/anon.go b/drivers/123_rapid/anon.go new file mode 100644 index 000000000..c25e5a578 --- /dev/null +++ b/drivers/123_rapid/anon.go @@ -0,0 +1,199 @@ +package _123rapid + +import ( + "encoding/base64" + "errors" + "fmt" + "net/http" + "net/url" + "strconv" + "time" + + "github.com/OpenListTeam/OpenList/v4/drivers/base" + "github.com/OpenListTeam/OpenList/v4/internal/model" + "github.com/OpenListTeam/OpenList/v4/pkg/utils" + "github.com/go-resty/resty/v2" + jsoniter "github.com/json-iterator/go" +) + +// 匿名(免登录)通道:www.123pan.cn 的分享接口对公开分享可直接列目录/换直链, +// 无需 Bearer/auth-key。常量与 drivers/123_share 对齐。 +const ( + AnonOrigin = "https://www.123pan.cn" + AnonShareList = AnonOrigin + "/b/api/share/get" + AnonDownloadInfo = AnonOrigin + "/b/api/share/download/info" + AnonUA = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.0.0 Safari/537.36" +) + +// shareFileMeta 匿名列分享取得的文件元信息,用于免登录换直链。 +type shareFileMeta struct { + FileID int64 + FileName string + Size int64 + Etag string + S3KeyFlag string +} + +type shareListResp struct { + Code int `json:"code"` + Data struct { + InfoList []struct { + FileId int64 `json:"FileId"` + FileName string `json:"FileName"` + Size int64 `json:"Size"` + Etag string `json:"Etag"` + S3KeyFlag string `json:"S3KeyFlag"` + Type int `json:"Type"` + } `json:"InfoList"` + Next string `json:"Next"` + } `json:"data"` +} + +// requestAnon 匿名请求:无 Authorization、无 auth-key 签名,仅浏览器 UA + Referer。 +func requestAnon(targetUrl, method string, callback base.ReqCallback) ([]byte, error) { + req := base.RestyClient.R() + req.SetHeaders(map[string]string{ + "user-agent": AnonUA, + "referer": AnonOrigin + "/", + "origin": AnonOrigin, + "accept": "application/json,text/plain,*/*", + "platform": "android", + "app-version": "43", + "content-type": "application/json;charset=UTF-8", + }) + if callback != nil { + callback(req) + } + res, err := req.Execute(method, targetUrl) + if err != nil { + return nil, err + } + return res.Body(), nil +} + +// listShareFile 匿名列分享,定位 fileID 对应文件,取 Etag/S3KeyFlag 等元信息。 +func listShareFile(shareKey, pwd string, fileID int64) (*shareFileMeta, error) { + page := 1 + next := "0" + for safety := 0; safety < 50; safety++ { + q := map[string]string{ + "limit": "100", + "next": next, + "orderBy": "file_id", + "orderDirection": "desc", + "parentFileId": "0", + "Page": strconv.Itoa(page), + "shareKey": shareKey, + "SharePwd": pwd, + } + body, err := requestAnon(AnonShareList, http.MethodGet, func(req *resty.Request) { + req.SetQueryParams(q) + }) + if err != nil { + return nil, err + } + var resp shareListResp + _ = jsoniter.Unmarshal(body, &resp) + for _, it := range resp.Data.InfoList { + if it.FileId == fileID { + return &shareFileMeta{ + FileID: it.FileId, + FileName: it.FileName, + Size: it.Size, + Etag: it.Etag, + S3KeyFlag: it.S3KeyFlag, + }, nil + } + } + if len(resp.Data.InfoList) == 0 || resp.Data.Next == "" || resp.Data.Next == "-1" { + break + } + next = resp.Data.Next + page++ + } + return nil, fmt.Errorf("rapid: share %s 未找到文件 %d", shareKey, fileID) +} + +// anonDownloadURL 匿名换直链。code 5112 表示分享方流量耗尽。 +func anonDownloadURL(shareKey, pwd string, m *shareFileMeta) (string, error) { + body := map[string]any{ + "driveId": "0", + "shareKey": shareKey, + "SharePwd": pwd, + "etag": m.Etag, + "fileId": m.FileID, + "s3keyFlag": m.S3KeyFlag, + "FileName": m.FileName, + "size": m.Size, + } + respBody, err := requestAnon(AnonDownloadInfo, http.MethodPost, func(req *resty.Request) { + req.SetBody(body) + }) + if err != nil { + return "", err + } + code := utils.Json.Get(respBody, "code").ToInt() + msg := jsoniter.Get(respBody, "message").ToString() + if code == 5112 { + return "", errors.New("rapid: 123 分享流量包不足 (5112)") + } + if code != 0 { + return "", errors.New(msg) + } + return utils.Json.Get(respBody, "data", "DownloadURL").ToString(), nil +} + +// unwrap123DownloadLink 把 download/info 返回的 DownloadURL 解包成可播放直链: +// 处理 base64 params 重定向、302 location / data.redirect_url,并设 Referer;15 分钟有效。 +func unwrap123DownloadLink(downloadUrl string) (*model.Link, error) { + if downloadUrl == "" { + return nil, errors.New("empty 123 download url") + } + ou, err := url.Parse(downloadUrl) + if err != nil { + return nil, err + } + u_ := ou.String() + if nu := ou.Query().Get("params"); nu != "" { + du, e := base64.StdEncoding.DecodeString(nu) + if e != nil { + return nil, e + } + u, e := url.Parse(string(du)) + if e != nil { + return nil, e + } + u_ = u.String() + } + res, err := base.NoRedirectClient.R().SetHeader("Referer", AnonOrigin+"/").Get(u_) + if err != nil { + return nil, err + } + exp := 15 * time.Minute + link := &model.Link{Expiration: &exp, URL: u_} + if res.StatusCode() == 302 { + link.URL = res.Header().Get("location") + } else if res.StatusCode() < 300 { + link.URL = utils.Json.Get(res.Body(), "data", "redirect_url").ToString() + } + link.Header = http.Header{ + "Referer": []string{fmt.Sprintf("%s://%s/", ou.Scheme, ou.Host)}, + } + return link, nil +} + +// resolveAnonShareLink 免登录解析 123 分享直链:列分享取元信息 → 换直链 → 解包。 +func resolveAnonShareLink(shareKey, pwd string, fileID int64) (*model.Link, error) { + if shareKey == "" || fileID <= 0 { + return nil, errors.New("rapid: empty shareKey/fileID") + } + meta, err := listShareFile(shareKey, pwd, fileID) + if err != nil { + return nil, err + } + dl, err := anonDownloadURL(shareKey, pwd, meta) + if err != nil { + return nil, err + } + return unwrap123DownloadLink(dl) +} diff --git a/drivers/123_rapid/rapid.go b/drivers/123_rapid/rapid.go new file mode 100644 index 000000000..28e467b36 --- /dev/null +++ b/drivers/123_rapid/rapid.go @@ -0,0 +1,155 @@ +// Package _123rapid 实现跨盘秒传到 123(115/夸克·UC/光鸭 → 123)。 +// +// 移植自 Node.js 参考实现 rapid-transfer.js:纯 hash 秒传(不下载/上传源文件), +// 命中后 createShare 建公开分享,再免登录解析 123 分享直链返回。 +// 失败/未命中返回 nil 链,由调用方(各源 share 驱动的 Link())回退到源原生链。 +// +// 秒传原语复用 drivers/123_open: +// - SHA1(115) → Open123.sha1_reuse +// - MD5(夸克/UC/光鸭) → Open123.file/create (etag=md5) +// +// 免登录解析(匿名 share/get + share/download/info)移植自 drivers/123_share。 +package _123rapid + +import ( + "context" + "errors" + "fmt" + "strings" + "time" + + _123_open "github.com/OpenListTeam/OpenList/v4/drivers/123_open" + "github.com/OpenListTeam/OpenList/v4/internal/cache" + "github.com/OpenListTeam/OpenList/v4/internal/model" + "github.com/OpenListTeam/OpenList/v4/internal/op" + "github.com/OpenListTeam/OpenList/v4/pkg/utils" + log "github.com/sirupsen/logrus" +) + +const ( + open123DriverName = "123 Open" + // mappingTTL:秒传结果(fileId/shareKey)缓存时长。v1 不清理 123 临时文件, + // 分享长期有效,故给较长 TTL 避免重复秒传;过期后重放一次秒传即可。 + mappingTTL = 6 * time.Hour + shareExpire = 0 // 永久分享 +) + +// Source 描述待秒传的源文件。HashType=SHA1(115) 或 MD5(夸克/UC/光鸭)。 +type Source struct { + HashType *utils.HashType + Hash string // 40-hex(SHA1) 或 32-hex(MD5),大小写不敏感 + Name string + Size int64 +} + +func (s Source) valid() bool { + if s.Size <= 0 || s.Name == "" || s.HashType == nil { + return false + } + h := strings.ToLower(strings.TrimSpace(s.Hash)) + switch s.HashType { + case utils.SHA1: + return len(h) == utils.SHA1.Width + case utils.MD5: + return len(h) == utils.MD5.Width + } + return false +} + +func (s Source) cacheKey() string { + width := 0 + if s.HashType != nil { + width = s.HashType.Width + } + return fmt.Sprintf("%d:%s:%d", width, strings.ToLower(s.Hash), s.Size) +} + +// rapidEntry 缓存秒传结果:同一 hash+size 命中后不必再秒传/建分享,直接免登录解析。 +type rapidEntry struct { + fileID int64 + shareKey string + pwd string +} + +var ( + rapidCache = cache.NewKeyedCache[*rapidEntry](mappingTTL) + // rapidIdx 轮询 123 Open 账号(同一 hash 各账号结果一致,主要分散落盘)。 + rapidIdx int +) + +// RapidTo123 秒传 src 到某个 123 Open 账号并返回免登录可播放直链。 +// 成功返回 (link, nil);未命中或任何失败返回 (nil, err)(调用方据此回退源链)。 +// 找不到 123 Open 账号时返回 (nil, errNoOpen123)。 +func RapidTo123(ctx context.Context, src Source) (*model.Link, error) { + if !src.valid() { + return nil, errors.New("rapid: invalid source (need name/size/hash)") + } + + // 命中缓存:分享仍有效就直接免登录解析。 + if e, ok := rapidCache.Get(src.cacheKey()); ok && e != nil { + if link, err := resolveAnonShareLink(e.shareKey, e.pwd, e.fileID); err == nil && link != nil { + return link, nil + } + } + + count := op.GetDriverCount(open123DriverName) + if count == 0 { + return nil, errNoOpen123 + } + var lastErr error + for i := 0; i < count; i++ { + storage := op.GetFirstDriver(open123DriverName, rapidIdx) + rapidIdx++ + if storage == nil { + continue + } + open, ok := storage.(*_123_open.Open123) + if !ok || open == nil { + continue + } + + reuse, fileID, err := open.Reuse(src.HashType, src.Hash, src.Name, src.Size, 1) + if err != nil { + // 鉴权/限流类错误换下一个账号重试;业务错误直接返回。 + lastErr = err + log.Warnf("[rapid-to-123] Reuse failed (%s/%s): %v", open123DriverName, src.Name, err) + continue + } + if !reuse { + // 未命中:同一 hash 各账号结果一致,无需再试。 + return nil, errRapidMiss + } + if fileID <= 0 { + lastErr = errors.New("rapid: reuse hit but empty fileID") + continue + } + + share, err := open.CreateShare(fileID, src.Name, shareExpire) + if err != nil || share == nil || share.Data.ShareKey == "" { + lastErr = fmt.Errorf("rapid: create share failed: %w", err) + log.Warnf("[rapid-to-123] CreateShare failed (%s/%s): %v", open123DriverName, src.Name, err) + continue + } + entry := &rapidEntry{fileID: fileID, shareKey: share.Data.ShareKey, pwd: share.Data.SharePwd} + rapidCache.Set(src.cacheKey(), entry) + + link, err := resolveAnonShareLink(entry.shareKey, entry.pwd, fileID) + if err != nil || link == nil { + lastErr = fmt.Errorf("rapid: anon resolve failed: %w", err) + log.Warnf("[rapid-to-123] anon resolve failed (%s/%s): %v", open123DriverName, src.Name, err) + return nil, lastErr + } + log.Infof("[rapid-to-123] %s reuse hit → share %s", src.Name, entry.shareKey) + return link, nil + } + if lastErr != nil { + return nil, lastErr + } + return nil, errNoOpen123 +} + +var ( + errNoOpen123 = errors.New("rapid: 找不到 123 Open 账号") + // errRapidMiss 123 未命中该 hash(非错误,调用方静默回退)。 + errRapidMiss = errors.New("rapid: 123 未命中秒传") +) diff --git a/drivers/123_rapid/rapid_test.go b/drivers/123_rapid/rapid_test.go new file mode 100644 index 000000000..05d1a34d2 --- /dev/null +++ b/drivers/123_rapid/rapid_test.go @@ -0,0 +1,62 @@ +package _123rapid + +import ( + "strings" + "testing" + + "github.com/OpenListTeam/OpenList/v4/pkg/utils" +) + +func sha1Hex() string { return strings.Repeat("a", utils.SHA1.Width) } +func md5Hex() string { return strings.Repeat("b", utils.MD5.Width) } + +func TestSourceValid(t *testing.T) { + cases := []struct { + name string + src Source + want bool + }{ + {"sha1 ok", Source{HashType: utils.SHA1, Hash: sha1Hex(), Name: "a.mp4", Size: 1}, true}, + {"md5 ok", Source{HashType: utils.MD5, Hash: md5Hex(), Name: "a.mp4", Size: 1}, true}, + {"sha1 upper ok", Source{HashType: utils.SHA1, Hash: strings.ToUpper(sha1Hex()), Name: "a", Size: 2}, true}, + {"nil hashtype", Source{HashType: nil, Hash: sha1Hex(), Name: "a", Size: 1}, false}, + {"empty name", Source{HashType: utils.SHA1, Hash: sha1Hex(), Name: "", Size: 1}, false}, + {"zero size", Source{HashType: utils.SHA1, Hash: sha1Hex(), Name: "a", Size: 0}, false}, + {"sha1 too short", Source{HashType: utils.SHA1, Hash: "abc", Name: "a", Size: 1}, false}, + {"md5 too long", Source{HashType: utils.MD5, Hash: sha1Hex(), Name: "a", Size: 1}, false}, + {"unknown type", Source{HashType: &fakeHashType, Hash: sha1Hex(), Name: "a", Size: 1}, false}, + {"empty hash", Source{HashType: utils.SHA1, Hash: "", Name: "a", Size: 1}, false}, + } + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + if got := c.src.valid(); got != c.want { + t.Fatalf("valid=%v want %v", got, c.want) + } + }) + } +} + +// fakeHashType 既非 SHA1 也非 MD5,用于校验 valid() 的 default 分支。 +var fakeHashType = utils.HashType{} + +func TestSourceCacheKey(t *testing.T) { + s := Source{HashType: utils.MD5, Hash: strings.ToUpper(md5Hex()), Name: "x", Size: 7} + k := s.cacheKey() + // 缓存键必须小写化 hash,且含 width 与 size,避免不同文件/大小撞键。 + if !strings.Contains(k, strings.ToLower(md5Hex())) { + t.Fatalf("cacheKey %q 未小写化 hash", k) + } + if !strings.HasSuffix(k, ":7") { + t.Fatalf("cacheKey %q 未含 size", k) + } + // 相同源稳定。 + if s.cacheKey() != k { + t.Fatalf("cacheKey 不稳定") + } + // 不同 size 不同键。 + s2 := s + s2.Size = 8 + if s2.cacheKey() == k { + t.Fatalf("不同 size 应有不同 cacheKey") + } +} diff --git a/drivers/aliyundrive_share2_open/driver.go b/drivers/aliyundrive_share2_open/driver.go index 69af34d99..54ff4aa90 100755 --- a/drivers/aliyundrive_share2_open/driver.go +++ b/drivers/aliyundrive_share2_open/driver.go @@ -3,6 +3,7 @@ package aliyundrive_share2_open import ( "context" _115 "github.com/OpenListTeam/OpenList/v4/drivers/115" + _123rapid "github.com/OpenListTeam/OpenList/v4/drivers/123_rapid" "github.com/OpenListTeam/OpenList/v4/drivers/base" "github.com/OpenListTeam/OpenList/v4/internal/cache" "github.com/OpenListTeam/OpenList/v4/internal/conf" @@ -34,8 +35,35 @@ var aliyundriveShareAliTo115Enabled = func() bool { return setting.GetBool(conf.AliTo115) } +var aliyundriveShareAliTo123Enabled = func() bool { + return setting.GetBool(conf.AliTo123) +} + +// rapidAliTo123 把阿里文件按 SHA1(content_hash)秒传到 123。声明为 var 便于单测替换。 +var rapidAliTo123 = func(ctx context.Context, file *MyFile) *model.Link { + if file == nil { + return nil + } + sha1 := file.HashInfo.GetHash(utils.SHA1) + if len(sha1) != utils.SHA1.Width { + return nil + } + link, err := _123rapid.RapidTo123(ctx, _123rapid.Source{ + HashType: utils.SHA1, + Hash: sha1, + Name: file.Name, + Size: file.Size, + }) + if err != nil || link == nil { + log.Debugf("[ali-share] rapid to 123 skipped: %v", err) + return nil + } + log.Infof("[ali-share] 使用123秒传直链: %s", file.Name) + return link +} + func aliyundriveShareLinkCacheKey(fileID string) string { - return fileID + "|" + strconv.FormatBool(aliyundriveShareAliTo115Enabled()) + return fileID + "|" + strconv.FormatBool(aliyundriveShareAliTo115Enabled()) + "|" + strconv.FormatBool(aliyundriveShareAliTo123Enabled()) } var resolveAliyundriveShareLink = func(ctx context.Context, d *AliyundriveShare2Open, file model.Obj, args model.LinkArgs) (*model.Link, error) { @@ -44,7 +72,16 @@ var resolveAliyundriveShareLink = func(ctx context.Context, d *AliyundriveShare2 for i := 0; i < count; i++ { link, myFile, err := d.aliLink(file) if err == nil { - if strings.HasSuffix(file.GetName(), ".md") || !aliyundriveShareAliTo115Enabled() { + if strings.HasSuffix(file.GetName(), ".md") { + return link, nil + } + // 123 优先:阿里 content_hash 即 SHA1,与 115 同构,直接喂 123 sha1_reuse;未命中回退 115/阿里。 + if aliyundriveShareAliTo123Enabled() { + if l := rapidAliTo123(ctx, myFile); l != nil { + return l, nil + } + } + if !aliyundriveShareAliTo115Enabled() { return link, nil } diff --git a/drivers/aliyundrive_share2_open/driver_test.go b/drivers/aliyundrive_share2_open/driver_test.go index f8acc140f..a094e8067 100644 --- a/drivers/aliyundrive_share2_open/driver_test.go +++ b/drivers/aliyundrive_share2_open/driver_test.go @@ -14,6 +14,7 @@ func TestAliyundriveShare2OpenLink_CachesByFileIDAndAliTo115State(t *testing.T) origCache := aliyundriveShareLinkCache origResolver := resolveAliyundriveShareLink origAliTo115 := aliyundriveShareAliTo115Enabled + origAliTo123 := aliyundriveShareAliTo123Enabled aliyundriveShareLinkCache = cache.NewKeyedCache[*model.Link](time.Hour) resolveCalls := 0 resolveAliyundriveShareLink = func(ctx context.Context, d *AliyundriveShare2Open, file model.Obj, args model.LinkArgs) (*model.Link, error) { @@ -24,11 +25,13 @@ func TestAliyundriveShare2OpenLink_CachesByFileIDAndAliTo115State(t *testing.T) aliyundriveShareLinkCache = origCache resolveAliyundriveShareLink = origResolver aliyundriveShareAliTo115Enabled = origAliTo115 + aliyundriveShareAliTo123Enabled = origAliTo123 }) d := &AliyundriveShare2Open{} file := &model.Object{ID: "file-1", Name: "video.mp4"} + aliyundriveShareAliTo123Enabled = func() bool { return false } aliyundriveShareAliTo115Enabled = func() bool { return false } first, err := d.Link(context.Background(), file, model.LinkArgs{}) if err != nil { @@ -67,6 +70,7 @@ func TestAliyundriveShare2OpenLink_DoesNotCacheErrors(t *testing.T) { origCache := aliyundriveShareLinkCache origResolver := resolveAliyundriveShareLink origAliTo115 := aliyundriveShareAliTo115Enabled + origAliTo123 := aliyundriveShareAliTo123Enabled aliyundriveShareLinkCache = cache.NewKeyedCache[*model.Link](time.Hour) resolveCalls := 0 resolveAliyundriveShareLink = func(ctx context.Context, d *AliyundriveShare2Open, file model.Obj, args model.LinkArgs) (*model.Link, error) { @@ -77,8 +81,10 @@ func TestAliyundriveShare2OpenLink_DoesNotCacheErrors(t *testing.T) { aliyundriveShareLinkCache = origCache resolveAliyundriveShareLink = origResolver aliyundriveShareAliTo115Enabled = origAliTo115 + aliyundriveShareAliTo123Enabled = origAliTo123 }) + aliyundriveShareAliTo123Enabled = func() bool { return false } aliyundriveShareAliTo115Enabled = func() bool { return false } d := &AliyundriveShare2Open{} file := &model.Object{ID: "file-1", Name: "video.mp4"} diff --git a/drivers/guangyapan/driver.go b/drivers/guangyapan/driver.go index c81e1fec8..bd0cc27d5 100644 --- a/drivers/guangyapan/driver.go +++ b/drivers/guangyapan/driver.go @@ -221,6 +221,21 @@ func (d *GuangYaPan) Link(ctx context.Context, file model.Obj, args model.LinkAr }, nil } +// GetFileDetail 通过 /userres/v1/file/get_file_detail 读取文件 md5,用于跨盘秒传到 123。 +func (d *GuangYaPan) GetFileDetail(ctx context.Context, fileId string) (string, error) { + var resp fileDetailResp + if err := d.postAPI(ctx, "/userres/v1/file/get_file_detail", map[string]any{"fileId": fileId}, &resp); err != nil { + return "", err + } + for _, h := range []string{resp.Data.FileInfo.Md5, resp.Data.FileInfo.Etag, resp.Data.FileInfo.Hash, resp.Data.Md5, resp.Data.Etag} { + h = strings.ToLower(strings.TrimSpace(h)) + if len(h) == utils.MD5.Width { + return h, nil + } + } + return "", fmt.Errorf("file_detail 无可用 md5 (fileId=%s)", fileId) +} + func (d *GuangYaPan) MakeDir(ctx context.Context, parentDir model.Obj, dirName string) error { if err := d.ensureAccessToken(ctx); err != nil { return err diff --git a/drivers/guangyapan/types.go b/drivers/guangyapan/types.go index bc95b8007..8fecb8b05 100644 --- a/drivers/guangyapan/types.go +++ b/drivers/guangyapan/types.go @@ -71,6 +71,21 @@ type downloadResp struct { } `json:"data"` } +// fileDetailResp /userres/v1/file/get_file_detail(兼容 md5 在 fileInfo 或 data 直挂两种形态)。 +type fileDetailResp struct { + Code int `json:"code"` + Msg string `json:"msg"` + Data struct { + Md5 string `json:"md5"` + Etag string `json:"etag"` + FileInfo struct { + Md5 string `json:"md5"` + Etag string `json:"etag"` + Hash string `json:"hash"` + } `json:"fileInfo"` + } `json:"data"` +} + type createDirResp struct { Code int `json:"code"` Msg string `json:"msg"` diff --git a/drivers/guangyapan_share/driver.go b/drivers/guangyapan_share/driver.go index 6c2981627..ed7a5ca72 100644 --- a/drivers/guangyapan_share/driver.go +++ b/drivers/guangyapan_share/driver.go @@ -11,6 +11,7 @@ import ( "strings" "time" + _123rapid "github.com/OpenListTeam/OpenList/v4/drivers/123_rapid" "github.com/OpenListTeam/OpenList/v4/drivers/base" "github.com/OpenListTeam/OpenList/v4/drivers/guangyapan" "github.com/OpenListTeam/OpenList/v4/internal/cache" @@ -20,6 +21,7 @@ import ( "github.com/OpenListTeam/OpenList/v4/internal/model" "github.com/OpenListTeam/OpenList/v4/internal/op" "github.com/OpenListTeam/OpenList/v4/internal/setting" + "github.com/OpenListTeam/OpenList/v4/pkg/utils" "github.com/go-resty/resty/v2" log "github.com/sirupsen/logrus" ) @@ -202,17 +204,49 @@ func (d *GuangYaPanShare) link(ctx context.Context, file model.Obj, args model.L return nil, err } + // 临时文件无论最终走 123 还是原链,都延迟清理。 + go d.delete(ctx, restored, account) + + // 123 优先:开关开启时,用 restore 后文件的 md5 秒传到 123;成功返回 123 直链,失败回退原链。 + if setting.GetBool(conf.GuangyaTo123) { + if link := rapidGuangyaTo123(ctx, account, restored); link != nil { + return link, nil + } + } + link, err := account.Link(ctx, restored, args) if err != nil { log.Infof("link error: %v", err) return nil, err } - go d.delete(ctx, restored, account) - return link, nil } +// rapidGuangyaTo123 用 restore 后文件的 md5 秒传到 123。声明为 var 便于单测替换。 +var rapidGuangyaTo123 = func(ctx context.Context, account *guangyapan.GuangYaPan, restored model.Obj) *model.Link { + if account == nil || restored == nil { + return nil + } + md5, err := account.GetFileDetail(ctx, restored.GetID()) + if err != nil || len(md5) != utils.MD5.Width { + log.Debugf("[guangya-share] rapid to 123 skipped (no md5): %v", err) + return nil + } + link, err := _123rapid.RapidTo123(ctx, _123rapid.Source{ + HashType: utils.MD5, + Hash: md5, + Name: restored.GetName(), + Size: restored.GetSize(), + }) + if err != nil || link == nil { + log.Debugf("[guangya-share] rapid to 123 skipped: %v", err) + return nil + } + log.Infof("[guangya-share] 使用123秒传直链: %s", restored.GetName()) + return link +} + func (d *GuangYaPanShare) delete(ctx context.Context, file model.Obj, bd *guangyapan.GuangYaPan) { delayTime := setting.GetInt(conf.DeleteDelayTime, 900) if delayTime == 0 { diff --git a/drivers/quark_uc_share/types.go b/drivers/quark_uc_share/types.go index 88cd673b5..8d8c66863 100755 --- a/drivers/quark_uc_share/types.go +++ b/drivers/quark_uc_share/types.go @@ -71,6 +71,7 @@ type DownResp struct { Resp Data []struct { DownloadUrl string `json:"download_url"` + Md5 string `json:"md5"` } `json:"data"` } diff --git a/drivers/quark_uc_share/util.go b/drivers/quark_uc_share/util.go index bffd2bf29..bd9544c18 100755 --- a/drivers/quark_uc_share/util.go +++ b/drivers/quark_uc_share/util.go @@ -9,6 +9,7 @@ import ( "strings" "time" + _123rapid "github.com/OpenListTeam/OpenList/v4/drivers/123_rapid" quark "github.com/OpenListTeam/OpenList/v4/drivers/quark_uc" "github.com/OpenListTeam/OpenList/v4/drivers/quark_uc_tv" "github.com/OpenListTeam/OpenList/v4/internal/conf" @@ -16,6 +17,7 @@ import ( "github.com/OpenListTeam/OpenList/v4/internal/op" "github.com/OpenListTeam/OpenList/v4/internal/setting" "github.com/OpenListTeam/OpenList/v4/pkg/cookie" + "github.com/OpenListTeam/OpenList/v4/pkg/utils" "github.com/go-resty/resty/v2" "github.com/OpenListTeam/OpenList/v4/drivers/base" @@ -92,6 +94,36 @@ func (d *QuarkUCShare) getDriverName() string { return name } +// rapidTo123Enabled 按驱动类型(夸克/UC)选对应的「秒传到123」开关。 +func rapidTo123Enabled(d *QuarkUCShare) bool { + if d == nil { + return false + } + if d.getDriverName() == "UC" { + return setting.GetBool(conf.UCTo123) + } + return setting.GetBool(conf.QuarkTo123) +} + +// rapidQuarkUCTo123 按 MD5 把夸克/UC 文件秒传到 123。声明为 var 便于单测替换。 +var rapidQuarkUCTo123 = func(name, md5 string, size int64) *model.Link { + if len(md5) != utils.MD5.Width { + return nil + } + link, err := _123rapid.RapidTo123(context.Background(), _123rapid.Source{ + HashType: utils.MD5, + Hash: md5, + Name: name, + Size: size, + }) + if err != nil || link == nil { + log.Debugf("[quark-uc-share] rapid to 123 skipped: %v", err) + return nil + } + log.Infof("[quark-uc-share] 使用123秒传直链: %s", name) + return link +} + func (d *QuarkUCShare) request(pathname string, method string, callback base.ReqCallback, resp interface{}) ([]byte, error) { name := d.getDriverName() driver := op.GetFirstDriver(name, idx) @@ -616,6 +648,16 @@ var resolveShareDirectLink = func(d *QuarkUCShare, file model.Obj) (*model.Link, return nil, errors.New("empty share download url") } downloadUrl := resp.Data[0].DownloadUrl + // 123 优先:开关开启且响应带回 md5 时,按 MD5 秒传到 123;失败/未命中回退下面的免转存直链。 + if rapidTo123Enabled(d) { + md5 := "" + if len(resp.Data) > 0 { + md5 = resp.Data[0].Md5 + } + if link := rapidQuarkUCTo123(file.GetName(), md5, file.GetSize()); link != nil { + return link, nil + } + } // 后端代理用此 Header(魔法 Referer)绕过 checkplay。 header := http.Header{ "User-Agent": []string{d.conf.ua}, diff --git a/internal/bootstrap/data/setting.go b/internal/bootstrap/data/setting.go index ce5aa0db4..2879a0d7c 100644 --- a/internal/bootstrap/data/setting.go +++ b/internal/bootstrap/data/setting.go @@ -254,6 +254,11 @@ func InitialSettings() []model.SettingItem { {Key: conf.DeleteDelayTime, Value: "900", Type: conf.TypeNumber, Group: model.SINGLE, Flag: model.PRIVATE}, {Key: conf.ExternalPort, Value: "5344", Type: conf.TypeNumber, Group: model.SINGLE, Flag: model.PRIVATE}, {Key: conf.AliTo115, Value: "false", Type: conf.TypeBool, Group: model.SINGLE, Flag: model.PRIVATE}, + {Key: conf.AliTo123, Value: "false", Type: conf.TypeBool, Group: model.SINGLE, Flag: model.PRIVATE}, + {Key: conf.Pan115To123, Value: "false", Type: conf.TypeBool, Group: model.SINGLE, Flag: model.PRIVATE}, + {Key: conf.QuarkTo123, Value: "false", Type: conf.TypeBool, Group: model.SINGLE, Flag: model.PRIVATE}, + {Key: conf.UCTo123, Value: "false", Type: conf.TypeBool, Group: model.SINGLE, Flag: model.PRIVATE}, + {Key: conf.GuangyaTo123, Value: "false", Type: conf.TypeBool, Group: model.SINGLE, Flag: model.PRIVATE}, {Key: conf.DriverRoundRobin, Value: "false", Type: conf.TypeBool, Group: model.SINGLE, Flag: model.PRIVATE}, {Key: conf.UssQuarkTv, Value: "true", Type: conf.TypeBool, Group: model.SINGLE, Flag: model.PRIVATE}, {Key: "ali_lazy_load", Value: "true", Type: conf.TypeBool, Group: model.SINGLE, Flag: model.PRIVATE}, diff --git a/internal/conf/const.go b/internal/conf/const.go index 27f9ddfdf..f28ac855f 100644 --- a/internal/conf/const.go +++ b/internal/conf/const.go @@ -111,12 +111,18 @@ const ( UC = "UC" PAN115 = "PAN115" OPEN115 = "OPEN115" + OPEN123 = "OPEN123" PAN139 = "PAN139" CLOUD189 = "CLOUD189" BAIDU = "BAIDU" THUNDER = "THUNDER" UC_TV = "UC_TV" AliTo115 = "ali_to_115" + AliTo123 = "ali_to_123" + Pan115To123 = "115_to_123" + QuarkTo123 = "quark_to_123" + UCTo123 = "uc_to_123" + GuangyaTo123 = "guangya_to_123" AliAccountId = "ali_account_id" TempDirName = "alist-tvbox-temp" OfflineDirName = "alist-tvbox-offline" From 67d25f1dece12227f889643fcf77e9c30509b589 Mon Sep 17 00:00:00 2001 From: Har01d Date: Wed, 29 Jul 2026 12:59:46 +0800 Subject: [PATCH 2/6] =?UTF-8?q?123=E7=A7=92=E4=BC=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- drivers/123_open/types.go | 5 +++-- drivers/123_rapid/anon.go | 7 ++++--- drivers/123_share/util.go | 5 +++-- 3 files changed, 10 insertions(+), 7 deletions(-) diff --git a/drivers/123_open/types.go b/drivers/123_open/types.go index b2fbaa458..95519f9c4 100644 --- a/drivers/123_open/types.go +++ b/drivers/123_open/types.go @@ -229,7 +229,8 @@ type ShareCreateResp struct { BaseResp Data struct { ShareKey string `json:"shareKey"` - ShareID string `json:"shareID"` - SharePwd string `json:"sharePwd"` + // ShareID 接口返回数字,用 interface{} 兼容(本仓库未使用该字段)。 + ShareID interface{} `json:"shareID"` + SharePwd string `json:"sharePwd"` } `json:"data"` } diff --git a/drivers/123_rapid/anon.go b/drivers/123_rapid/anon.go index c25e5a578..bd05b311f 100644 --- a/drivers/123_rapid/anon.go +++ b/drivers/123_rapid/anon.go @@ -16,10 +16,11 @@ import ( jsoniter "github.com/json-iterator/go" ) -// 匿名(免登录)通道:www.123pan.cn 的分享接口对公开分享可直接列目录/换直链, -// 无需 Bearer/auth-key。常量与 drivers/123_share 对齐。 +// 匿名(免登录)通道:yun.123pan.com 的分享接口对公开分享可直接列目录/换直链, +// 无需 Bearer/auth-key。www.123pan.cn 已不再对该 API 提供 HTTPS(握手返回明文 HTTP), +// 故改用 yun.123pan.com(实测匿名 share/get、share/download/info 均 code:0)。 const ( - AnonOrigin = "https://www.123pan.cn" + AnonOrigin = "https://yun.123pan.com" AnonShareList = AnonOrigin + "/b/api/share/get" AnonDownloadInfo = AnonOrigin + "/b/api/share/download/info" AnonUA = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.0.0 Safari/537.36" diff --git a/drivers/123_share/util.go b/drivers/123_share/util.go index a5ac4e1ad..e4ae95d60 100644 --- a/drivers/123_share/util.go +++ b/drivers/123_share/util.go @@ -33,8 +33,9 @@ const ( DownloadInfo = MainApi + "/share/download/info" //AuthKeySalt = "8-8D$sL8gPjom7bk#cY" - // 匿名(免登录)通道:www.123pan.cn 的分享接口对公开分享可直接换直链,无需 Bearer/auth-key。 - AnonOrigin = "https://www.123pan.cn" + // 匿名(免登录)通道:yun.123pan.com 的分享接口对公开分享可直接换直链,无需 Bearer/auth-key。 + // www.123pan.cn 已不再对该 API 提供 HTTPS(握手返回明文 HTTP),故改用 yun.123pan.com。 + AnonOrigin = "https://yun.123pan.com" AnonDownloadInfo = AnonOrigin + "/b/api/share/download/info" AnonUA = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.0.0 Safari/537.36" ) From 1e984f2e228417bfc80612c5a45af42270ba4e45 Mon Sep 17 00:00:00 2001 From: Har01d Date: Wed, 29 Jul 2026 13:13:37 +0800 Subject: [PATCH 3/6] =?UTF-8?q?123=E7=A7=92=E4=BC=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- drivers/123_open/util.go | 11 ++++++ drivers/123_rapid/rapid.go | 56 ++++++++++++++++++++++-------- drivers/guangyapan/driver.go | 66 ++++++++++++++++++++++++++++++++---- 3 files changed, 111 insertions(+), 22 deletions(-) diff --git a/drivers/123_open/util.go b/drivers/123_open/util.go index 8f38f70e6..d14954264 100644 --- a/drivers/123_open/util.go +++ b/drivers/123_open/util.go @@ -177,6 +177,17 @@ func (d *Open123) getDownloadInfo(fileId int64) (*DownloadInfoResp, error) { return &resp, nil } +// DownloadURL 取账号个人盘文件的下载直链(/api/v1/file/download_info,鉴权)。 +// 供 drivers/123_rapid 在免登录分享直链被 5112(分享流量包不足)挡住时回退使用—— +// 个人文件下载不走分享流量,不受 5112 限制。 +func (d *Open123) DownloadURL(fileID int64) (string, error) { + resp, err := d.getDownloadInfo(fileID) + if err != nil { + return "", err + } + return resp.Data.DownloadUrl, nil +} + func (d *Open123) getDirectLink(fileId int64) (*DirectLinkResp, error) { var resp DirectLinkResp diff --git a/drivers/123_rapid/rapid.go b/drivers/123_rapid/rapid.go index 28e467b36..e4d51e127 100644 --- a/drivers/123_rapid/rapid.go +++ b/drivers/123_rapid/rapid.go @@ -85,9 +85,9 @@ func RapidTo123(ctx context.Context, src Source) (*model.Link, error) { return nil, errors.New("rapid: invalid source (need name/size/hash)") } - // 命中缓存:分享仍有效就直接免登录解析。 + // 命中缓存:优先免登录分享直链,失败(如 5112)回退 Open 账号个人下载。 if e, ok := rapidCache.Get(src.cacheKey()); ok && e != nil { - if link, err := resolveAnonShareLink(e.shareKey, e.pwd, e.fileID); err == nil && link != nil { + if link := resolve123Link(firstOpen123(), e); link != nil { return link, nil } } @@ -124,23 +124,22 @@ func RapidTo123(ctx context.Context, src Source) (*model.Link, error) { continue } - share, err := open.CreateShare(fileID, src.Name, shareExpire) - if err != nil || share == nil || share.Data.ShareKey == "" { - lastErr = fmt.Errorf("rapid: create share failed: %w", err) - log.Warnf("[rapid-to-123] CreateShare failed (%s/%s): %v", open123DriverName, src.Name, err) - continue + // 建分享用于免登录解析;失败不致命(免费账号分享流量为 0 会 5112,改走个人下载)。 + entry := &rapidEntry{fileID: fileID} + if share, err := open.CreateShare(fileID, src.Name, shareExpire); err == nil && share != nil && share.Data.ShareKey != "" { + entry.shareKey = share.Data.ShareKey + entry.pwd = share.Data.SharePwd + } else { + log.Warnf("[rapid-to-123] CreateShare failed, will use personal download (%s): %v", src.Name, err) } - entry := &rapidEntry{fileID: fileID, shareKey: share.Data.ShareKey, pwd: share.Data.SharePwd} rapidCache.Set(src.cacheKey(), entry) - link, err := resolveAnonShareLink(entry.shareKey, entry.pwd, fileID) - if err != nil || link == nil { - lastErr = fmt.Errorf("rapid: anon resolve failed: %w", err) - log.Warnf("[rapid-to-123] anon resolve failed (%s/%s): %v", open123DriverName, src.Name, err) - return nil, lastErr + if link := resolve123Link(open, entry); link != nil { + log.Infof("[rapid-to-123] %s reuse hit (via share=%v)", src.Name, entry.shareKey != "") + return link, nil } - log.Infof("[rapid-to-123] %s reuse hit → share %s", src.Name, entry.shareKey) - return link, nil + lastErr = errors.New("rapid: resolve link failed (anon share + personal download)") + log.Warnf("[rapid-to-123] resolve failed (%s/%s)", open123DriverName, src.Name) } if lastErr != nil { return nil, lastErr @@ -148,6 +147,33 @@ func RapidTo123(ctx context.Context, src Source) (*model.Link, error) { return nil, errNoOpen123 } +// firstOpen123 取首个 123 Open 账号(缓存命中回退个人下载时用)。 +func firstOpen123() *_123_open.Open123 { + s := op.GetFirstDriver(open123DriverName, 0) + if s == nil { + return nil + } + open, _ := s.(*_123_open.Open123) + return open +} + +// resolve123Link 对齐 JS tryResolveA123PlayableUrl 的兜底链: +// 1) 免登录分享直链(share/download/info);2) 失败(含 5112 分享流量不足)回退 Open 账号个人 download_info(不走分享流量)。 +func resolve123Link(open *_123_open.Open123, entry *rapidEntry) *model.Link { + if entry.shareKey != "" { + if link, err := resolveAnonShareLink(entry.shareKey, entry.pwd, entry.fileID); err == nil && link != nil { + return link + } + } + if open != nil { + if dl, err := open.DownloadURL(entry.fileID); err == nil && dl != "" { + exp := 30 * time.Minute + return &model.Link{URL: dl, Expiration: &exp} + } + } + return nil +} + var ( errNoOpen123 = errors.New("rapid: 找不到 123 Open 账号") // errRapidMiss 123 未命中该 hash(非错误,调用方静默回退)。 diff --git a/drivers/guangyapan/driver.go b/drivers/guangyapan/driver.go index bd0cc27d5..d02807373 100644 --- a/drivers/guangyapan/driver.go +++ b/drivers/guangyapan/driver.go @@ -7,6 +7,7 @@ import ( "crypto/sha1" "encoding/base64" "encoding/hex" + "encoding/json" "errors" "fmt" "io" @@ -23,6 +24,7 @@ import ( "github.com/OpenListTeam/OpenList/v4/pkg/utils" "github.com/aliyun/aliyun-oss-go-sdk/oss" "github.com/go-resty/resty/v2" + log "github.com/sirupsen/logrus" ) type GuangYaPan struct { @@ -222,20 +224,70 @@ func (d *GuangYaPan) Link(ctx context.Context, file model.Obj, args model.LinkAr } // GetFileDetail 通过 /userres/v1/file/get_file_detail 读取文件 md5,用于跨盘秒传到 123。 +// 光鸭 detail 返回结构多变(md5 可能挂在 fileInfo/data 不同层级或不同键名),故取原始响应 +// 递归查找 32 位 hex(md5);找不到时打 debug 日志便于诊断,调用方静默回退光鸭直链。 func (d *GuangYaPan) GetFileDetail(ctx context.Context, fileId string) (string, error) { - var resp fileDetailResp - if err := d.postAPI(ctx, "/userres/v1/file/get_file_detail", map[string]any{"fileId": fileId}, &resp); err != nil { + if strings.TrimSpace(d.AccessToken) == "" { + return "", errors.New("access token is empty") + } + resp, err := d.apiClient.R(). + SetContext(ctx). + SetHeader("Authorization", "Bearer "+d.AccessToken). + SetBody(map[string]any{"fileId": fileId}). + Post("/userres/v1/file/get_file_detail") + if err != nil { return "", err } - for _, h := range []string{resp.Data.FileInfo.Md5, resp.Data.FileInfo.Etag, resp.Data.FileInfo.Hash, resp.Data.Md5, resp.Data.Etag} { - h = strings.ToLower(strings.TrimSpace(h)) - if len(h) == utils.MD5.Width { - return h, nil - } + body := resp.Body() + log.Debugf("[guangya] get_file_detail(%s): %s", fileId, string(body)) + if md5 := findMd5InJSON(body); md5 != "" { + return md5, nil } return "", fmt.Errorf("file_detail 无可用 md5 (fileId=%s)", fileId) } +// findMd5InJSON 递归遍历 JSON,返回第一个 32 位小写 hex 字符串(光鸭 detail 里即为 md5; +// gcid/sha1 为 40 位,不会误匹配)。 +func findMd5InJSON(body []byte) string { + var v any + if err := json.Unmarshal(body, &v); err != nil { + return "" + } + var result string + var walk func(x any) + walk = func(x any) { + if result != "" { + return + } + switch t := x.(type) { + case map[string]any: + for _, val := range t { + walk(val) + } + case []any: + for _, val := range t { + walk(val) + } + case string: + s := strings.ToLower(strings.TrimSpace(t)) + if len(s) == utils.MD5.Width { + isHex := true + for _, c := range s { + if !((c >= '0' && c <= '9') || (c >= 'a' && c <= 'f')) { + isHex = false + break + } + } + if isHex { + result = s + } + } + } + } + walk(v) + return result +} + func (d *GuangYaPan) MakeDir(ctx context.Context, parentDir model.Obj, dirName string) error { if err := d.ensureAccessToken(ctx); err != nil { return err From 660c8c16efa05b7f4315935b752f9718c8d75476 Mon Sep 17 00:00:00 2001 From: Har01d Date: Wed, 29 Jul 2026 13:20:06 +0800 Subject: [PATCH 4/6] =?UTF-8?q?123=E7=A7=92=E4=BC=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- drivers/guangyapan/driver.go | 67 +++++------------------------------- 1 file changed, 8 insertions(+), 59 deletions(-) diff --git a/drivers/guangyapan/driver.go b/drivers/guangyapan/driver.go index d02807373..68b83e616 100644 --- a/drivers/guangyapan/driver.go +++ b/drivers/guangyapan/driver.go @@ -7,7 +7,6 @@ import ( "crypto/sha1" "encoding/base64" "encoding/hex" - "encoding/json" "errors" "fmt" "io" @@ -24,7 +23,6 @@ import ( "github.com/OpenListTeam/OpenList/v4/pkg/utils" "github.com/aliyun/aliyun-oss-go-sdk/oss" "github.com/go-resty/resty/v2" - log "github.com/sirupsen/logrus" ) type GuangYaPan struct { @@ -224,68 +222,19 @@ func (d *GuangYaPan) Link(ctx context.Context, file model.Obj, args model.LinkAr } // GetFileDetail 通过 /userres/v1/file/get_file_detail 读取文件 md5,用于跨盘秒传到 123。 -// 光鸭 detail 返回结构多变(md5 可能挂在 fileInfo/data 不同层级或不同键名),故取原始响应 -// 递归查找 32 位 hex(md5);找不到时打 debug 日志便于诊断,调用方静默回退光鸭直链。 +// md5 固定在 data.fileInfo.md5(大写 hex);取不到说明该文件无 md5,调用方静默回退光鸭直链。 func (d *GuangYaPan) GetFileDetail(ctx context.Context, fileId string) (string, error) { - if strings.TrimSpace(d.AccessToken) == "" { - return "", errors.New("access token is empty") - } - resp, err := d.apiClient.R(). - SetContext(ctx). - SetHeader("Authorization", "Bearer "+d.AccessToken). - SetBody(map[string]any{"fileId": fileId}). - Post("/userres/v1/file/get_file_detail") - if err != nil { + var resp fileDetailResp + if err := d.postAPI(ctx, "/userres/v1/file/get_file_detail", map[string]any{"fileId": fileId}, &resp); err != nil { return "", err } - body := resp.Body() - log.Debugf("[guangya] get_file_detail(%s): %s", fileId, string(body)) - if md5 := findMd5InJSON(body); md5 != "" { - return md5, nil - } - return "", fmt.Errorf("file_detail 无可用 md5 (fileId=%s)", fileId) -} - -// findMd5InJSON 递归遍历 JSON,返回第一个 32 位小写 hex 字符串(光鸭 detail 里即为 md5; -// gcid/sha1 为 40 位,不会误匹配)。 -func findMd5InJSON(body []byte) string { - var v any - if err := json.Unmarshal(body, &v); err != nil { - return "" - } - var result string - var walk func(x any) - walk = func(x any) { - if result != "" { - return - } - switch t := x.(type) { - case map[string]any: - for _, val := range t { - walk(val) - } - case []any: - for _, val := range t { - walk(val) - } - case string: - s := strings.ToLower(strings.TrimSpace(t)) - if len(s) == utils.MD5.Width { - isHex := true - for _, c := range s { - if !((c >= '0' && c <= '9') || (c >= 'a' && c <= 'f')) { - isHex = false - break - } - } - if isHex { - result = s - } - } + for _, h := range []string{resp.Data.FileInfo.Md5, resp.Data.Md5} { + h = strings.ToLower(strings.TrimSpace(h)) + if len(h) == utils.MD5.Width { + return h, nil } } - walk(v) - return result + return "", fmt.Errorf("file_detail 无可用 md5 (fileId=%s)", fileId) } func (d *GuangYaPan) MakeDir(ctx context.Context, parentDir model.Obj, dirName string) error { From 4aab0b9daf537383867dd4d3e4bd475eda6ce9d8 Mon Sep 17 00:00:00 2001 From: Har01d Date: Wed, 29 Jul 2026 19:04:49 +0800 Subject: [PATCH 5/6] =?UTF-8?q?123=E7=A7=92=E4=BC=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- drivers/123_open/driver.go | 8 ++++++-- drivers/123_open/upload.go | 6 ++++-- drivers/123_rapid/rapid.go | 3 +++ drivers/123_share/driver.go | 6 +++++- drivers/123_share/driver_test.go | 30 +++++++++++++++++++++++++++++- drivers/123_share/util.go | 23 +++++++++++++++++++++++ 6 files changed, 70 insertions(+), 6 deletions(-) diff --git a/drivers/123_open/driver.go b/drivers/123_open/driver.go index c9e860fa0..c207fff38 100644 --- a/drivers/123_open/driver.go +++ b/drivers/123_open/driver.go @@ -17,8 +17,9 @@ import ( type Open123 struct { model.Storage Addition - UID uint64 - tm *tokenManager + UID uint64 + TempDirId int64 // alist-tvbox-temp 目录 fileId,秒传文件落点 + tm *tokenManager } func (d *Open123) Config() driver.Config { @@ -60,6 +61,9 @@ func (d *Open123) Init(ctx context.Context) error { return fmt.Errorf("init get access token error: %w", err) } + // best-effort:秒传临时目录查找/创建,失败不影响驱动启动。 + d.GetTempFolder() + return nil } diff --git a/drivers/123_open/upload.go b/drivers/123_open/upload.go index 3511a4d4c..9a325fab4 100644 --- a/drivers/123_open/upload.go +++ b/drivers/123_open/upload.go @@ -205,18 +205,20 @@ func (d *Open123) sha1Reuse(parentFileID int64, filename string, sha1Hash string // Reuse 跨盘秒传入口(纯 hash,不下载/上传源文件),供 drivers/123_rapid 调用。 // hashType=SHA1(115) 走 sha1_reuse;hashType=MD5(夸克/UC/光鸭)走 file/create(etag=md5)。 // 返回 reuse=true 表示命中,fileID 为目标文件 id;reuse=false 表示未命中,调用方应回退。 +// 秒传文件落到 alist-tvbox-temp(d.TempDirId),便于延时清理;未就绪则落根目录。 func (d *Open123) Reuse(hashType *utils.HashType, hash, filename string, size int64, duplicate int) (reuse bool, fileID int64, err error) { if len(filename) == 0 { return false, 0, fmt.Errorf("reuse: empty filename") } + parent := d.TempDirId if hashType == utils.SHA1 { - resp, e := d.sha1Reuse(0, filename, hash, size, duplicate) + resp, e := d.sha1Reuse(parent, filename, hash, size, duplicate) if e != nil { return false, 0, e } return resp.Data.Reuse, resp.Data.FileID, nil } - resp, e := d.create(0, filename, hash, size, duplicate, false) + resp, e := d.create(parent, filename, hash, size, duplicate, false) if e != nil { return false, 0, e } diff --git a/drivers/123_rapid/rapid.go b/drivers/123_rapid/rapid.go index e4d51e127..c7dd34242 100644 --- a/drivers/123_rapid/rapid.go +++ b/drivers/123_rapid/rapid.go @@ -136,6 +136,9 @@ func RapidTo123(ctx context.Context, src Source) (*model.Link, error) { if link := resolve123Link(open, entry); link != nil { log.Infof("[rapid-to-123] %s reuse hit (via share=%v)", src.Name, entry.shareKey != "") + // 新鲜秒传文件已落到 alist-tvbox-temp,延时清理(仅新鲜路径排程; + // 缓存命中由首次创建者排程;DeleteDelayTime=0 不清理)。 + go open.DeleteDelay(entry.fileID) return link, nil } lastErr = errors.New("rapid: resolve link failed (anon share + personal download)") diff --git a/drivers/123_share/driver.go b/drivers/123_share/driver.go index e65371ac4..e468fd0f1 100644 --- a/drivers/123_share/driver.go +++ b/drivers/123_share/driver.go @@ -80,7 +80,11 @@ func (d *Pan123Share) Link(ctx context.Context, file model.Obj, args model.LinkA if link, err := resolveAnonLink(d, f, args.IP); err == nil { return link, nil } else if errors.Is(err, err123TrafficLimit) { - // 分享方流量耗尽,账号重试也无意义,直接返回真实原因。 + // 分享方流量耗尽:按 MD5(Etag)秒传到 123 Open 账号走个人下载(不走分享流量), + // 参考 drivers/123_rapid/rapid.go;失败则透传真实错误。 + if link := rapidShareTo123(f); link != nil { + return link, nil + } return nil, err } } diff --git a/drivers/123_share/driver_test.go b/drivers/123_share/driver_test.go index 4cc319dee..187aec554 100644 --- a/drivers/123_share/driver_test.go +++ b/drivers/123_share/driver_test.go @@ -36,13 +36,17 @@ func TestPan123ShareLink_AnonymousFirstReturnsAnonLink(t *testing.T) { } func TestPan123ShareLink_TrafficLimitShortCircuits(t *testing.T) { - // 5112 流量包不足:不回退账号,直接透传真实错误。 + // 5112 流量包不足:秒传兜底也未命中时,不回退账号,直接透传真实错误。 origAnon := resolveAnonLink resolveAnonLink = func(d *Pan123Share, f File, ip string) (*model.Link, error) { return nil, err123TrafficLimit } t.Cleanup(func() { resolveAnonLink = origAnon }) + origRapid := rapidShareTo123 + rapidShareTo123 = func(f File) *model.Link { return nil } + t.Cleanup(func() { rapidShareTo123 = origRapid }) + d := &Pan123Share{} file := File{FileName: "video.mp4"} @@ -51,3 +55,27 @@ func TestPan123ShareLink_TrafficLimitShortCircuits(t *testing.T) { t.Fatalf("expected err123TrafficLimit, got %v", err) } } + +func TestPan123ShareLink_TrafficLimitRapidFallback(t *testing.T) { + // 5112 时秒传兜底命中 → 返回秒传直链,不再透传错误。 + origAnon := resolveAnonLink + resolveAnonLink = func(d *Pan123Share, f File, ip string) (*model.Link, error) { + return nil, err123TrafficLimit + } + t.Cleanup(func() { resolveAnonLink = origAnon }) + + origRapid := rapidShareTo123 + rapidShareTo123 = func(f File) *model.Link { return &model.Link{URL: "https://example.com/rapid-123"} } + t.Cleanup(func() { rapidShareTo123 = origRapid }) + + d := &Pan123Share{} + file := File{FileName: "video.mp4"} + + link, err := d.Link(context.Background(), file, model.LinkArgs{}) + if err != nil { + t.Fatalf("link: %v", err) + } + if link == nil || link.URL != "https://example.com/rapid-123" { + t.Fatalf("expected rapid fallback link, got %+v", link) + } +} diff --git a/drivers/123_share/util.go b/drivers/123_share/util.go index e4ae95d60..51efb66a9 100644 --- a/drivers/123_share/util.go +++ b/drivers/123_share/util.go @@ -15,6 +15,7 @@ import ( "time" _123 "github.com/OpenListTeam/OpenList/v4/drivers/123" + _123rapid "github.com/OpenListTeam/OpenList/v4/drivers/123_rapid" "github.com/OpenListTeam/OpenList/v4/drivers/base" "github.com/OpenListTeam/OpenList/v4/internal/model" "github.com/OpenListTeam/OpenList/v4/internal/op" @@ -176,6 +177,28 @@ func unwrap123DownloadLink(downloadUrl string) (*model.Link, error) { return link, nil } +// rapidShareTo123 在分享方流量耗尽(err123TrafficLimit, 5112)时,按 MD5(Etag)把 123 分享文件 +// 秒传到 123 Open 账号并取个人下载直链(不走分享流量)。声明为 var 便于单测替换。 +// 参考 drivers/123_rapid/rapid.go(RapidTo123)与 drivers/quark_uc_share/util.go:rapidQuarkUCTo123; +// 秒传文件落 alist-tvbox-temp 并延时清理(见 drivers/123_open/extension.go)。 +var rapidShareTo123 = func(f File) *model.Link { + if len(f.Etag) != utils.MD5.Width { + return nil + } + link, err := _123rapid.RapidTo123(context.Background(), _123rapid.Source{ + HashType: utils.MD5, + Hash: f.Etag, + Name: f.FileName, + Size: f.Size, + }) + if err != nil || link == nil { + log.Debugf("[123_share] rapid to 123 skipped: %v", err) + return nil + } + log.Infof("[123_share] 使用123秒传直链(5112 兜底): %s", f.FileName) + return link +} + // anonDownloadLink 匿名换取 123 分享直链(无需 123Pan 账号)。 // 返回 err123TrafficLimit 表示分享方流量耗尽,调用方不应再回退账号重试。 func (d *Pan123Share) anonDownloadLink(f File, ip string) (*model.Link, error) { From c992ac38c1797b72e87571c6b3aa6a8253a8bba4 Mon Sep 17 00:00:00 2001 From: Har01d Date: Wed, 29 Jul 2026 20:09:51 +0800 Subject: [PATCH 6/6] =?UTF-8?q?123=E7=A7=92=E4=BC=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- drivers/123_open/extension.go | 117 ++++++++++++++++++++++++++++++++++ 1 file changed, 117 insertions(+) create mode 100644 drivers/123_open/extension.go diff --git a/drivers/123_open/extension.go b/drivers/123_open/extension.go new file mode 100644 index 000000000..2e9f865ac --- /dev/null +++ b/drivers/123_open/extension.go @@ -0,0 +1,117 @@ +package _123_open + +import ( + "time" + + "github.com/OpenListTeam/OpenList/v4/internal/conf" + "github.com/OpenListTeam/OpenList/v4/internal/setting" + log "github.com/sirupsen/logrus" +) + +// 临时目录与延时清理,镜像 drivers/quark_uc/extension.go + drivers/quark_uc_share deleteDelay。 +// 跨盘秒传(drivers/123_rapid)产生的文件落到 alist-tvbox-temp,播放后延时移入回收站, +// 避免占满 123 Open 账号存储。 + +// GetTempFolder 查找/创建根目录下的 alist-tvbox-temp 临时目录,记录其 fileId。 +// 找到则清理上次崩溃遗留;找不到则新建。Init 时 best-effort 调用。 +func (d *Open123) GetTempFolder() { + if id, ok := d.findTempFolder(); ok { + d.TempDirId = id + log.Infof("[%d] 123 Open temp folder id: %d", d.ID, d.TempDirId) + d.CleanTempFolder() + return + } + d.CreateTempFolder() +} + +// CreateTempFolder 在根目录创建 alist-tvbox-temp 并记录其 fileId。 +func (d *Open123) CreateTempFolder() { + if err := d.mkdir(0, conf.TempDirName); err != nil { + // 已存在等情形:重列根目录兜底找回。 + log.Warnf("[%d] create 123 Open temp folder error: %v", d.ID, err) + } + if id, ok := d.findTempFolder(); ok { + d.TempDirId = id + log.Infof("[%d] create 123 Open temp folder id: %d", d.ID, d.TempDirId) + } +} + +// CleanTempFolder 清理 temp 目录下残留文件(上次崩溃/未及删除的遗留)。 +func (d *Open123) CleanTempFolder() { + if d.TempDirId == 0 { + return + } + files, err := d.listAllFiles(d.TempDirId) + if err != nil { + log.Warnf("[%d] list 123 Open temp folder error: %v", d.ID, err) + return + } + for _, f := range files { + go d.DeleteFile(f.FileId) + } +} + +// DeleteFile 将临时文件移入回收站(123 Open API 仅暴露 trash,无永久删除)。 +func (d *Open123) DeleteFile(fileID int64) { + if fileID <= 0 { + return + } + if err := d.trash(fileID); err != nil { + log.Warnf("[%d] delete 123 Open temp file %d failed: %v", d.ID, fileID, err) + } +} + +// DeleteDelay 延时删除秒传产生的临时文件。镜像 quark_uc_share deleteDelay。 +// delayTime=0 关闭清理(退回不清理旧行为);<5 钳到 5 秒。 +func (d *Open123) DeleteDelay(fileID int64) { + if fileID <= 0 { + return + } + delayTime := setting.GetInt(conf.DeleteDelayTime, 900) + if delayTime == 0 { + return + } + if delayTime < 5 { + delayTime = 5 + } + log.Infof("[%d] Delete 123 Open temp file %d after %d seconds.", d.ID, fileID, delayTime) + time.Sleep(time.Duration(delayTime) * time.Second) + d.DeleteFile(fileID) +} + +// findTempFolder 在根目录查找名为 alist-tvbox-temp 的非回收文件夹。 +func (d *Open123) findTempFolder() (int64, bool) { + files, err := d.listAllFiles(0) + if err != nil { + log.Warnf("[%d] list 123 Open root error: %v", d.ID, err) + return 0, false + } + for _, f := range files { + if f.IsDir() && f.FileName == conf.TempDirName { + return f.FileId, true + } + } + return 0, false +} + +// listAllFiles 列出 parentFileId 下全部非回收文件(分页直到 LastFileId==-1)。 +func (d *Open123) listAllFiles(parentFileId int64) ([]File, error) { + out := make([]File, 0, 64) + last := int64(0) + for page := 0; page < 1000; page++ { + resp, err := d.getFiles(parentFileId, 100, last) + if err != nil { + return nil, err + } + for _, f := range resp.Data.FileList { + if f.Trashed == 0 { + out = append(out, f) + } + } + if resp.Data.LastFileId == -1 || resp.Data.LastFileId == last { + break + } + last = resp.Data.LastFileId + } + return out, nil +}