Compare commits

..

22 Commits

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

---------

Co-authored-by: nostalume <nostalucent@gmail.com>
Co-authored-by: Codex <267193182+codex@users.noreply.github.com>
2026-09-24 12:01:50 +08:00
Nostalgia 90acfa18e4 refactor(context): centralize request origin access (#3100) 2026-09-24 01:57:21 +08:00
Nostalgia 1462d63a48 fix(net): preserve cancellation errors during partitioned downloads (#3090) 2026-09-24 01:47:30 +08:00
spark cadbf87246 fix(teldrive): use a valid root path for listings (#3108) 2026-09-24 01:46:35 +08:00
ZRHan 9de3f69b8f fix(fs): only list parent dir for related objs when file is video (#3113) 2026-09-24 01:46:14 +08:00
Wray e73a80c78c feat(drivers/pikpak): auto re-login with username/password when tokens expire (#3001) 2026-09-24 01:29:56 +08:00
ZRHan 6c6009109f fix(fs): return FOLDER type for directories in fs/get responses (#3114) 2026-09-24 01:26:42 +08:00
Nostalgia b51d1c8284 fix(s3): close ranged response bodies (#3095) 2026-09-24 01:26:26 +08:00
ILoveScratch f286862c61 refactor(stream): index parked readers by offset instead of full scans (#3048) 2026-09-24 01:25:57 +08:00
awaae 40f4f6546f fix(ftp): handle EOF and detect content type from read bytes (#3125) 2026-09-24 01:20:20 +08:00
Nostalgia 3a31b438a9 refactor(offline): centralize native tool setup (#3096)
- Keep storage-to-tool identity in the offline tool package.
- Share settings persistence, tool initialization, and storage admission across handlers.
- Preserve endpoint payloads and unsupported-storage diagnostics with focused tests.

Co-authored-by: nostalume <nostalucent@gmail.com>
Co-authored-by: Codex <267193182+codex@users.noreply.github.com>
2026-09-21 16:54:22 +08:00
Nostalgia 56064d1981 fix(op): enforce link cache lifecycle policy (#3101)
- Share one admitted lifecycle policy between regular and archive links.
- Reject and release links that combine TTL caching with owned resources.
- Keep wrapper clones independent from the source cache expiration.
- Cover reuse, reference, invalidation, conflict, and clone behavior.

Co-authored-by: nostalume <nostalucent@gmail.com>
Co-authored-by: Codex <267193182+codex@users.noreply.github.com>
2026-09-21 16:54:12 +08:00
Ziheng Mao d894a3983b fix(115_open): refresh expired OSS credentials during upload (#3063)
- Refresh OSS credentials and retry the rejected multipart operation
- Preserve upload IDs, completed parts, callbacks, and outer part retries
- Rewind parts before resending and pass the upload context to OSS requests

Co-authored-by: Codex <267193182+codex@users.noreply.github.com>
2026-09-21 10:38:14 +08:00
ShenLin 084c008102 chore(build): replace uncontrolled email domain (#3103)
- Use the controlled oplist.org domain for embedded build authors
- Update shell and release workflow build metadata

Signed-off-by: jyxjjj <16695261+jyxjjj@users.noreply.github.com>
Co-authored-by: Codex <267193182+codex@users.noreply.github.com>
2026-09-20 14:25:20 +08:00
Fighting 4580c4db33 fix(alist_v3): set child paths so nested directories resolve (#3019)
* fix(alist_v3): set child paths so nested directories resolve

- Set `Path` on every object returned by `List`, matching the OpenList
  driver. `op.Get` hands a child object straight back to `List`, so a
  child without a path made the driver request `""` from the upstream
  server, which answered with its own root: every directory below the
  mount point served the same listing back, endlessly.
- Add tests covering the child paths and a three-level descent.

Co-authored-by: Claude <81847+claude@users.noreply.github.com>

* test(alist_v3): trim the child-path test to a single case

Collapse the two tests into one two-level descent, drop the recorder
type and the helper funcs, and inline the driver setup. 173 -> 65 lines.

Co-authored-by: Claude <81847+claude@users.noreply.github.com>

---------

Co-authored-by: Fighting <3899648+Elity@users.noreply.github.com>
Co-authored-by: Claude <81847+claude@users.noreply.github.com>
2026-09-15 23:13:10 +08:00
Nostalgia 5447ecb072 fix(s3): encode multipart fixture paths (#3074)
test(s3): encode multipart fixture paths

- Serialize the Local storage addition instead of interpolating filesystem paths into JSON.
- Keep multipart fixture behavior portable across Windows and Linux.

Co-authored-by: nostalume <nostalucent@gmail.com>
Co-authored-by: Codex <267193182+codex@users.noreply.github.com>
2026-09-14 20:30:17 +08:00
80 changed files with 4209 additions and 840 deletions
+2 -2
View File
@@ -124,7 +124,7 @@ jobs:
FRONTEND_REPO: ${{ vars.FRONTEND_REPO }}
- name: Build
uses: OpenListTeam/cgo-actions@6fcace5934c36d70503dba06e2396bb58b766130 # v1.2.5
uses: OpenListTeam/cgo-actions@d760a8ec1a6be1f8ec181229e11cb2671797f9e0 # v1.3.0
with:
targets: ${{ matrix.target }}
flags: ${{ matrix.flags || '-ldflags=' }}
@@ -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@openlist.team>
github.com/OpenListTeam/OpenList/v4/internal/conf.GitAuthor=The OpenList Projects Contributors <noreply@oplist.org>
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
+2 -2
View File
@@ -42,7 +42,7 @@ jobs:
FRONTEND_REPO: ${{ vars.FRONTEND_REPO }}
- name: Build
uses: OpenListTeam/cgo-actions@6fcace5934c36d70503dba06e2396bb58b766130 # v1.2.5
uses: OpenListTeam/cgo-actions@d760a8ec1a6be1f8ec181229e11cb2671797f9e0 # v1.3.0
with:
targets: ${{ matrix.target }}
flags: ${{ contains(matrix.target, '-musl') && '-ldflags=-linkmode external -extldflags ''-static -fpic''' || '-ldflags=' }}
@@ -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@openlist.team>
github.com/OpenListTeam/OpenList/v4/internal/conf.GitAuthor=The OpenList Projects Contributors <noreply@oplist.org>
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
+3 -3
View File
@@ -1,7 +1,7 @@
set -e
appName="openlist"
builtAt="$(date +'%F %T %z')"
gitAuthor="The OpenList Projects Contributors <noreply@openlist.team>"
gitAuthor="The OpenList Projects Contributors <noreply@oplist.org>"
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.3"
freebsd_version="14.3"
echo "Failed to get FreeBSD version, falling back to 14.4"
freebsd_version="14.4"
fi
echo "Using FreeBSD version: $freebsd_version"
+61 -16
View File
@@ -3,6 +3,7 @@ package _115_open
import (
"context"
"encoding/base64"
"errors"
"io"
"time"
@@ -70,6 +71,19 @@ 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 {
@@ -80,7 +94,32 @@ func (d *Open115) multpartUpload(ctx context.Context, stream model.FileStreamer,
return err
}
imur, err := bucket.InitiateMultipartUpload(initResp.Object, oss.Sequential())
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
})
if err != nil {
return err
}
@@ -109,13 +148,17 @@ func (d *Open115) multpartUpload(ctx context.Context, stream model.FileStreamer,
return err
}
err = retry.Do(func() error {
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
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
})
},
retry.Context(ctx),
retry.Attempts(3),
@@ -134,14 +177,16 @@ func (d *Open115) multpartUpload(ctx context.Context, stream model.FileStreamer,
up(float64(offset) * 100 / float64(fileSize))
}
// 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),
)
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
})
if err != nil {
return err
}
+7 -2
View File
@@ -7,11 +7,16 @@ import (
type Addition struct {
LoginType string `json:"login_type" type:"select" options:"password,qrcode" default:"password" required:"true"`
Username string `json:"username" required:"true"`
Password string `json:"password" required:"true"`
Username string `json:"username" help:"Not needed when an access token or refresh token is provided"`
Password string `json:"password" help:"Not needed when an access token or refresh token is provided"`
VCode string `json:"validate_code"`
SmsCode string `json:"sms_code" help:"SMS code for the second device verification, fill it in and save again when login asks for it"`
AccessToken string `json:"access_token" required:"false"`
RefreshToken string `json:"refresh_token" help:"To switch accounts, please clear this field"`
DeviceID string `json:"device_id" help:"DEVICEID cookie issued after the second device verification, keep it to avoid verifying again"`
ClientSn string `json:"client_sn" help:"Device serial number captured from the official client, leave it empty if you do not have one"`
JgOpenId string `json:"jg_open_id" help:"Optional push id reported by the official client"`
UserFinger string `json:"user_finger" help:"Device fingerprint sent with login requests, generated and kept automatically when empty"`
driver.RootID
OrderBy string `json:"order_by" type:"select" options:"filename,filesize,lastOpTime" default:"filename"`
OrderDirection string `json:"order_direction" type:"select" options:"asc,desc" default:"asc"`
+62
View File
@@ -72,6 +72,8 @@ type BaseLoginParam struct {
// 请求头参数
Lt string
ReqId string
// logbox页面地址,作为后续请求的Referer,缺失会被判定为陌生设备
Referer string
// 表单参数
ParamId string
@@ -97,10 +99,20 @@ type LoginParam struct {
// rsa密钥
jRsaKey string
// 加密字段的前缀,服务端下发(如 {NRP})
rsaPrefix string
// 设备二次校验时服务端返回的加密手机号
SecondAuthMobile string
BaseLoginParam
}
// encryptSecret 用登陆时拿到的公钥加密敏感值,格式与userName/epd一致
func (p *LoginParam) encryptSecret(value string) string {
return p.rsaPrefix + RsaEncrypt(p.jRsaKey, value)
}
// 登陆加密相关
type EncryptConfResp struct {
Result int `json:"result"`
@@ -116,6 +128,35 @@ type LoginResp struct {
Msg string `json:"msg"`
Result int `json:"result"`
ToUrl string `json:"toUrl"`
// 设备二次校验时返回的加密手机号
Mobile string `json:"mobile"`
}
// 登陆页配置,新版登陆页的paramId由该接口下发
// 该接口的result可能是数字也可能是字符串
type AppConfResp struct {
Result any `json:"result"`
Msg string `json:"msg"`
Data struct {
ParamId string `json:"paramId"`
AccountType string `json:"accountType"`
ReturnUrl string `json:"returnUrl"`
MailSuffix string `json:"mailSuffix"`
} `json:"data"`
}
func (r *AppConfResp) Succeeded() bool {
switch v := r.Result.(type) {
case nil:
return true
case string:
return v == "0" || v == ""
case float64:
return v == 0
case int:
return v == 0
}
return false
}
// 刷新session返回
@@ -149,6 +190,27 @@ type AppSessionResp struct {
RefreshToken string `json:"refreshToken"`
}
// 刷新token返回,失败时以HTTP 200返回result/msg,需要单独判断
type RefreshTokenResp struct {
AccessToken string `json:"accessToken"`
RefreshToken string `json:"refreshToken"`
ExpiresIn int `json:"expiresIn"`
Result int `json:"result"`
Msg string `json:"msg"`
}
func (r *RefreshTokenResp) HasError() bool {
return r.Result != 0 || r.AccessToken == ""
}
func (r *RefreshTokenResp) Error() string {
if r.Msg != "" {
return fmt.Sprintf("refresh token failed, result: %d, msg: %s", r.Result, r.Msg)
}
return fmt.Sprintf("refresh token failed, result: %d", r.Result)
}
// 家庭云账户
type FamilyInfoListResp struct {
FamilyInfoResp []FamilyInfoResp `json:"familyInfoResp"`
+355 -77
View File
File diff suppressed because it is too large Load Diff
-1
View File
@@ -328,7 +328,6 @@ 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
}
+1
View File
@@ -95,6 +95,7 @@ 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,
+65
View File
@@ -0,0 +1,65 @@
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
+18 -4
View File
@@ -11,6 +11,7 @@ import (
"github.com/OpenListTeam/OpenList/v4/internal/driver"
"github.com/OpenListTeam/OpenList/v4/internal/errs"
"github.com/OpenListTeam/OpenList/v4/internal/model"
"github.com/OpenListTeam/OpenList/v4/internal/stream"
"github.com/OpenListTeam/OpenList/v4/pkg/utils"
"github.com/go-resty/resty/v2"
log "github.com/sirupsen/logrus"
@@ -22,8 +23,9 @@ type AliyundriveOpen struct {
DriveId string
limiter *limiter
ref *AliyundriveOpen
limiter *limiter
ref *AliyundriveOpen
callback *callbackRegistration
}
func (d *AliyundriveOpen) Config() driver.Config {
@@ -35,6 +37,7 @@ func (d *AliyundriveOpen) GetAddition() driver.Additional {
}
func (d *AliyundriveOpen) Init(ctx context.Context) error {
d.CallbackConcurrency = normalizeCallbackConcurrency(d.CallbackConcurrency)
d.limiter = getLimiterForUser(globalLimiterUserID) // First create a globally shared limiter to limit the initial requests.
if d.LIVPDownloadFormat == "" {
d.LIVPDownloadFormat = "jpeg"
@@ -52,6 +55,7 @@ func (d *AliyundriveOpen) Init(ctx context.Context) error {
userid := utils.Json.Get(res, "user_id").ToString()
d.limiter.free()
d.limiter = getLimiterForUser(userid) // Allocate a corresponding limiter for each user.
d.callback = registerCallbackLimiter(userid, d.CallbackConcurrency)
return nil
}
@@ -65,6 +69,10 @@ func (d *AliyundriveOpen) InitReference(storage driver.Driver) error {
}
func (d *AliyundriveOpen) Drop(ctx context.Context) error {
if d.callback != nil {
d.callback.unregister()
d.callback = nil
}
d.limiter.free()
d.limiter = nil
d.ref = nil
@@ -119,10 +127,16 @@ func (d *AliyundriveOpen) Link(ctx context.Context, file model.Obj, args model.L
url = utils.Json.Get(res, "streamsUrl", d.LIVPDownloadFormat).ToString()
}
exp := time.Minute
return &model.Link{
link := &model.Link{
URL: url,
Expiration: &exp,
}, nil
}
if args.Redirect {
return link, nil
}
link.URL = ""
link.RangeReader = stream.RateLimitRangeReaderFunc(d.callbackRangeReader(url, file.GetSize()))
return link, nil
}
func (d *AliyundriveOpen) MakeDir(ctx context.Context, parentDir model.Obj, dirName string) (model.Obj, error) {
+14 -13
View File
@@ -8,19 +8,20 @@ import (
type Addition struct {
DriveType string `json:"drive_type" type:"select" options:"default,resource,backup" default:"resource"`
driver.RootID
RefreshToken string `json:"refresh_token" required:"true"`
OrderBy string `json:"order_by" type:"select" options:"name,size,updated_at,created_at"`
OrderDirection string `json:"order_direction" type:"select" options:"ASC,DESC"`
UseOnlineAPI bool `json:"use_online_api" default:"true"`
AlipanType string `json:"alipan_type" required:"true" type:"select" default:"default" options:"default,alipanTV"`
APIAddress string `json:"api_url_address" default:"https://api.oplist.org/alicloud/renewapi"`
ClientID string `json:"client_id" help:"Keep it empty if you don't have one"`
ClientSecret string `json:"client_secret" help:"Keep it empty if you don't have one"`
RemoveWay string `json:"remove_way" required:"true" type:"select" options:"trash,delete"`
RapidUpload bool `json:"rapid_upload" help:"If you enable this option, the file will be uploaded to the server first, so the progress will be incorrect"`
InternalUpload bool `json:"internal_upload" help:"If you are using Aliyun ECS is located in Beijing, you can turn it on to boost the upload speed"`
LIVPDownloadFormat string `json:"livp_download_format" type:"select" options:"jpeg,mov" default:"jpeg"`
AccessToken string
RefreshToken string `json:"refresh_token" required:"true"`
OrderBy string `json:"order_by" type:"select" options:"name,size,updated_at,created_at"`
OrderDirection string `json:"order_direction" type:"select" options:"ASC,DESC"`
UseOnlineAPI bool `json:"use_online_api" default:"true"`
AlipanType string `json:"alipan_type" required:"true" type:"select" default:"default" options:"default,alipanTV"`
APIAddress string `json:"api_url_address" default:"https://api.oplist.org/alicloud/renewapi"`
ClientID string `json:"client_id" help:"Keep it empty if you don't have one"`
ClientSecret string `json:"client_secret" help:"Keep it empty if you don't have one"`
RemoveWay string `json:"remove_way" required:"true" type:"select" options:"trash,delete"`
RapidUpload bool `json:"rapid_upload" help:"If you enable this option, the file will be uploaded to the server first, so the progress will be incorrect"`
InternalUpload bool `json:"internal_upload" help:"If you are using Aliyun ECS is located in Beijing, you can turn it on to boost the upload speed"`
LIVPDownloadFormat string `json:"livp_download_format" type:"select" options:"jpeg,mov" default:"jpeg"`
CallbackConcurrency int `json:"callback_concurrency" type:"number" default:"1" help:"Maximum active proxied downloads shared by Aliyun user"`
AccessToken string
}
var config = driver.Config{
+43 -8
View File
@@ -315,6 +315,36 @@ 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
@@ -437,18 +467,23 @@ 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 {
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 {
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
}
+2 -1
View File
@@ -10,10 +10,11 @@ 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:"true" default:""`
RefreshToken string `json:"refresh_token" required:"false" 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{
+30 -10
View File
@@ -100,12 +100,13 @@ 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"
// 使用 用户填写的 CaptchaToken —————— (验证后的captcha_token)
if d.GetCaptchaToken() == "" {
if err := d.RefreshCaptchaTokenInLogin(GetAction(http.MethodPost, url), d.Username); err != nil {
return err
}
// Always refresh captcha token before signin (it may be expired)
if err := d.RefreshCaptchaTokenInLogin(GetAction(http.MethodPost, url), d.Username); err != nil {
return err
}
var e ErrResp
@@ -125,7 +126,12 @@ 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
}
@@ -159,9 +165,14 @@ 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 = jsoniter.Get(data, "refresh_token").ToString()
d.AccessToken = jsoniter.Get(data, "access_token").ToString()
d.RefreshToken = newRefreshToken
d.AccessToken = newAccessToken
d.Common.SetUserID(jsoniter.Get(data, "sub").ToString())
d.Addition.RefreshToken = d.RefreshToken
op.MustSaveDriverStorage(d)
@@ -197,12 +208,18 @@ func (d *PikPak) request(url string, method string, callback base.ReqCallback, r
case 0:
return res.Body(), nil
case 4122, 4121, 16:
// access_token 过期
if strings.Contains(url, "/v1/auth/") || strings.Contains(url, "/v1/shield/captcha/") {
return nil, errors.New(e.Error())
}
// access_token expired, refresh and retry
if err1 := d.refreshToken(d.RefreshToken); err1 != nil {
return nil, err1
}
return d.request(url, method, callback, resp)
case 9: // 验证码token过期
case 9: // captcha token expired
if strings.Contains(url, "/v1/shield/captcha/") {
return nil, errors.New(e.Error())
}
if err = d.RefreshCaptchaTokenAtLogin(GetAction(method, url), d.GetUserID()); err != nil {
return nil, err
}
@@ -369,6 +386,9 @@ 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)
}
@@ -407,7 +427,7 @@ func (d *PikPak) refreshCaptchaToken(action string, metas map[string]string) err
return errors.New(e.Error())
}
if resp.Url != "" {
if resp.Url != "" && !d.Addition.SkipVerification {
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
+11 -3
View File
@@ -55,10 +55,18 @@ 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": dir.GetPath(),
"path": dirPath,
"limit": "500",
"page": "1",
})
@@ -87,7 +95,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": dir.GetPath(),
"path": dirPath,
"limit": "500",
"page": strconv.Itoa(page),
})
@@ -114,7 +122,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(dir.GetPath(), src.Name),
Path: path.Join(dirPath, src.Name),
ID: src.ID,
Name: src.Name,
Size: func() int64 {
+43
View File
@@ -4,9 +4,11 @@ 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"
)
@@ -36,3 +38,44 @@ 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