mirror of
https://github.com/truewhile/MeBox.git
synced 2026-09-28 11:16:37 +08:00
247 lines
6.6 KiB
Go
247 lines
6.6 KiB
Go
package cloud
|
|
|
|
import (
|
|
"context"
|
|
"encoding/base64"
|
|
"fmt"
|
|
"net/http"
|
|
"net/url"
|
|
"path"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
)
|
|
|
|
// cloudDrive2Provider bridges CloudDrive2 through its WebDAV endpoint.
|
|
//
|
|
// CloudDrive2 integrates many cloud disks (115 / 123 / Aliyun and more).
|
|
// Treating it as a WebDAV-backed cloud provider lets MeBox
|
|
// browse, mount and upload to those disks without carrying every provider's
|
|
// private chunk-upload protocol in this project.
|
|
type cloudDrive2Provider struct {
|
|
typ string
|
|
name string
|
|
base *url.URL
|
|
username string
|
|
password string
|
|
token string // 配置的静态令牌(构造后只读)
|
|
ua string
|
|
apiBase *url.URL
|
|
client *http.Client
|
|
proxy bool
|
|
|
|
// tokenMu / loginToken / loginTokenSeen 保护 OpenList 用户名密码登录的
|
|
// token 缓存:多 worker 并发时单飞登录,缓存有效期内直接复用,
|
|
// 401 时清缓存重登(见 clouddrive2_openlist.go 的 openListAPIToken)。
|
|
tokenMu sync.Mutex
|
|
loginToken string
|
|
loginTokenSeen time.Time
|
|
}
|
|
|
|
func newCloudDrive2(cfg map[string]any, client *http.Client) *cloudDrive2Provider {
|
|
return newCloudDAVProvider(TypeCloudDrive2, "clouddrive2", cfg, client, "/dav")
|
|
}
|
|
|
|
func newOpenList(cfg map[string]any, client *http.Client) *cloudDrive2Provider {
|
|
return newCloudDAVProvider(TypeOpenList, "openlist", cfg, client, "/dav")
|
|
}
|
|
|
|
func newCloudDAVProvider(typ, name string, cfg map[string]any, client *http.Client, defaultDAVPath string) *cloudDrive2Provider {
|
|
rawURL := webDAVURLFromConfig(cfg, defaultDAVPath)
|
|
u, _ := url.Parse(strings.TrimRight(rawURL, "/"))
|
|
var apiBase *url.URL
|
|
if typ == TypeOpenList {
|
|
apiBase = openListAPIBaseFromConfig(cfg, rawURL, defaultDAVPath)
|
|
}
|
|
ua := str(cfg["ua"])
|
|
if ua == "" {
|
|
ua = defaultUA
|
|
}
|
|
proxy := true
|
|
return &cloudDrive2Provider{
|
|
typ: typ,
|
|
name: name,
|
|
base: u,
|
|
username: str(cfg["username"]),
|
|
password: str(cfg["password"]),
|
|
token: str(cfg["token"]),
|
|
ua: ua,
|
|
apiBase: apiBase,
|
|
client: client,
|
|
proxy: proxy,
|
|
}
|
|
}
|
|
|
|
func (p *cloudDrive2Provider) Type() string { return p.typ }
|
|
|
|
func (p *cloudDrive2Provider) Ping(ctx context.Context) error {
|
|
_, err := p.List(ctx, "")
|
|
return err
|
|
}
|
|
|
|
func (p *cloudDrive2Provider) Resolve(ctx context.Context, fileRef string) (*DirectLink, error) {
|
|
if err := p.validate(); err != nil {
|
|
return nil, err
|
|
}
|
|
ref := normalizeCloudDAVPath(fileRef)
|
|
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)
|
|
}
|
|
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)
|
|
}
|
|
return link, nil
|
|
}
|
|
if p.typ == TypeCloudDrive2 && isCloudVideoPlaybackCandidate(ref) {
|
|
link, err := p.resolveCloudDAVRedirectDirect(ctx, ref)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("%s: pure 302 playback requires CloudDrive2/WebDAV to return a CDN Location for %s: %w", p.name, ref, err)
|
|
}
|
|
return link, nil
|
|
}
|
|
headers := map[string]string{
|
|
"User-Agent": p.ua,
|
|
}
|
|
if p.token != "" {
|
|
headers["Authorization"] = p.token
|
|
} else if p.username != "" {
|
|
headers["Authorization"] = "Basic " + base64.StdEncoding.EncodeToString([]byte(p.username+":"+p.password))
|
|
}
|
|
return &DirectLink{URL: p.urlFor(ref), Headers: headers, Proxy: p.proxy}, nil
|
|
}
|
|
|
|
func (p *cloudDrive2Provider) validate() error {
|
|
if p.base == nil || p.base.Scheme == "" || p.base.Host == "" {
|
|
return fmt.Errorf("%s: missing WebDAV URL", p.name)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func webDAVURLFromConfig(cfg map[string]any, defaultDAVPath string) string {
|
|
rawURL := str(cfg["url"])
|
|
if rawURL == "" {
|
|
rawURL = str(cfg["webdav_url"])
|
|
}
|
|
if rawURL != "" {
|
|
return ensureDefaultDAVPath(rawURL, defaultDAVPath)
|
|
}
|
|
return defaultWebDAVURL(str(cfg["server"]), defaultDAVPath)
|
|
}
|
|
|
|
func defaultWebDAVURL(server, defaultDAVPath string) string {
|
|
server = strings.TrimRight(strings.TrimSpace(server), "/")
|
|
if server == "" {
|
|
return ""
|
|
}
|
|
davPath := strings.TrimSpace(defaultDAVPath)
|
|
if davPath == "" {
|
|
return server
|
|
}
|
|
if !strings.HasPrefix(davPath, "/") {
|
|
davPath = "/" + davPath
|
|
}
|
|
return server + davPath
|
|
}
|
|
|
|
func openListAPIBaseFromConfig(cfg map[string]any, webDAVURL, defaultDAVPath string) *url.URL {
|
|
raw := str(cfg["server"])
|
|
if raw == "" {
|
|
raw = firstNonEmpty(str(cfg["api_url"]), webDAVURL)
|
|
}
|
|
raw = strings.TrimRight(strings.TrimSpace(raw), "/")
|
|
if raw == "" {
|
|
return nil
|
|
}
|
|
u, err := url.Parse(raw)
|
|
if err != nil || u.Scheme == "" || u.Host == "" {
|
|
return nil
|
|
}
|
|
davPath := strings.Trim(strings.TrimSpace(defaultDAVPath), "/")
|
|
if davPath != "" {
|
|
pathParts := strings.Split(strings.TrimRight(u.Path, "/"), "/")
|
|
if len(pathParts) > 0 && strings.EqualFold(pathParts[len(pathParts)-1], davPath) {
|
|
u.Path = strings.Join(pathParts[:len(pathParts)-1], "/")
|
|
if u.Path == "" {
|
|
u.Path = "/"
|
|
}
|
|
}
|
|
}
|
|
u.RawPath = ""
|
|
u.RawQuery = ""
|
|
u.Fragment = ""
|
|
return u
|
|
}
|
|
|
|
func (p *cloudDrive2Provider) openListAPIURL(apiPath string) string {
|
|
if p.apiBase == nil {
|
|
return ""
|
|
}
|
|
u := *p.apiBase
|
|
u.RawPath = ""
|
|
basePath := strings.TrimRight(u.Path, "/")
|
|
apiPath = "/" + strings.TrimLeft(apiPath, "/")
|
|
if basePath == "" || basePath == "/" {
|
|
u.Path = apiPath
|
|
} else {
|
|
u.Path = basePath + apiPath
|
|
}
|
|
return u.String()
|
|
}
|
|
|
|
func ensureDefaultDAVPath(rawURL, defaultDAVPath string) string {
|
|
rawURL = strings.TrimRight(strings.TrimSpace(rawURL), "/")
|
|
if rawURL == "" {
|
|
return ""
|
|
}
|
|
u, err := url.Parse(rawURL)
|
|
if err != nil || u.Scheme == "" || u.Host == "" {
|
|
return rawURL
|
|
}
|
|
if strings.TrimSpace(defaultDAVPath) == "" {
|
|
return rawURL
|
|
}
|
|
if u.Path == "" || u.Path == "/" {
|
|
davPath := strings.TrimSpace(defaultDAVPath)
|
|
if !strings.HasPrefix(davPath, "/") {
|
|
davPath = "/" + davPath
|
|
}
|
|
u.Path = davPath
|
|
u.RawPath = ""
|
|
return strings.TrimRight(u.String(), "/")
|
|
}
|
|
return rawURL
|
|
}
|
|
|
|
func normalizeCloudDAVPath(p string) string {
|
|
p = strings.ReplaceAll(strings.TrimSpace(p), "\\", "/")
|
|
if p == "" || p == "." {
|
|
return "/"
|
|
}
|
|
if !strings.HasPrefix(p, "/") {
|
|
p = "/" + p
|
|
}
|
|
cleaned := path.Clean(p)
|
|
if cleaned == "." {
|
|
return "/"
|
|
}
|
|
return cleaned
|
|
}
|
|
|
|
func sameCloudDAVPath(a, b string) bool {
|
|
return strings.TrimRight(normalizeCloudDAVPath(a), "/") == strings.TrimRight(normalizeCloudDAVPath(b), "/")
|
|
}
|
|
|
|
func firstNonEmpty(values ...string) string {
|
|
for _, v := range values {
|
|
if strings.TrimSpace(v) != "" {
|
|
return strings.TrimSpace(v)
|
|
}
|
|
}
|
|
return ""
|
|
}
|