Compare commits

..

3 Commits

Author SHA1 Message Date
truewhile 5a189a44fc Merge pull request #27 from gaodyoffice/fix/strm-sync-metadata
fix: OpenList metadata download 401 - use API instead of WebDAV
2026-09-06 12:30:40 +08:00
truewhile fb84c62e9a fix(sync): fallback to recursive traversal when 115 flat list hits deep-paging limit
115 API's flat list (search under the hood) enforces a hard limit of offset+limit <= 10000.
When syncing huge directories (e.g. >10000 files), this silently truncates results,
causing remote files to appear as missing locally, leading to infinite metadata
re-upload loops and potential wrongful deletion of strm files.

This patch auto-detects if the total file count >= 9500 and dynamically
falls back to the standard recursive concurrent traversal (walkRemote).
2026-09-06 12:27:56 +08:00
Gaodaiyang 355fd06036 fix: OpenList metadata download using API instead of WebDAV
OpenList 同步目录下载元数据(nfo/jpg/png/srt 等)全部失败,错误 http 401。

根因:Resolve() 中非视频文件走 WebDAV 直接下载,用 API token 作为 Authorization。
但 AList WebDAV 端点不接受 API token 认证,需要 Basic Auth。

修复:OpenList 在有 apiBase 时,所有文件都走 API /api/fs/get 获取直链,
不再走 WebDAV。API 失败时非视频文件可回退到 WebDAV。

