Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 28 additions & 1 deletion drivers/115_share/driver.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"

Expand Down Expand Up @@ -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++ {
Expand All @@ -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++
Expand Down
16 changes: 14 additions & 2 deletions drivers/123_open/driver.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -38,6 +39,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 不可靠
Expand All @@ -52,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
}

Expand Down
117 changes: 117 additions & 0 deletions drivers/123_open/extension.go
Original file line number Diff line number Diff line change
@@ -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
}
6 changes: 6 additions & 0 deletions drivers/123_open/meta.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"`
Expand Down
62 changes: 62 additions & 0 deletions drivers/123_open/token.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
Expand Down Expand Up @@ -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
Expand Down
11 changes: 11 additions & 0 deletions drivers/123_open/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -223,3 +223,14 @@ 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 接口返回数字,用 interface{} 兼容(本仓库未使用该字段)。
ShareID interface{} `json:"shareID"`
SharePwd string `json:"sharePwd"`
} `json:"data"`
}
23 changes: 23 additions & 0 deletions drivers/123_open/upload.go
Original file line number Diff line number Diff line change
Expand Up @@ -201,3 +201,26 @@ 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 表示未命中,调用方应回退。
// 秒传文件落到 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(parent, filename, hash, size, duplicate)
if e != nil {
return false, 0, e
}
return resp.Data.Reuse, resp.Data.FileID, nil
}
resp, e := d.create(parent, filename, hash, size, duplicate, false)
if e != nil {
return false, 0, e
}
return resp.Data.Reuse, resp.Data.FileID, nil
}
32 changes: 32 additions & 0 deletions drivers/123_open/util.go
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -176,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

Expand Down Expand Up @@ -246,6 +258,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,
Expand Down
Loading
Loading