mirror of
https://github.com/OpenListTeam/OpenList.git
synced 2026-10-10 21:13:10 +08:00
Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 136e107463 | |||
| 7b4d4454a8 | |||
| 23164ac7f2 |
@@ -124,7 +124,7 @@ jobs:
|
||||
FRONTEND_REPO: ${{ vars.FRONTEND_REPO }}
|
||||
|
||||
- name: Build
|
||||
uses: OpenListTeam/cgo-actions@d760a8ec1a6be1f8ec181229e11cb2671797f9e0 # v1.3.0
|
||||
uses: OpenListTeam/cgo-actions@6fcace5934c36d70503dba06e2396bb58b766130 # v1.2.5
|
||||
with:
|
||||
targets: ${{ matrix.target }}
|
||||
flags: ${{ matrix.flags || '-ldflags=' }}
|
||||
@@ -136,7 +136,7 @@ jobs:
|
||||
musl-base-url: "https://github.com/OpenListTeam/musl-compilers/releases/latest/download/"
|
||||
x-flags: |
|
||||
github.com/OpenListTeam/OpenList/v4/internal/conf.BuiltAt=$built_at
|
||||
github.com/OpenListTeam/OpenList/v4/internal/conf.GitAuthor=The OpenList Projects Contributors <noreply@oplist.org>
|
||||
github.com/OpenListTeam/OpenList/v4/internal/conf.GitAuthor=The OpenList Projects Contributors <noreply@openlist.team>
|
||||
github.com/OpenListTeam/OpenList/v4/internal/conf.GitCommit=$git_commit
|
||||
github.com/OpenListTeam/OpenList/v4/internal/conf.Version=$tag
|
||||
github.com/OpenListTeam/OpenList/v4/internal/conf.WebVersion=rolling
|
||||
|
||||
@@ -42,7 +42,7 @@ jobs:
|
||||
FRONTEND_REPO: ${{ vars.FRONTEND_REPO }}
|
||||
|
||||
- name: Build
|
||||
uses: OpenListTeam/cgo-actions@d760a8ec1a6be1f8ec181229e11cb2671797f9e0 # v1.3.0
|
||||
uses: OpenListTeam/cgo-actions@6fcace5934c36d70503dba06e2396bb58b766130 # v1.2.5
|
||||
with:
|
||||
targets: ${{ matrix.target }}
|
||||
flags: ${{ contains(matrix.target, '-musl') && '-ldflags=-linkmode external -extldflags ''-static -fpic''' || '-ldflags=' }}
|
||||
@@ -52,7 +52,7 @@ jobs:
|
||||
out-dir: build
|
||||
x-flags: |
|
||||
github.com/OpenListTeam/OpenList/v4/internal/conf.BuiltAt=$built_at
|
||||
github.com/OpenListTeam/OpenList/v4/internal/conf.GitAuthor=The OpenList Projects Contributors <noreply@oplist.org>
|
||||
github.com/OpenListTeam/OpenList/v4/internal/conf.GitAuthor=The OpenList Projects Contributors <noreply@openlist.team>
|
||||
github.com/OpenListTeam/OpenList/v4/internal/conf.GitCommit=$git_commit
|
||||
github.com/OpenListTeam/OpenList/v4/internal/conf.Version=$tag
|
||||
github.com/OpenListTeam/OpenList/v4/internal/conf.WebVersion=rolling
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
set -e
|
||||
appName="openlist"
|
||||
builtAt="$(date +'%F %T %z')"
|
||||
gitAuthor="The OpenList Projects Contributors <noreply@oplist.org>"
|
||||
gitAuthor="The OpenList Projects Contributors <noreply@openlist.team>"
|
||||
gitCommit=$(git log --pretty=format:"%h" -1)
|
||||
|
||||
# Set frontend repository, default to OpenListTeam/OpenList-Frontend
|
||||
@@ -531,8 +531,8 @@ BuildReleaseFreeBSD() {
|
||||
sed 's/\.0$//')
|
||||
|
||||
if [ -z "$freebsd_version" ]; then
|
||||
echo "Failed to get FreeBSD version, falling back to 14.4"
|
||||
freebsd_version="14.4"
|
||||
echo "Failed to get FreeBSD version, falling back to 14.3"
|
||||
freebsd_version="14.3"
|
||||
fi
|
||||
|
||||
echo "Using FreeBSD version: $freebsd_version"
|
||||
|
||||
+16
-61
@@ -3,7 +3,6 @@ package _115_open
|
||||
import (
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"errors"
|
||||
"io"
|
||||
"time"
|
||||
|
||||
@@ -71,19 +70,6 @@ func (d *Open115) singleUpload(ctx context.Context, tempF model.File, tokenResp
|
||||
// } `json:"data"`
|
||||
// }
|
||||
|
||||
// retryExpiredToken retries only the rejected OSS operation, preserving the upload ID.
|
||||
func retryExpiredToken(refresh func() error, operation func() error) error {
|
||||
err := operation()
|
||||
var serviceErr oss.ServiceError
|
||||
if !errors.As(err, &serviceErr) || serviceErr.Code != "SecurityTokenExpired" {
|
||||
return err
|
||||
}
|
||||
if err := refresh(); err != nil {
|
||||
return err
|
||||
}
|
||||
return operation()
|
||||
}
|
||||
|
||||
func (d *Open115) multpartUpload(ctx context.Context, stream model.FileStreamer, up driver.UpdateProgress, tokenResp *sdk.UploadGetTokenResp, initResp *sdk.UploadInitResp) error {
|
||||
ossClient, err := netutil.NewOSSClient(tokenResp.Endpoint, tokenResp.AccessKeyId, tokenResp.AccessKeySecret, oss.SecurityToken(tokenResp.SecurityToken))
|
||||
if err != nil {
|
||||
@@ -94,32 +80,7 @@ func (d *Open115) multpartUpload(ctx context.Context, stream model.FileStreamer,
|
||||
return err
|
||||
}
|
||||
|
||||
refresh := func() error {
|
||||
if err := d.WaitLimit(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
token, err := d.client.UploadGetToken(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
client, err := netutil.NewOSSClient(token.Endpoint, token.AccessKeyId, token.AccessKeySecret, oss.SecurityToken(token.SecurityToken))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
newBucket, err := client.Bucket(initResp.Bucket)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
bucket = newBucket
|
||||
return nil
|
||||
}
|
||||
|
||||
var imur oss.InitiateMultipartUploadResult
|
||||
err = retryExpiredToken(refresh, func() error {
|
||||
var err error
|
||||
imur, err = bucket.InitiateMultipartUpload(initResp.Object, oss.Sequential(), oss.WithContext(ctx))
|
||||
return err
|
||||
})
|
||||
imur, err := bucket.InitiateMultipartUpload(initResp.Object, oss.Sequential())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -148,17 +109,13 @@ func (d *Open115) multpartUpload(ctx context.Context, stream model.FileStreamer,
|
||||
return err
|
||||
}
|
||||
err = retry.Do(func() error {
|
||||
return retryExpiredToken(refresh, func() error {
|
||||
if _, err := rd.Seek(0, io.SeekStart); err != nil {
|
||||
return err
|
||||
}
|
||||
part, err := bucket.UploadPart(imur, driver.NewLimitedUploadStream(ctx, rd), partSize, int(i), oss.WithContext(ctx))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
parts[i-1] = part
|
||||
return nil
|
||||
})
|
||||
rd.Seek(0, io.SeekStart)
|
||||
part, err := bucket.UploadPart(imur, driver.NewLimitedUploadStream(ctx, rd), partSize, int(i))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
parts[i-1] = part
|
||||
return nil
|
||||
},
|
||||
retry.Context(ctx),
|
||||
retry.Attempts(3),
|
||||
@@ -177,16 +134,14 @@ func (d *Open115) multpartUpload(ctx context.Context, stream model.FileStreamer,
|
||||
up(float64(offset) * 100 / float64(fileSize))
|
||||
}
|
||||
|
||||
err = retryExpiredToken(refresh, func() error {
|
||||
_, err := bucket.CompleteMultipartUpload(
|
||||
imur,
|
||||
parts,
|
||||
oss.Callback(base64.StdEncoding.EncodeToString([]byte(initResp.Callback.Value.Callback))),
|
||||
oss.CallbackVar(base64.StdEncoding.EncodeToString([]byte(initResp.Callback.Value.CallbackVar))),
|
||||
oss.WithContext(ctx),
|
||||
)
|
||||
return err
|
||||
})
|
||||
// callbackRespBytes := make([]byte, 1024)
|
||||
_, err = bucket.CompleteMultipartUpload(
|
||||
imur,
|
||||
parts,
|
||||
oss.Callback(base64.StdEncoding.EncodeToString([]byte(initResp.Callback.Value.Callback))),
|
||||
oss.CallbackVar(base64.StdEncoding.EncodeToString([]byte(initResp.Callback.Value.CallbackVar))),
|
||||
// oss.CallbackResult(&callbackRespBytes),
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -7,16 +7,11 @@ import (
|
||||
|
||||
type Addition struct {
|
||||
LoginType string `json:"login_type" type:"select" options:"password,qrcode" default:"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"`
|
||||
Username string `json:"username" required:"true"`
|
||||
Password string `json:"password" required:"true"`
|
||||
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"`
|
||||
|
||||
@@ -72,8 +72,6 @@ type BaseLoginParam struct {
|
||||
// 请求头参数
|
||||
Lt string
|
||||
ReqId string
|
||||
// logbox页面地址,作为后续请求的Referer,缺失会被判定为陌生设备
|
||||
Referer string
|
||||
|
||||
// 表单参数
|
||||
ParamId string
|
||||
@@ -99,20 +97,10 @@ 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"`
|
||||
@@ -128,35 +116,6 @@ 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返回
|
||||
@@ -190,27 +149,6 @@ 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"`
|
||||
|
||||
+77
-355
File diff suppressed because it is too large
Load Diff
@@ -328,6 +328,7 @@ func (d *Alias) Link(ctx context.Context, file model.Obj, args model.LinkArgs) (
|
||||
return nil, err
|
||||
}
|
||||
resultLink := link.Clone() // 复制一份,避免修改到原始link
|
||||
resultLink.Expiration = nil
|
||||
if args.Redirect {
|
||||
return resultLink, nil
|
||||
}
|
||||
|
||||
@@ -95,7 +95,6 @@ func (d *AListV3) List(ctx context.Context, dir model.Obj, args model.ListArgs)
|
||||
file := model.ObjThumb{
|
||||
Object: model.Object{
|
||||
Name: f.Name,
|
||||
Path: path.Join(dir.GetPath(), f.Name),
|
||||
Modified: f.Modified,
|
||||
Ctime: f.Created,
|
||||
Size: f.Size,
|
||||
|
||||
@@ -1,65 +0,0 @@
|
||||
package alist_v3
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/OpenListTeam/OpenList/v4/drivers/base"
|
||||
"github.com/OpenListTeam/OpenList/v4/internal/driver"
|
||||
"github.com/OpenListTeam/OpenList/v4/internal/model"
|
||||
"github.com/go-resty/resty/v2"
|
||||
)
|
||||
|
||||
// TestListSetsChildPaths descends two levels the way op.Get does, feeding an
|
||||
// object from one listing back into List as dir. Without a Path on that object
|
||||
// the driver asks upstream for "", which a real server answers with its own
|
||||
// root -- hence the fake upstream's fallback, and the endless self-similar tree.
|
||||
func TestListSetsChildPaths(t *testing.T) {
|
||||
tree := map[string][]ObjResp{
|
||||
"/": {{Name: "root-marker", IsDir: true}},
|
||||
"/drive": {{Name: "concerts", IsDir: true}},
|
||||
"/drive/concerts": {{Name: "show.mkv"}},
|
||||
}
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
var req ListReq
|
||||
_ = json.NewDecoder(r.Body).Decode(&req)
|
||||
content, ok := tree[req.Path]
|
||||
if !ok {
|
||||
content = tree["/"]
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json") // resty only unmarshals JSON
|
||||
_ = json.NewEncoder(w).Encode(map[string]any{
|
||||
"code": 200, "message": "success",
|
||||
"data": map[string]any{"content": content, "total": len(content)},
|
||||
})
|
||||
}))
|
||||
t.Cleanup(srv.Close)
|
||||
|
||||
// conf.Conf is nil outside a booted server, so base.InitClient() is unusable.
|
||||
prev := base.RestyClient
|
||||
base.RestyClient = resty.New().SetTimeout(5 * time.Second)
|
||||
t.Cleanup(func() { base.RestyClient = prev })
|
||||
|
||||
d := &AListV3{Addition: Addition{
|
||||
RootPath: driver.RootPath{RootFolderPath: "/drive"},
|
||||
Address: srv.URL,
|
||||
}}
|
||||
dir := model.Obj(&model.Object{Path: "/drive", IsFolder: true})
|
||||
for _, want := range []string{"/drive/concerts", "/drive/concerts/show.mkv"} {
|
||||
objs, err := d.List(context.Background(), dir, model.ListArgs{})
|
||||
if err != nil {
|
||||
t.Fatalf("List(%q): %v", dir.GetPath(), err)
|
||||
}
|
||||
if len(objs) != 1 {
|
||||
t.Fatalf("List(%q) returned %d objects, want 1", dir.GetPath(), len(objs))
|
||||
}
|
||||
if got := objs[0].GetPath(); got != want {
|
||||
t.Fatalf("child of %q has path %q, want %q", dir.GetPath(), got, want)
|
||||
}
|
||||
dir = objs[0]
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -11,7 +11,6 @@ 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"
|
||||
@@ -23,9 +22,8 @@ type AliyundriveOpen struct {
|
||||
|
||||
DriveId string
|
||||
|
||||
limiter *limiter
|
||||
ref *AliyundriveOpen
|
||||
callback *callbackRegistration
|
||||
limiter *limiter
|
||||
ref *AliyundriveOpen
|
||||
}
|
||||
|
||||
func (d *AliyundriveOpen) Config() driver.Config {
|
||||
@@ -37,7 +35,6 @@ 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"
|
||||
@@ -55,7 +52,6 @@ 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
|
||||
}
|
||||
|
||||
@@ -69,10 +65,6 @@ 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
|
||||
@@ -127,16 +119,10 @@ 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
|
||||
link := &model.Link{
|
||||
return &model.Link{
|
||||
URL: url,
|
||||
Expiration: &exp,
|
||||
}
|
||||
if args.Redirect {
|
||||
return link, nil
|
||||
}
|
||||
link.URL = ""
|
||||
link.RangeReader = stream.RateLimitRangeReaderFunc(d.callbackRangeReader(url, file.GetSize()))
|
||||
return link, nil
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (d *AliyundriveOpen) MakeDir(ctx context.Context, parentDir model.Obj, dirName string) (model.Obj, error) {
|
||||
|
||||
@@ -8,20 +8,19 @@ 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"`
|
||||
CallbackConcurrency int `json:"callback_concurrency" type:"number" default:"1" help:"Maximum active proxied downloads shared by Aliyun user"`
|
||||
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"`
|
||||
AccessToken string
|
||||
}
|
||||
|
||||
var config = driver.Config{
|
||||
|
||||
+8
-43
@@ -315,36 +315,6 @@ var findDownPageParamReg = regexp.MustCompile(`<iframe.*?src="(.+?)"`)
|
||||
// 获取文件ID
|
||||
var findFileIDReg = regexp.MustCompile(`'/ajax(?:file|m)\.php\?file=(\d+)'`)
|
||||
|
||||
// 2026-10 改版:文件页将下载参数移入 /fn? 内页,接口变为 apifile 绝对地址并携带签名
|
||||
var (
|
||||
fnDomainReg = regexp.MustCompile(`var\s+domain[12]\s*=\s*'([^']*(?:ajaxfile|ajaxm)\.php\?file=(\d+)[^']*)'`)
|
||||
fnSignReg = regexp.MustCompile(`var\s+wp_sign\s*=\s*'([^']*)'`)
|
||||
fnAjaxDataReg = regexp.MustCompile(`var\s+ajaxdata\s*=\s*'([^']*)'`)
|
||||
)
|
||||
|
||||
// parseFnPage 从改版后的 /fn? 内页提取下载接口地址与签名表单
|
||||
// 对应页面 JS:POST domain1 {'action':'downprocess','websignkey':ajaxdata,'signs':ajaxdata,'sign':wp_sign,'websign':'2','kd':kdns,'ves':1}
|
||||
func parseFnPage(pageData string) (string, map[string]string, error) {
|
||||
matches := fnDomainReg.FindStringSubmatch(pageData)
|
||||
if len(matches) < 3 {
|
||||
return "", nil, fmt.Errorf("not find fn ajax url")
|
||||
}
|
||||
sign := fnSignReg.FindStringSubmatch(pageData)
|
||||
ajaxdata := fnAjaxDataReg.FindStringSubmatch(pageData)
|
||||
if len(sign) < 2 || len(ajaxdata) < 2 {
|
||||
return "", nil, fmt.Errorf("not find fn sign")
|
||||
}
|
||||
return matches[1], map[string]string{
|
||||
"action": "downprocess",
|
||||
"websignkey": ajaxdata[1],
|
||||
"signs": ajaxdata[1],
|
||||
"sign": sign[1],
|
||||
"websign": "2",
|
||||
"kd": "1",
|
||||
"ves": "1",
|
||||
}, nil
|
||||
}
|
||||
|
||||
// 获取分享链接主界面
|
||||
func (d *LanZou) getShareUrlHtml(shareID string) (string, error) {
|
||||
var vs string
|
||||
@@ -467,23 +437,18 @@ func (d *LanZou) getFilesByShareUrl(shareID, pwd string, sharePageData string) (
|
||||
return nil, err
|
||||
}
|
||||
nextPageData := RemoveNotes(string(data))
|
||||
param, err = htmlJsonToMap(nextPageData)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var resp FileShareInfoAndUrlResp[int]
|
||||
matches := findFileIDReg.FindStringSubmatch(nextPageData)
|
||||
if len(matches) >= 2 {
|
||||
// 旧版结构:相对路径 /ajaxm.php?file=N
|
||||
param, err = htmlJsonToMap(nextPageData)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ajaxUrl := d.ShareUrl + matches[0][1:len(matches[0])-1]
|
||||
_, err = d.post(ajaxUrl, func(req *resty.Request) { req.SetFormData(param) }, &resp)
|
||||
} else if fnUrl, fnForm, ferr := parseFnPage(nextPageData); ferr == nil {
|
||||
// 2026-10 改版结构:/fn? 内页携带 apifile 绝对地址与签名参数
|
||||
_, err = d.post(fnUrl, func(req *resty.Request) { req.SetFormData(fnForm) }, &resp)
|
||||
} else {
|
||||
if len(matches) < 2 {
|
||||
return nil, fmt.Errorf("not find file id")
|
||||
}
|
||||
ajaxUrl := d.ShareUrl + matches[0][1:len(matches[0])-1]
|
||||
var resp FileShareInfoAndUrlResp[int]
|
||||
_, err = d.post(ajaxUrl, func(req *resty.Request) { req.SetFormData(param) }, &resp)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -10,11 +10,10 @@ type Addition struct {
|
||||
Username string `json:"username" required:"true"`
|
||||
Password string `json:"password" required:"true"`
|
||||
Platform string `json:"platform" required:"true" default:"web" type:"select" options:"android,web,pc"`
|
||||
RefreshToken string `json:"refresh_token" required:"false" default:""`
|
||||
RefreshToken string `json:"refresh_token" required:"true" default:""`
|
||||
CaptchaToken string `json:"captcha_token" default:""`
|
||||
DeviceID string `json:"device_id" required:"false" default:""`
|
||||
DisableMediaLink bool `json:"disable_media_link" default:"true"`
|
||||
SkipVerification bool `json:"skip_verification" default:"false" help:"ignore the human verification URL returned by the captcha API instead of failing; enabling this may trigger PikPak risk control"`
|
||||
}
|
||||
|
||||
var config = driver.Config{
|
||||
|
||||
+10
-30
@@ -100,13 +100,12 @@ func (d *PikPak) login() error {
|
||||
return errors.New("username or password is empty")
|
||||
}
|
||||
|
||||
// Clear expired access token so captcha requests don't carry a stale bearer
|
||||
d.AccessToken = ""
|
||||
|
||||
url := "https://user.mypikpak.net/v1/auth/signin"
|
||||
// Always refresh captcha token before signin (it may be expired)
|
||||
if err := d.RefreshCaptchaTokenInLogin(GetAction(http.MethodPost, url), d.Username); err != nil {
|
||||
return err
|
||||
// 使用 用户填写的 CaptchaToken —————— (验证后的captcha_token)
|
||||
if d.GetCaptchaToken() == "" {
|
||||
if err := d.RefreshCaptchaTokenInLogin(GetAction(http.MethodPost, url), d.Username); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
var e ErrResp
|
||||
@@ -126,12 +125,7 @@ func (d *PikPak) login() error {
|
||||
data := res.Body()
|
||||
d.RefreshToken = jsoniter.Get(data, "refresh_token").ToString()
|
||||
d.AccessToken = jsoniter.Get(data, "access_token").ToString()
|
||||
if d.AccessToken == "" || d.RefreshToken == "" {
|
||||
return errors.New("login failed: server returned empty tokens")
|
||||
}
|
||||
d.Common.SetUserID(jsoniter.Get(data, "sub").ToString())
|
||||
d.Addition.RefreshToken = d.RefreshToken
|
||||
op.MustSaveDriverStorage(d)
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -165,14 +159,9 @@ func (d *PikPak) refreshToken(refreshToken string) error {
|
||||
return errors.New(e.Error())
|
||||
}
|
||||
data := res.Body()
|
||||
newAccessToken := jsoniter.Get(data, "access_token").ToString()
|
||||
newRefreshToken := jsoniter.Get(data, "refresh_token").ToString()
|
||||
if newAccessToken == "" || newRefreshToken == "" {
|
||||
return errors.New("refresh failed: server returned empty tokens")
|
||||
}
|
||||
d.Status = "work"
|
||||
d.RefreshToken = newRefreshToken
|
||||
d.AccessToken = newAccessToken
|
||||
d.RefreshToken = jsoniter.Get(data, "refresh_token").ToString()
|
||||
d.AccessToken = jsoniter.Get(data, "access_token").ToString()
|
||||
d.Common.SetUserID(jsoniter.Get(data, "sub").ToString())
|
||||
d.Addition.RefreshToken = d.RefreshToken
|
||||
op.MustSaveDriverStorage(d)
|
||||
@@ -208,18 +197,12 @@ func (d *PikPak) request(url string, method string, callback base.ReqCallback, r
|
||||
case 0:
|
||||
return res.Body(), nil
|
||||
case 4122, 4121, 16:
|
||||
if strings.Contains(url, "/v1/auth/") || strings.Contains(url, "/v1/shield/captcha/") {
|
||||
return nil, errors.New(e.Error())
|
||||
}
|
||||
// access_token expired, refresh and retry
|
||||
// access_token 过期
|
||||
if err1 := d.refreshToken(d.RefreshToken); err1 != nil {
|
||||
return nil, err1
|
||||
}
|
||||
return d.request(url, method, callback, resp)
|
||||
case 9: // captcha token expired
|
||||
if strings.Contains(url, "/v1/shield/captcha/") {
|
||||
return nil, errors.New(e.Error())
|
||||
}
|
||||
case 9: // 验证码token过期
|
||||
if err = d.RefreshCaptchaTokenAtLogin(GetAction(method, url), d.GetUserID()); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -386,9 +369,6 @@ func (d *PikPak) RefreshCaptchaTokenInLogin(action, username string) error {
|
||||
} else {
|
||||
metas["username"] = username
|
||||
}
|
||||
metas["client_version"] = d.ClientVersion
|
||||
metas["package_name"] = d.PackageName
|
||||
metas["timestamp"], metas["captcha_sign"] = d.Common.GetCaptchaSign()
|
||||
return d.refreshCaptchaToken(action, metas)
|
||||
}
|
||||
|
||||
@@ -427,7 +407,7 @@ func (d *PikPak) refreshCaptchaToken(action string, metas map[string]string) err
|
||||
return errors.New(e.Error())
|
||||
}
|
||||
|
||||
if resp.Url != "" && !d.Addition.SkipVerification {
|
||||
if resp.Url != "" {
|
||||
return fmt.Errorf(`need verify: <a target="_blank" href="%s">Click Here</a>`, resp.Url)
|
||||
}
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -55,18 +55,10 @@ func (d *Teldrive) Drop(ctx context.Context) error {
|
||||
}
|
||||
|
||||
func (d *Teldrive) List(ctx context.Context, dir model.Obj, args model.ListArgs) ([]model.Obj, error) {
|
||||
dirPath := dir.GetPath()
|
||||
if dirPath == "" {
|
||||
dirPath = d.GetRootPath()
|
||||
}
|
||||
if dirPath == "" {
|
||||
dirPath = "/"
|
||||
}
|
||||
|
||||
var firstResp ListResp
|
||||
err := d.request(http.MethodGet, "/api/files", func(req *resty.Request) {
|
||||
req.SetQueryParams(map[string]string{
|
||||
"path": dirPath,
|
||||
"path": dir.GetPath(),
|
||||
"limit": "500",
|
||||
"page": "1",
|
||||
})
|
||||
@@ -95,7 +87,7 @@ func (d *Teldrive) List(ctx context.Context, dir model.Obj, args model.ListArgs)
|
||||
var resp ListResp
|
||||
err := d.request(http.MethodGet, "/api/files", func(req *resty.Request) {
|
||||
req.SetQueryParams(map[string]string{
|
||||
"path": dirPath,
|
||||
"path": dir.GetPath(),
|
||||
"limit": "500",
|
||||
"page": strconv.Itoa(page),
|
||||
})
|
||||
@@ -122,7 +114,7 @@ func (d *Teldrive) List(ctx context.Context, dir model.Obj, args model.ListArgs)
|
||||
|
||||
return utils.SliceConvert(allItems, func(src Object) (model.Obj, error) {
|
||||
return &model.Object{
|
||||
Path: path.Join(dirPath, src.Name),
|
||||
Path: path.Join(dir.GetPath(), src.Name),
|
||||
ID: src.ID,
|
||||
Name: src.Name,
|
||||
Size: func() int64 {
|
||||
|
||||
@@ -4,11 +4,9 @@ import (
|
||||
"context"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"sync"
|
||||
"testing"
|
||||
|
||||
"github.com/OpenListTeam/OpenList/v4/drivers/base"
|
||||
"github.com/OpenListTeam/OpenList/v4/internal/driver"
|
||||
"github.com/OpenListTeam/OpenList/v4/internal/model"
|
||||
"github.com/go-resty/resty/v2"
|
||||
)
|
||||
@@ -38,44 +36,3 @@ func TestListEmptyDir(t *testing.T) {
|
||||
t.Fatalf("expected no entries for an empty dir, got %d", len(objs))
|
||||
}
|
||||
}
|
||||
|
||||
func TestListRootUsesConfiguredRootPath(t *testing.T) {
|
||||
var (
|
||||
mu sync.Mutex
|
||||
paths []string
|
||||
)
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
mu.Lock()
|
||||
paths = append(paths, r.URL.Query().Get("path"))
|
||||
mu.Unlock()
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
_, _ = w.Write([]byte(`{"items":[{"id":"child","name":"child","type":"folder"}],"meta":{"count":1,"totalPages":1,"currentPage":1}}`))
|
||||
}))
|
||||
defer srv.Close()
|
||||
|
||||
oldClient := base.RestyClient
|
||||
base.RestyClient = resty.New()
|
||||
defer func() { base.RestyClient = oldClient }()
|
||||
|
||||
d := &Teldrive{
|
||||
Addition: Addition{
|
||||
RootPath: driver.RootPath{RootFolderPath: "/configured-root"},
|
||||
},
|
||||
}
|
||||
d.Address = srv.URL
|
||||
|
||||
objs, err := d.List(context.Background(), &model.Object{}, model.ListArgs{})
|
||||
if err != nil {
|
||||
t.Fatalf("List returned error: %v", err)
|
||||
}
|
||||
|
||||
mu.Lock()
|
||||
defer mu.Unlock()
|
||||
if len(paths) != 1 || paths[0] != "/configured-root" {
|
||||
t.Fatalf("expected request path %q, got %q", "/configured-root", paths)
|
||||
}
|
||||
if len(objs) != 1 || objs[0].GetPath() != "/configured-root/child" {
|
||||
t.Fatalf("expected child path %q, got %#v", "/configured-root/child", objs)
|
||||
}
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user