mirror of
https://github.com/truewhile/MeBox.git
synced 2026-09-29 03:26:37 +08:00
prefer openlist api credentials
This commit is contained in:
@@ -448,7 +448,7 @@ func TestOpenListWebDAVListAndResolve(t *testing.T) {
|
||||
}))
|
||||
defer srv.Close()
|
||||
|
||||
p, err := New(TypeOpenList, map[string]any{"server": srv.URL, "username": "u", "password": "p"}, srv.Client())
|
||||
p, err := New(TypeOpenList, map[string]any{"url": srv.URL + "/dav"}, srv.Client())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -474,6 +474,84 @@ func TestOpenListWebDAVListAndResolve(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestOpenListListUsesAPIUsernamePasswordWithoutWebDAVFallback(t *testing.T) {
|
||||
var loginSeen, listSeen, davSeen bool
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
switch r.URL.Path {
|
||||
case "/api/auth/login":
|
||||
loginSeen = true
|
||||
var body map[string]string
|
||||
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
|
||||
t.Fatalf("decode login body: %v", err)
|
||||
}
|
||||
if body["username"] != "alice" || body["password"] != "secret" {
|
||||
t.Fatalf("login body = %#v", body)
|
||||
}
|
||||
_, _ = w.Write([]byte(`{"code":200,"data":{"token":"api-token"}}`))
|
||||
case "/api/fs/list":
|
||||
listSeen = true
|
||||
if r.Header.Get("Authorization") != "api-token" {
|
||||
t.Fatalf("Authorization = %q, want api-token", r.Header.Get("Authorization"))
|
||||
}
|
||||
_, _ = w.Write([]byte(`{"code":200,"data":{"content":[{"name":"Movies","is_dir":true,"size":0},{"name":"Movie.mkv","is_dir":false,"size":1024}],"total":2}}`))
|
||||
case "/dav":
|
||||
davSeen = true
|
||||
w.WriteHeader(http.StatusMultiStatus)
|
||||
default:
|
||||
t.Fatalf("unexpected path %s", r.URL.Path)
|
||||
}
|
||||
}))
|
||||
defer srv.Close()
|
||||
|
||||
p, err := New(TypeOpenList, map[string]any{"server": srv.URL, "username": "alice", "password": "secret"}, srv.Client())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
entries, err := p.List(context.Background(), "")
|
||||
if err != nil {
|
||||
t.Fatalf("list: %v", err)
|
||||
}
|
||||
if !loginSeen || !listSeen {
|
||||
t.Fatalf("expected api login/list, login=%v list=%v", loginSeen, listSeen)
|
||||
}
|
||||
if davSeen {
|
||||
t.Fatal("openlist API credentials should not fall back to WebDAV")
|
||||
}
|
||||
if len(entries) != 2 || entries[0].ID != "/Movies" || !entries[0].IsDir || entries[1].ID != "/Movie.mkv" || entries[1].Size != 1024 {
|
||||
t.Fatalf("entries = %#v", entries)
|
||||
}
|
||||
}
|
||||
|
||||
func TestOpenListListAPIFailureDoesNotFallbackToWebDAV(t *testing.T) {
|
||||
var davSeen bool
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
switch r.URL.Path {
|
||||
case "/api/auth/login":
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
_, _ = w.Write([]byte(`{"code":500,"message":"bad password"}`))
|
||||
case "/dav":
|
||||
davSeen = true
|
||||
w.WriteHeader(http.StatusMultiStatus)
|
||||
default:
|
||||
t.Fatalf("unexpected path %s", r.URL.Path)
|
||||
}
|
||||
}))
|
||||
defer srv.Close()
|
||||
|
||||
p, err := New(TypeOpenList, map[string]any{"server": srv.URL, "username": "alice", "password": "bad"}, srv.Client())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
_, err = p.List(context.Background(), "")
|
||||
if err == nil || !strings.Contains(err.Error(), "api login failed") || !strings.Contains(err.Error(), "bad password") {
|
||||
t.Fatalf("list error = %v, want api login failure", err)
|
||||
}
|
||||
if davSeen {
|
||||
t.Fatal("openlist API failure fell back to WebDAV")
|
||||
}
|
||||
}
|
||||
|
||||
func TestOpenListResolveUsesAPIRawURLFor302Playback(t *testing.T) {
|
||||
var gotPath, gotAuth string
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
@@ -81,9 +81,7 @@ func (p *cloudDrive2Provider) List(ctx context.Context, dir string) ([]FileEntry
|
||||
return nil, err
|
||||
}
|
||||
if p.typ == TypeOpenList && p.apiBase != nil && p.hasOpenListAPICredentials() {
|
||||
if entries, err := p.listOpenListAPI(ctx, dir); err == nil {
|
||||
return entries, nil
|
||||
}
|
||||
return p.listOpenListAPI(ctx, dir)
|
||||
}
|
||||
target := normalizeCloudDAVPath(dir)
|
||||
req, err := http.NewRequestWithContext(ctx, "PROPFIND", p.urlFor(target), strings.NewReader(cloudDAVPropfindBody))
|
||||
|
||||
@@ -227,10 +227,12 @@ func joinRemotePath(base, rel string) string {
|
||||
}
|
||||
|
||||
type alistUploader struct {
|
||||
name string
|
||||
server string
|
||||
token string
|
||||
client *http.Client
|
||||
name string
|
||||
server string
|
||||
token string
|
||||
username string
|
||||
password string
|
||||
client *http.Client
|
||||
}
|
||||
|
||||
func newAlistUploader(cfg map[string]any) *alistUploader {
|
||||
@@ -239,10 +241,12 @@ func newAlistUploader(cfg map[string]any) *alistUploader {
|
||||
|
||||
func newNamedAlistUploader(name string, cfg map[string]any) *alistUploader {
|
||||
return &alistUploader{
|
||||
name: name,
|
||||
server: strings.TrimRight(strr(cfg["server"]), "/"),
|
||||
token: strr(cfg["token"]),
|
||||
client: &http.Client{},
|
||||
name: name,
|
||||
server: strings.TrimRight(strr(cfg["server"]), "/"),
|
||||
token: strr(cfg["token"]),
|
||||
username: strr(cfg["username"]),
|
||||
password: strr(cfg["password"]),
|
||||
client: &http.Client{},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -250,6 +254,9 @@ func (a *alistUploader) ensureDir(ctx context.Context, remoteDir string) error {
|
||||
if a.server == "" {
|
||||
return fmt.Errorf("%s missing server", a.name)
|
||||
}
|
||||
if err := a.ensureToken(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
remoteDir = normalizeRemotePath(remoteDir)
|
||||
if remoteDir == "/" {
|
||||
return nil
|
||||
@@ -277,6 +284,9 @@ func (a *alistUploader) ensureDir(ctx context.Context, remoteDir string) error {
|
||||
}
|
||||
|
||||
func (a *alistUploader) exists(ctx context.Context, remotePath string) (bool, error) {
|
||||
if err := a.ensureToken(ctx); err != nil {
|
||||
return false, err
|
||||
}
|
||||
payload, _ := json.Marshal(map[string]string{"path": normalizeRemotePath(remotePath)})
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodPost, a.server+"/api/fs/get", bytes.NewReader(payload))
|
||||
if err != nil {
|
||||
@@ -301,6 +311,9 @@ func (a *alistUploader) exists(ctx context.Context, remotePath string) (bool, er
|
||||
}
|
||||
|
||||
func (a *alistUploader) upload(ctx context.Context, localPath, remotePath string, size int64) error {
|
||||
if err := a.ensureToken(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
f, err := os.Open(localPath) // #nosec G304 -- localPath is selected from configured local media files before upload.
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -321,6 +334,59 @@ func (a *alistUploader) upload(ctx context.Context, localPath, remotePath string
|
||||
return a.checkJSON(resp, "alist upload")
|
||||
}
|
||||
|
||||
func (a *alistUploader) ensureToken(ctx context.Context) error {
|
||||
if strings.TrimSpace(a.token) != "" {
|
||||
return nil
|
||||
}
|
||||
if strings.TrimSpace(a.username) == "" || a.password == "" {
|
||||
return nil
|
||||
}
|
||||
if a.server == "" {
|
||||
return fmt.Errorf("%s missing server", a.name)
|
||||
}
|
||||
payload, _ := json.Marshal(map[string]string{
|
||||
"username": a.username,
|
||||
"password": a.password,
|
||||
})
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodPost, a.server+"/api/auth/login", bytes.NewReader(payload))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
req.Header.Set("Accept", "application/json")
|
||||
resp, err := a.client.Do(req)
|
||||
if err != nil {
|
||||
return decorateStorageTransportError(a.name, a.server, err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
body, _ := io.ReadAll(io.LimitReader(resp.Body, 4096))
|
||||
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
|
||||
return fmt.Errorf("%s login: http %d: %s", a.name, resp.StatusCode, strings.TrimSpace(string(body)))
|
||||
}
|
||||
var out struct {
|
||||
Code int `json:"code"`
|
||||
Message string `json:"message"`
|
||||
Data struct {
|
||||
Token string `json:"token"`
|
||||
} `json:"data"`
|
||||
}
|
||||
if err := json.Unmarshal(body, &out); err != nil {
|
||||
return fmt.Errorf("%s login: decode response: %w", a.name, err)
|
||||
}
|
||||
if out.Code != 0 && out.Code != 200 {
|
||||
msg := strings.TrimSpace(out.Message)
|
||||
if msg == "" {
|
||||
msg = fmt.Sprintf("code %d", out.Code)
|
||||
}
|
||||
return fmt.Errorf("%s login: %s", a.name, msg)
|
||||
}
|
||||
a.token = strings.TrimSpace(out.Data.Token)
|
||||
if a.token == "" {
|
||||
return fmt.Errorf("%s login returned empty token", a.name)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *alistUploader) auth(req *http.Request) {
|
||||
if a.token != "" {
|
||||
req.Header.Set("Authorization", a.token)
|
||||
|
||||
@@ -146,6 +146,83 @@ func TestStorageConfigUploadLocalToOpenListAPI(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestStorageConfigUploadLocalToOpenListAPIWithUsernamePassword(t *testing.T) {
|
||||
var loginSeen bool
|
||||
var uploaded []string
|
||||
openlist := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
switch r.URL.Path {
|
||||
case "/api/auth/login":
|
||||
loginSeen = true
|
||||
var body map[string]string
|
||||
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
|
||||
t.Fatalf("decode login body: %v", err)
|
||||
}
|
||||
if body["username"] != "alice" || body["password"] != "secret" {
|
||||
t.Fatalf("login body = %#v", body)
|
||||
}
|
||||
_, _ = w.Write([]byte(`{"code":200,"data":{"token":"openlist-session-token"}}`))
|
||||
case "/api/fs/mkdir":
|
||||
if r.Header.Get("Authorization") != "openlist-session-token" {
|
||||
t.Fatalf("mkdir authorization = %q", r.Header.Get("Authorization"))
|
||||
}
|
||||
_, _ = w.Write([]byte(`{"code":200,"message":"success"}`))
|
||||
case "/api/fs/get":
|
||||
if r.Header.Get("Authorization") != "openlist-session-token" {
|
||||
t.Fatalf("get authorization = %q", r.Header.Get("Authorization"))
|
||||
}
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
_, _ = w.Write([]byte(`{"code":404,"message":"not found"}`))
|
||||
case "/api/fs/put":
|
||||
if r.Header.Get("Authorization") != "openlist-session-token" {
|
||||
t.Fatalf("put authorization = %q", r.Header.Get("Authorization"))
|
||||
}
|
||||
decoded, err := url.PathUnescape(r.Header.Get("File-Path"))
|
||||
if err != nil {
|
||||
t.Fatalf("decode file path: %v", err)
|
||||
}
|
||||
uploaded = append(uploaded, decoded)
|
||||
_, _ = w.Write([]byte(`{"code":200,"message":"success"}`))
|
||||
case "/dav":
|
||||
t.Fatal("OpenList username/password upload should use API, not WebDAV")
|
||||
default:
|
||||
t.Fatalf("unexpected openlist path %s", r.URL.Path)
|
||||
}
|
||||
}))
|
||||
defer openlist.Close()
|
||||
|
||||
_, storage := newStorageUploadTestService(t)
|
||||
if _, err := storage.Save(t.Context(), StorageInput{
|
||||
Type: "openlist",
|
||||
Config: map[string]any{
|
||||
"server": openlist.URL,
|
||||
"username": "alice",
|
||||
"password": "secret",
|
||||
},
|
||||
}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
source := t.TempDir()
|
||||
if err := os.WriteFile(filepath.Join(source, "Movie.2026.mkv"), []byte("movie"), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
res, err := storage.UploadLocal(t.Context(), CloudUploadInput{
|
||||
Type: "openlist",
|
||||
SourcePath: source,
|
||||
DestPath: "/OpenList",
|
||||
Recursive: true,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("upload local: %v", err)
|
||||
}
|
||||
if !loginSeen {
|
||||
t.Fatal("expected OpenList API login")
|
||||
}
|
||||
if res.Uploaded != 1 || len(uploaded) != 1 || uploaded[0] != "/OpenList/Movie.2026.mkv" {
|
||||
t.Fatalf("result = %+v uploaded=%#v", res, uploaded)
|
||||
}
|
||||
}
|
||||
|
||||
func TestStorageConfigOpenListHTTPSAgainstHTTPHint(t *testing.T) {
|
||||
openlist := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
_, _ = w.Write([]byte(`{"code":200}`))
|
||||
|
||||
@@ -113,11 +113,11 @@ const FIELD_DEFS: Record<StorageType, { key: string; label: string; secret?: boo
|
||||
{ key: 'token', label: 'Token', secret: true },
|
||||
],
|
||||
openlist: [
|
||||
{ key: 'server', label: 'OpenList Server URL(API / 转存)', placeholder: 'http://NAS-IP:5244' },
|
||||
{ key: 'token', label: 'OpenList Token(API 转存可选)', secret: true },
|
||||
{ key: 'url', label: 'WebDAV URL(浏览/挂载)', placeholder: 'http://NAS-IP:5244/dav/' },
|
||||
{ key: 'username', label: 'WebDAV 用户名' },
|
||||
{ key: 'password', label: 'WebDAV 密码', secret: true },
|
||||
{ key: 'server', label: 'OpenList 服务地址(API / 浏览 / 挂载 / 转存)', placeholder: 'http://NAS-IP:5244' },
|
||||
{ key: 'username', label: 'OpenList 用户名' },
|
||||
{ key: 'password', label: 'OpenList 密码', secret: true },
|
||||
{ key: 'token', label: 'OpenList Token(可选,优先于用户名密码)', secret: true },
|
||||
{ key: 'url', label: 'WebDAV URL(可选兼容备用)', placeholder: '通常不用填;需要备用时填 http://NAS-IP:5244/dav/' },
|
||||
{ key: 'timeout_seconds', label: '请求超时秒数', placeholder: '120' },
|
||||
],
|
||||
webdav: [
|
||||
@@ -373,7 +373,7 @@ function StorageUploadPanel({ type }: { type: StorageType }) {
|
||||
)}
|
||||
{type === 'openlist' && (
|
||||
<p className="mb-3 rounded-lg border border-blue-200 bg-blue-50 px-3 py-2 text-xs text-blue-800">
|
||||
OpenList 默认端口常见为 5244,WebDAV 地址通常是 http://host:5244/dav/。如果未配置 HTTPS 反代,请不要填写 https://,否则会出现 “server gave HTTP response to HTTPS client”。
|
||||
OpenList 优先使用服务地址 + 用户名密码/Token 调用 API 进行浏览、挂载、转存和获取播放直链;WebDAV URL 只是兼容备用。默认端口常见为 5244,未配置 HTTPS 反代时请填写 http://。
|
||||
</p>
|
||||
)}
|
||||
{type === 'clouddrive2' && (
|
||||
|
||||
Reference in New Issue
Block a user