影响范围:
- 只影响 OpenList 类型的非视频文件下载
- 不影响 115、CloudDrive2 等其他网盘
- 不影响视频播放/strm 生成
2026-09-06 11:41:12 +08:00
4 changed files with 65 additions and 13 deletions
+7 -7
View File
@@ -87,15 +87,15 @@ func (p *cloudDrive2Provider) Resolve(ctx context.Context, fileRef string) (*Dir
if ref == "/" {
return nil, fmt.Errorf("%s: file reference required", p.name)
}
if p.typ == TypeOpenList && isCloudVideoPlaybackCandidate(ref) {
if p.apiBase == nil {
return nil, fmt.Errorf("%s: pure 302 playback requires an OpenList API server address; configure server/api_url so /api/fs/get can return raw_url", p.name)
}
if p.typ == TypeOpenList && p.apiBase != nil {
link, err := p.resolveOpenListAPIDirect(ctx, ref)
if err != nil {
return nil, fmt.Errorf("%s: pure 302 playback requires OpenList raw_url for %s: %w", p.name, ref, err)
if err == nil {
return link, nil
}
// API 获取直链失败:非视频文件(元数据)回退到 WebDAV;视频文件报错
if isCloudVideoPlaybackCandidate(ref) {
return nil, fmt.Errorf("%s: resolve download URL for %s via API failed: %w", p.name, ref, err)
}
return link, nil
}
if p.typ == TypeCloudDrive2 && isCloudVideoPlaybackCandidate(ref) {
link, err := p.resolveCloudDAVRedirectDirect(ctx, ref)
+3 -2
View File
@@ -61,9 +61,10 @@ func TestOpenListWebDAVListAndResolve(t *testing.T) {
if len(entries) != 1 || entries[0].ID != "/Cloud/Movie.mkv" || entries[0].Size != 1024 {
t.Fatalf("entries = %#v", entries)
}
// Video file: API fails → error (no WebDAV fallback for video)
_, err = p.Resolve(context.Background(), entries[0].ID)
if err == nil || !strings.Contains(err.Error(), "pure 302 playback requires OpenList raw_url") {
t.Fatalf("openlist video resolve should require raw_url instead of WebDAV proxy fallback, err=%v", err)
if err == nil || !strings.Contains(err.Error(), "resolve download URL") || !strings.Contains(err.Error(), "via API failed") {
t.Fatalf("openlist video resolve should error on API failure, err=%v", err)
}
}
@@ -195,10 +195,43 @@ func TestOpenListResolveDoesNotFallbackToWebDAVWhenAPIRawURLFails(t *testing.T)
t.Fatal(err)
}
_, err = p.Resolve(context.Background(), "/Cloud/Movie.mkv")
if err == nil || !strings.Contains(err.Error(), "pure 302 playback requires OpenList raw_url") {
t.Fatalf("resolve error = %v, want raw_url requirement", err)
if err == nil || !strings.Contains(err.Error(), "resolve download URL") || !strings.Contains(err.Error(), "via API failed") {
t.Fatalf("resolve error = %v, want API resolve failure", err)
}
if davSeen {
t.Fatal("openlist video resolve fell back to WebDAV after raw_url failure")
}
}
func TestOpenListResolveMetadataUsesAPIInsteadOfWebDAV(t *testing.T) {
var gotPath, gotAuth string
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
gotPath = r.URL.Path
gotAuth = r.Header.Get("Authorization")
if r.Method != http.MethodPost || r.URL.Path != "/api/fs/get" {
t.Fatalf("unexpected request %s %s; metadata should use API, not WebDAV", r.Method, r.URL.Path)
}
w.Header().Set("Content-Type", "application/json")
_, _ = w.Write([]byte(`{"code":200,"data":{"raw_url":"https://cdn.example.test/poster.jpg?sign=1"}}`))
}))
defer srv.Close()
p, err := New(TypeOpenList, map[string]any{"server": srv.URL, "token": "alist-token"}, srv.Client())
if err != nil {
t.Fatal(err)
}
// .nfo metadata file should use API, not WebDAV
link, err := p.Resolve(context.Background(), "/Cloud/Movie/Movie.nfo")
if err != nil {
t.Fatalf("resolve: %v", err)
}
if gotPath != "/api/fs/get" {
t.Fatalf("api path = %q, want /api/fs/get (metadata should not use WebDAV)", gotPath)
}
if gotAuth != "alist-token" {
t.Fatalf("Authorization = %q, want token", gotAuth)
}
if link.URL != "https://cdn.example.test/poster.jpg?sign=1" {
t.Fatalf("url = %q", link.URL)
}
}
+20 -2
View File
@@ -25,6 +25,8 @@ import (
"github.com/truewhile/MeBox/internal/service/cloud115"
)
var errFallbackToWalkRemote = errors.New("fallback to walk remote")
// remoteMetaItem 记录远端存在的单个元数据文件副本信息(大小、文件ID、内容SHA1、修改时间)。
type remoteMetaItem struct {
ID string
@@ -352,8 +354,17 @@ func (st *strmSyncState) run() error {
if st.provider != nil {
if open115, ok := st.provider.(cloud.OpenAPI115Provider); ok && st.p.Provider == model.StrmProvider115 {
if err := st.walk115Flat(open115.OpenClient()); err != nil {
return err
err := st.walk115Flat(open115.OpenClient())
if err != nil {
if errors.Is(err, errFallbackToWalkRemote) {
st.s.log.Warn("115: 扁平列表文件数超限,自动降级为传统并发递归同步",
zap.String("path_id", st.p.ID))
if errWalk := st.walkRemote(); errWalk != nil {
return errWalk
}
} else {
return err
}
}
} else {
if err := st.walkRemote(); err != nil {
@@ -675,6 +686,13 @@ func (st *strmSyncState) walk115Flat(open115 *cloud115.OpenClient) error {
return fmt.Errorf("115: 获取文件列表失败:%w", err)
}
// 115 的扁平化列表(搜索底层)对 offset + limit 有 10000 的最大深度限制。
// 当扁平模式下的总文件数 >= 9500 时,强行拒绝继续扁平拉取,而是抛出降级错误,
// 让外层回退到使用普通的按目录并发递归(walkRemote),以免截断导致后排文件被误删/重传。
if totalCount >= 9500 {
return errFallbackToWalkRemote
}
st.updateSyncMessage(fmt.Sprintf("正在拉取远端文件列表 (共 %d 个文件)...", totalCount))
allFiles := make([]cloud115.RemoteFile, 0, totalCount)