mirror of
https://github.com/truewhile/MeBox.git
synced 2026-09-30 03:36:37 +08:00
174 lines
5.5 KiB
Go
174 lines
5.5 KiB
Go
package cloud
|
|
|
|
import (
|
|
"context"
|
|
"encoding/base64"
|
|
"encoding/xml"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"net/url"
|
|
"path"
|
|
"strings"
|
|
)
|
|
|
|
func (p *cloudDrive2Provider) List(ctx context.Context, dir string) ([]FileEntry, error) {
|
|
if err := p.validate(); err != nil {
|
|
return nil, err
|
|
}
|
|
if p.typ == TypeOpenList && p.apiBase != nil && p.hasOpenListAPICredentials() {
|
|
return p.listOpenListAPI(ctx, dir)
|
|
}
|
|
target := normalizeCloudDAVPath(dir)
|
|
req, err := http.NewRequestWithContext(ctx, "PROPFIND", p.urlFor(target), strings.NewReader(cloudDAVPropfindBody))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
p.auth(req)
|
|
req.Header.Set("Depth", "1")
|
|
req.Header.Set("Content-Type", "application/xml; charset=utf-8")
|
|
req.Header.Set("Accept", "application/xml,text/xml,*/*")
|
|
resp, err := p.client.Do(req)
|
|
if err != nil {
|
|
return nil, decorateDAVTransportError(p.name, p.urlFor(target), err)
|
|
}
|
|
defer resp.Body.Close()
|
|
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
|
|
return nil, p.decorateDAVStatusError(resp, target)
|
|
}
|
|
// 流式解码:超大目录(如上万条目的网盘目录)响应可能远超旧 4MB 截断上限,
|
|
// 直接 xml.Unmarshal 会截断报错;这里用 LimitReader(64MB) + Decoder 边读边解
|
|
var multi cloudDAVMultiStatus
|
|
if err := xml.NewDecoder(io.LimitReader(resp.Body, 64<<20)).Decode(&multi); err != nil {
|
|
return nil, fmt.Errorf("%s: decode webdav: %w", p.name, err)
|
|
}
|
|
basePath := strings.TrimRight(p.base.EscapedPath(), "/")
|
|
currentID := normalizeCloudDAVPath(target)
|
|
out := make([]FileEntry, 0, len(multi.Responses))
|
|
for _, item := range multi.Responses {
|
|
entryPath, err := p.entryIDFromHref(item.Href, basePath)
|
|
if err != nil || entryPath == "" || sameCloudDAVPath(entryPath, currentID) {
|
|
continue
|
|
}
|
|
name := firstNonEmpty(item.PropStat.Prop.DisplayName, path.Base(strings.TrimRight(entryPath, "/")))
|
|
if decoded, err := url.PathUnescape(name); err == nil {
|
|
name = decoded
|
|
}
|
|
if name == "" || name == "." || name == "/" {
|
|
continue
|
|
}
|
|
out = append(out, FileEntry{
|
|
ID: entryPath,
|
|
Name: name,
|
|
IsDir: item.PropStat.Prop.ResourceType.Collection != nil || strings.HasSuffix(item.Href, "/"),
|
|
Size: parseDAVSize(item.PropStat.Prop.ContentLength),
|
|
})
|
|
}
|
|
return out, nil
|
|
}
|
|
|
|
func (p *cloudDrive2Provider) resolveCloudDAVRedirectDirect(ctx context.Context, fileRef string) (*DirectLink, error) {
|
|
target := p.urlFor(fileRef)
|
|
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))
|
|
}
|
|
location, status, err := p.firstHTTPRedirectLocation(ctx, target, headers)
|
|
if err != nil {
|
|
return nil, decorateDAVTransportError(p.name, target, err)
|
|
}
|
|
if location == "" {
|
|
return nil, fmt.Errorf("%s: WebDAV %s returned http %d without CDN Location; refusing WebDAV/proxy fallback for pure 302 playback", p.name, fileRef, status)
|
|
}
|
|
return &DirectLink{URL: location, Headers: nil, Proxy: false}, nil
|
|
}
|
|
|
|
func (p *cloudDrive2Provider) firstHTTPRedirectLocation(ctx context.Context, target string, headers map[string]string) (string, int, error) {
|
|
req, err := http.NewRequestWithContext(ctx, http.MethodGet, target, nil)
|
|
if err != nil {
|
|
return "", 0, err
|
|
}
|
|
req.Header.Set("Accept", "*/*")
|
|
req.Header.Set("Accept-Encoding", "identity")
|
|
req.Header.Set("Range", "bytes=0-0")
|
|
if strings.TrimSpace(p.ua) != "" {
|
|
req.Header.Set("User-Agent", p.ua)
|
|
}
|
|
for key, value := range headers {
|
|
key = strings.TrimSpace(key)
|
|
if key != "" && strings.TrimSpace(value) != "" {
|
|
req.Header.Set(key, value)
|
|
}
|
|
}
|
|
client := p.client
|
|
if client == nil {
|
|
client = http.DefaultClient
|
|
}
|
|
noFollow := *client
|
|
noFollow.CheckRedirect = func(*http.Request, []*http.Request) error {
|
|
return http.ErrUseLastResponse
|
|
}
|
|
resp, err := noFollow.Do(req)
|
|
if err != nil {
|
|
return "", 0, err
|
|
}
|
|
defer resp.Body.Close()
|
|
status := resp.StatusCode
|
|
if status >= 300 && status < 400 {
|
|
rawLocation := strings.TrimSpace(resp.Header.Get("Location"))
|
|
if rawLocation == "" {
|
|
return "", status, fmt.Errorf("%s: upstream returned redirect http %d without Location", p.name, status)
|
|
}
|
|
location, err := resolveHTTPRedirectLocation(target, rawLocation)
|
|
if err != nil {
|
|
return "", status, err
|
|
}
|
|
return location, status, nil
|
|
}
|
|
return "", status, nil
|
|
}
|
|
|
|
func resolveHTTPRedirectLocation(baseURL, rawLocation string) (string, error) {
|
|
rawLocation = strings.TrimSpace(rawLocation)
|
|
if rawLocation == "" {
|
|
return "", fmt.Errorf("empty redirect Location")
|
|
}
|
|
if strings.HasPrefix(rawLocation, "//") {
|
|
base, err := url.Parse(baseURL)
|
|
if err != nil || base.Scheme == "" {
|
|
return "", fmt.Errorf("protocol-relative redirect Location without base scheme")
|
|
}
|
|
rawLocation = base.Scheme + ":" + rawLocation
|
|
}
|
|
location, err := url.Parse(rawLocation)
|
|
if err != nil {
|
|
return "", fmt.Errorf("invalid redirect Location: %w", err)
|
|
}
|
|
if location.IsAbs() {
|
|
if location.Scheme != "http" && location.Scheme != "https" {
|
|
return "", fmt.Errorf("unsupported redirect Location scheme %q", location.Scheme)
|
|
}
|
|
return location.String(), nil
|
|
}
|
|
base, err := url.Parse(baseURL)
|
|
if err != nil {
|
|
return "", fmt.Errorf("invalid redirect base URL: %w", err)
|
|
}
|
|
return base.ResolveReference(location).String(), nil
|
|
}
|
|
|
|
func (p *cloudDrive2Provider) auth(req *http.Request) {
|
|
req.Header.Set("User-Agent", p.ua)
|
|
if p.token != "" {
|
|
req.Header.Set("Authorization", p.token)
|
|
return
|
|
}
|
|
if p.username != "" {
|
|
req.SetBasicAuth(p.username, p.password)
|
|
}
|
|
}
|