fix: Fix partial WebDAV upload failures (#10077)

This commit is contained in:
ssongliu 2025-08-20 16:44:43 +08:00 committed by GitHub
parent d09d686378
commit 75197db3e6
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
12 changed files with 1226 additions and 8 deletions

View file

@ -44,7 +44,6 @@ require (
github.com/spf13/afero v1.11.0
github.com/spf13/cobra v1.9.1
github.com/spf13/viper v1.19.0
github.com/studio-b12/gowebdav v0.9.0
github.com/subosito/gotenv v1.6.0
github.com/tencentyun/cos-go-sdk-v5 v0.7.54
github.com/tomasen/fcgi_client v0.0.0-20180423082037-2bb3d819fd19

View file

@ -881,8 +881,6 @@ github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/studio-b12/gowebdav v0.9.0 h1:1j1sc9gQnNxbXXM4M/CebPOX4aXYtr7MojAVcN4dHjU=
github.com/studio-b12/gowebdav v0.9.0/go.mod h1:bHA7t77X/QFExdeAnDzK6vKM34kEZAcE1OX4MfiwjkE=
github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.563/go.mod h1:7sCQWVkxcsR38nffDW057DRGk8mUjK1Ing/EFOK8s8Y=

View file

@ -0,0 +1,306 @@
package webdav
import (
"bytes"
"errors"
"io"
"net/http"
"strings"
"sync"
)
type AuthFactory func(c *http.Client, rs *http.Response, path string) (auth Authenticator, err error)
type Authorizer interface {
NewAuthenticator(body io.Reader) (Authenticator, io.Reader)
AddAuthenticator(key string, fn AuthFactory)
}
type Authenticator interface {
Authorize(c *http.Client, rq *http.Request, path string) error
Verify(c *http.Client, rs *http.Response, path string) (redo bool, err error)
Clone() Authenticator
io.Closer
}
type authfactory struct {
key string
create AuthFactory
}
type authorizer struct {
factories []authfactory
defAuthMux sync.Mutex
defAuth Authenticator
}
type preemptiveAuthorizer struct {
auth Authenticator
}
type authShim struct {
factory AuthFactory
body io.Reader
auth Authenticator
}
type negoAuth struct {
auths []Authenticator
setDefaultAuthenticator func(auth Authenticator)
}
type nullAuth struct{}
type noAuth struct{}
func NewAutoAuth(login string, secret string) Authorizer {
fmap := make([]authfactory, 0)
az := &authorizer{factories: fmap, defAuthMux: sync.Mutex{}, defAuth: &nullAuth{}}
az.AddAuthenticator("basic", func(c *http.Client, rs *http.Response, path string) (auth Authenticator, err error) {
return &BasicAuth{user: login, pw: secret}, nil
})
az.AddAuthenticator("digest", func(c *http.Client, rs *http.Response, path string) (auth Authenticator, err error) {
return NewDigestAuth(login, secret, rs)
})
az.AddAuthenticator("passport1.4", func(c *http.Client, rs *http.Response, path string) (auth Authenticator, err error) {
return NewPassportAuth(c, login, secret, rs.Request.URL.String(), &rs.Header)
})
return az
}
func NewEmptyAuth() Authorizer {
fmap := make([]authfactory, 0)
az := &authorizer{factories: fmap, defAuthMux: sync.Mutex{}, defAuth: &nullAuth{}}
return az
}
func NewPreemptiveAuth(auth Authenticator) Authorizer {
return &preemptiveAuthorizer{auth: auth}
}
func (a *authorizer) NewAuthenticator(body io.Reader) (Authenticator, io.Reader) {
var retryBuf io.Reader = body
if body != nil {
if _, ok := retryBuf.(io.Seeker); ok {
body = io.NopCloser(body)
} else {
buff := &bytes.Buffer{}
retryBuf = buff
body = io.TeeReader(body, buff)
}
}
a.defAuthMux.Lock()
defAuth := a.defAuth.Clone()
a.defAuthMux.Unlock()
return &authShim{factory: a.factory, body: retryBuf, auth: defAuth}, body
}
func (a *authorizer) AddAuthenticator(key string, fn AuthFactory) {
key = strings.ToLower(key)
for _, f := range a.factories {
if f.key == key {
panic("Authenticator exists: " + key)
}
}
a.factories = append(a.factories, authfactory{key, fn})
}
func (a *authorizer) factory(c *http.Client, rs *http.Response, path string) (auth Authenticator, err error) {
headers := rs.Header.Values("Www-Authenticate")
if len(headers) > 0 {
auths := make([]Authenticator, 0)
for _, f := range a.factories {
for _, header := range headers {
headerLower := strings.ToLower(header)
if strings.Contains(headerLower, f.key) {
rs.Header.Set("Www-Authenticate", header)
if auth, err = f.create(c, rs, path); err == nil {
auths = append(auths, auth)
break
}
}
}
}
switch len(auths) {
case 0:
return nil, NewPathError("NoAuthenticator", path, rs.StatusCode)
case 1:
auth = auths[0]
default:
auth = &negoAuth{auths: auths, setDefaultAuthenticator: a.setDefaultAuthenticator}
}
} else {
auth = &noAuth{}
}
a.setDefaultAuthenticator(auth)
return auth, nil
}
func (a *authorizer) setDefaultAuthenticator(auth Authenticator) {
a.defAuthMux.Lock()
a.defAuth.Close()
a.defAuth = auth
a.defAuthMux.Unlock()
}
func (s *authShim) Authorize(c *http.Client, rq *http.Request, path string) error {
if err := s.auth.Authorize(c, rq, path); err != nil {
return err
}
body := s.body
rq.GetBody = func() (io.ReadCloser, error) {
if body != nil {
if sk, ok := body.(io.Seeker); ok {
if _, err := sk.Seek(0, io.SeekStart); err != nil {
return nil, err
}
}
return io.NopCloser(body), nil
}
return nil, nil
}
return nil
}
func (s *authShim) Verify(c *http.Client, rs *http.Response, path string) (redo bool, err error) {
redo, err = s.auth.Verify(c, rs, path)
if err != nil && errors.Is(err, ErrAuthChanged) {
if auth, aerr := s.factory(c, rs, path); aerr == nil {
s.auth.Close()
s.auth = auth
return true, nil
} else {
return false, aerr
}
}
return
}
func (s *authShim) Close() error {
s.auth.Close()
s.auth, s.factory = nil, nil
if s.body != nil {
if closer, ok := s.body.(io.Closer); ok {
return closer.Close()
}
}
return nil
}
func (s *authShim) Clone() Authenticator {
return &noAuth{}
}
func (s *authShim) String() string {
return "AuthShim"
}
func (n *negoAuth) Authorize(c *http.Client, rq *http.Request, path string) error {
if len(n.auths) == 0 {
return NewPathError("NoAuthenticator", path, 400)
}
return n.auths[0].Authorize(c, rq, path)
}
func (n *negoAuth) Verify(c *http.Client, rs *http.Response, path string) (redo bool, err error) {
if len(n.auths) == 0 {
return false, NewPathError("NoAuthenticator", path, 400)
}
redo, err = n.auths[0].Verify(c, rs, path)
if err != nil {
if len(n.auths) > 1 {
n.auths[0].Close()
n.auths = n.auths[1:]
return true, nil
}
} else if redo {
return
} else {
auth := n.auths[0]
n.auths = n.auths[1:]
n.setDefaultAuthenticator(auth)
return
}
return false, NewPathError("NoAuthenticator", path, rs.StatusCode)
}
func (n *negoAuth) Close() error {
for _, a := range n.auths {
a.Close()
}
n.setDefaultAuthenticator = nil
return nil
}
func (n *negoAuth) Clone() Authenticator {
auths := make([]Authenticator, len(n.auths))
for i, e := range n.auths {
auths[i] = e.Clone()
}
return &negoAuth{auths: auths, setDefaultAuthenticator: n.setDefaultAuthenticator}
}
func (n *negoAuth) String() string {
return "NegoAuth"
}
func (n *noAuth) Authorize(c *http.Client, rq *http.Request, path string) error {
return nil
}
func (n *noAuth) Verify(c *http.Client, rs *http.Response, path string) (redo bool, err error) {
if "" != rs.Header.Get("Www-Authenticate") {
err = ErrAuthChanged
}
return
}
func (n *noAuth) Close() error {
return nil
}
func (n *noAuth) Clone() Authenticator {
return n
}
func (n *noAuth) String() string {
return "NoAuth"
}
func (n *nullAuth) Authorize(c *http.Client, rq *http.Request, path string) error {
rq.Header.Set(XInhibitRedirect, "1")
return nil
}
func (n *nullAuth) Verify(c *http.Client, rs *http.Response, path string) (redo bool, err error) {
return true, ErrAuthChanged
}
func (n *nullAuth) Close() error {
return nil
}
func (n *nullAuth) Clone() Authenticator {
return n
}
func (n *nullAuth) String() string {
return "NullAuth"
}
func (b *preemptiveAuthorizer) NewAuthenticator(body io.Reader) (Authenticator, io.Reader) {
return b.auth.Clone(), body
}
func (b *preemptiveAuthorizer) AddAuthenticator(key string, fn AuthFactory) {
panic("You're funny! A preemptive authorizer may only have a single authentication method")
}

View file

@ -0,0 +1,36 @@
package webdav
import (
"fmt"
"net/http"
)
type BasicAuth struct {
user string
pw string
}
func (b *BasicAuth) Authorize(c *http.Client, rq *http.Request, path string) error {
rq.SetBasicAuth(b.user, b.pw)
return nil
}
func (b *BasicAuth) Verify(c *http.Client, rs *http.Response, path string) (redo bool, err error) {
if rs.StatusCode == 401 {
err = NewPathError("Authorize", path, rs.StatusCode)
}
return
}
func (b *BasicAuth) Close() error {
return nil
}
func (b *BasicAuth) Clone() Authenticator {
// no copy due to read only access
return b
}
func (b *BasicAuth) String() string {
return fmt.Sprintf("BasicAuth login: %s", b.user)
}

View file

@ -0,0 +1,172 @@
package webdav
import (
"crypto/md5"
"crypto/rand"
"encoding/hex"
"fmt"
"io"
"net/http"
"strings"
)
type DigestAuth struct {
user string
pw string
digestParts map[string]string
}
func NewDigestAuth(login, secret string, rs *http.Response) (Authenticator, error) {
return &DigestAuth{user: login, pw: secret, digestParts: digestParts(rs)}, nil
}
func (d *DigestAuth) Authorize(c *http.Client, rq *http.Request, path string) error {
d.digestParts["uri"] = path
d.digestParts["method"] = rq.Method
d.digestParts["username"] = d.user
d.digestParts["password"] = d.pw
rq.Header.Set("Authorization", getDigestAuthorization(d.digestParts))
return nil
}
func (d *DigestAuth) Verify(c *http.Client, rs *http.Response, path string) (redo bool, err error) {
if rs.StatusCode == 401 {
if isStaled(rs) {
redo = true
err = ErrAuthChanged
} else {
err = NewPathError("Authorize", path, rs.StatusCode)
}
}
return
}
func (d *DigestAuth) Close() error {
return nil
}
func (d *DigestAuth) Clone() Authenticator {
parts := make(map[string]string, len(d.digestParts))
for k, v := range d.digestParts {
parts[k] = v
}
return &DigestAuth{user: d.user, pw: d.pw, digestParts: parts}
}
func (d *DigestAuth) String() string {
return fmt.Sprintf("DigestAuth login: %s", d.user)
}
func digestParts(resp *http.Response) map[string]string {
result := map[string]string{}
if len(resp.Header["Www-Authenticate"]) > 0 {
wantedHeaders := []string{"nonce", "realm", "qop", "opaque", "algorithm", "entityBody"}
responseHeaders := strings.Split(resp.Header["Www-Authenticate"][0], ",")
for _, r := range responseHeaders {
for _, w := range wantedHeaders {
if strings.Contains(r, w) {
result[w] = strings.Trim(
strings.SplitN(r, `=`, 2)[1],
`"`,
)
}
}
}
}
return result
}
func getMD5(text string) string {
hasher := md5.New()
hasher.Write([]byte(text))
return hex.EncodeToString(hasher.Sum(nil))
}
func getCnonce() string {
b := make([]byte, 8)
io.ReadFull(rand.Reader, b)
return fmt.Sprintf("%x", b)[:16]
}
func getDigestAuthorization(digestParts map[string]string) string {
d := digestParts
var (
ha1 string
ha2 string
nonceCount = 00000001
cnonce = getCnonce()
response string
)
switch d["algorithm"] {
case "MD5", "":
ha1 = getMD5(d["username"] + ":" + d["realm"] + ":" + d["password"])
case "MD5-sess":
ha1 = getMD5(
fmt.Sprintf("%s:%v:%s",
getMD5(d["username"]+":"+d["realm"]+":"+d["password"]),
nonceCount,
cnonce,
),
)
}
switch d["qop"] {
case "auth", "":
ha2 = getMD5(d["method"] + ":" + d["uri"])
case "auth-int":
if d["entityBody"] != "" {
ha2 = getMD5(d["method"] + ":" + d["uri"] + ":" + getMD5(d["entityBody"]))
}
}
switch d["qop"] {
case "":
response = getMD5(
fmt.Sprintf("%s:%s:%s",
ha1,
d["nonce"],
ha2,
),
)
case "auth", "auth-int":
response = getMD5(
fmt.Sprintf("%s:%s:%v:%s:%s:%s",
ha1,
d["nonce"],
nonceCount,
cnonce,
d["qop"],
ha2,
),
)
}
authorization := fmt.Sprintf(`Digest username="%s", realm="%s", nonce="%s", uri="%s", nc=%v, cnonce="%s", response="%s"`,
d["username"], d["realm"], d["nonce"], d["uri"], nonceCount, cnonce, response)
if d["qop"] != "" {
authorization += fmt.Sprintf(`, qop=%s`, d["qop"])
}
if d["opaque"] != "" {
authorization += fmt.Sprintf(`, opaque="%s"`, d["opaque"])
}
return authorization
}
func isStaled(rs *http.Response) bool {
header := rs.Header.Get("Www-Authenticate")
if len(header) > 0 {
directives := strings.Split(header, ",")
for i := range directives {
name, value, _ := strings.Cut(strings.Trim(directives[i], " "), "=")
if strings.EqualFold(name, "stale") {
return strings.EqualFold(value, "true")
}
}
}
return false
}

View file

@ -0,0 +1,160 @@
package webdav
import (
"fmt"
"io"
"net/http"
"net/url"
"strings"
)
type PassportAuth struct {
user string
pw string
cookies []http.Cookie
inhibitRedirect bool
}
func NewPassportAuth(c *http.Client, user, pw, partnerURL string, header *http.Header) (Authenticator, error) {
p := &PassportAuth{
user: user,
pw: pw,
inhibitRedirect: true,
}
err := p.genCookies(c, partnerURL, header)
return p, err
}
func (p *PassportAuth) Authorize(c *http.Client, rq *http.Request, path string) error {
if p.inhibitRedirect {
rq.Header.Set(XInhibitRedirect, "1")
} else {
p.inhibitRedirect = true
}
for _, cookie := range p.cookies {
rq.AddCookie(&cookie)
}
return nil
}
func (p *PassportAuth) Verify(c *http.Client, rs *http.Response, path string) (redo bool, err error) {
switch rs.StatusCode {
case 301, 302, 307, 308:
redo = true
if rs.Header.Get("Www-Authenticate") != "" {
err = p.genCookies(c, rs.Request.URL.String(), &rs.Header)
} else {
p.inhibitRedirect = false
}
case 401:
err = NewPathError("Authorize", path, rs.StatusCode)
}
return
}
func (p *PassportAuth) Close() error {
return nil
}
func (p *PassportAuth) Clone() Authenticator {
clonedCookies := make([]http.Cookie, len(p.cookies))
copy(clonedCookies, p.cookies)
return &PassportAuth{
user: p.user,
pw: p.pw,
cookies: clonedCookies,
inhibitRedirect: true,
}
}
func (p *PassportAuth) String() string {
return fmt.Sprintf("PassportAuth login: %s", p.user)
}
func (p *PassportAuth) genCookies(c *http.Client, partnerUrl string, header *http.Header) error {
baseAuthenticationServer := header.Get("Location")
baseAuthenticationServerURL, err := url.Parse(baseAuthenticationServer)
if err != nil {
return err
}
authenticationServerUrl := url.URL{
Scheme: baseAuthenticationServerURL.Scheme,
Host: baseAuthenticationServerURL.Host,
Path: "/login2.srf",
}
partnerServerChallenge := strings.Split(header.Get("Www-Authenticate"), " ")[1]
req := http.Request{
Method: "GET",
URL: &authenticationServerUrl,
Header: http.Header{
"Authorization": []string{"Passport1.4 sign-in=" + url.QueryEscape(p.user) + ",pwd=" + url.QueryEscape(p.pw) + ",OrgVerb=GET,OrgUrl=" + partnerUrl + "," + partnerServerChallenge},
},
}
rs, err := c.Do(&req)
if err != nil {
return err
}
io.Copy(io.Discard, rs.Body)
rs.Body.Close()
if rs.StatusCode != 200 {
return NewPathError("Authorize", "/", rs.StatusCode)
}
tokenResponseHeader := rs.Header.Get("Authentication-Info")
if tokenResponseHeader == "" {
return NewPathError("Authorize", "/", 401)
}
tokenResponseHeaderList := strings.Split(tokenResponseHeader, ",")
token := ""
for _, tokenResponseHeader := range tokenResponseHeaderList {
if strings.HasPrefix(tokenResponseHeader, "from-PP='") {
token = tokenResponseHeader
break
}
}
if token == "" {
return NewPathError("Authorize", "/", 401)
}
origUrl, err := url.Parse(partnerUrl)
if err != nil {
return err
}
req = http.Request{
Method: "GET",
URL: origUrl,
Header: http.Header{
"Authorization": []string{"Passport1.4 " + token},
},
}
rs, err = c.Do(&req)
if err != nil {
return err
}
io.Copy(io.Discard, rs.Body)
rs.Body.Close()
if rs.StatusCode != 200 && rs.StatusCode != 302 {
return NewPathError("Authorize", "/", rs.StatusCode)
}
cookies := rs.Header.Values("Set-Cookie")
p.cookies = make([]http.Cookie, len(cookies))
for i, cookie := range cookies {
cookieParts := strings.Split(cookie, ";")
cookieName := strings.Split(cookieParts[0], "=")[0]
cookieValue := strings.Split(cookieParts[0], "=")[1]
p.cookies[i] = http.Cookie{
Name: cookieName,
Value: cookieValue,
}
}
return nil
}

View file

@ -0,0 +1,35 @@
package webdav
import (
"errors"
"fmt"
"os"
)
var ErrAuthChanged = errors.New("authentication failed, change algorithm")
var ErrTooManyRedirects = errors.New("stopped after 10 redirects")
type StatusError struct {
Status int
}
func (se StatusError) Error() string {
return fmt.Sprintf("%d", se.Status)
}
func NewPathError(op string, path string, statusCode int) error {
return &os.PathError{
Op: op,
Path: path,
Err: StatusError{statusCode},
}
}
func NewPathErrorErr(op string, path string, err error) error {
return &os.PathError{
Op: op,
Path: path,
Err: err,
}
}

View file

@ -0,0 +1,61 @@
package webdav
import (
"fmt"
"os"
"time"
)
type File struct {
path string
name string
contentType string
size int64
modified time.Time
etag string
isdir bool
}
func (f File) Name() string {
return f.name
}
func (f File) ContentType() string {
return f.contentType
}
func (f File) Size() int64 {
return f.size
}
func (f File) Mode() os.FileMode {
if f.isdir {
return 0775 | os.ModeDir
}
return 0664
}
func (f File) ModTime() time.Time {
return f.modified
}
func (f File) ETag() string {
return f.etag
}
func (f File) IsDir() bool {
return f.isdir
}
func (f File) Sys() interface{} {
return nil
}
func (f File) String() string {
if f.isdir {
return fmt.Sprintf("Dir : '%s' - '%s'", f.path, f.name)
}
return fmt.Sprintf("File: '%s' SIZE: %d MODIFIED: %s ETAG: %s CTYPE: %s", f.path, f.size, f.modified.String(), f.etag, f.contentType)
}

View file

@ -0,0 +1,95 @@
package webdav
import (
"fmt"
"io"
"net/http"
"strings"
)
func (c *Client) req(method, path string, body io.Reader, intercept func(*http.Request)) (rs *http.Response, err error) {
var redo bool
var r *http.Request
var uri = PathEscape(Join(c.root, path))
auth, body := c.auth.NewAuthenticator(body)
defer auth.Close()
for {
if r, err = http.NewRequest(method, uri, body); err != nil {
err = fmt.Errorf("handle request with uri: %s, method: %s failed, err: %v", uri, method, err)
return
}
for k, vals := range c.headers {
for _, v := range vals {
r.Header.Add(k, v)
}
}
if err = auth.Authorize(c.c, r, path); err != nil {
return
}
if intercept != nil {
intercept(r)
}
if rs, err = c.c.Do(r); err != nil {
err = fmt.Errorf("do request for resp with uri: %s, method: %s failed, err: %v", uri, method, err)
return
}
if redo, err = auth.Verify(c.c, rs, path); err != nil {
rs.Body.Close()
return nil, err
}
if redo {
rs.Body.Close()
if body, err = r.GetBody(); err != nil {
return nil, err
}
continue
}
break
}
return rs, err
}
func (c *Client) propfind(path string, self bool, body string, resp interface{}, parse func(resp interface{}) error) error {
rs, err := c.req("PROPFIND", path, strings.NewReader(body), func(rq *http.Request) {
if self {
rq.Header.Add("Depth", "0")
} else {
rq.Header.Add("Depth", "1")
}
rq.Header.Add("Content-Type", "application/xml;charset=UTF-8")
rq.Header.Add("Accept", "application/xml,text/xml")
rq.Header.Add("Accept-Charset", "utf-8")
// TODO add support for 'gzip,deflate;q=0.8,q=0.7'
rq.Header.Add("Accept-Encoding", "")
})
if err != nil {
return err
}
defer rs.Body.Close()
if rs.StatusCode != 207 {
return NewPathError("PROPFIND", path, rs.StatusCode)
}
return parseXML(rs.Body, resp, parse)
}
func (c *Client) put(path string, stream io.Reader, contentLength int64) (status int, err error) {
rs, err := c.req("PUT", path, stream, func(r *http.Request) {
r.ContentLength = contentLength
})
if err != nil {
return
}
defer rs.Body.Close()
status = rs.StatusCode
return
}

View file

@ -0,0 +1,95 @@
package webdav
import (
"bytes"
"encoding/xml"
"io"
"net/url"
"path"
"strconv"
"strings"
)
func PathEscape(path string) string {
s := strings.Split(path, "/")
for i, e := range s {
s[i] = url.PathEscape(e)
}
return strings.Join(s, "/")
}
func FixSlash(s string) string {
if !strings.HasSuffix(s, "/") {
s += "/"
}
return s
}
func SplitPathToHierarchy(fullPath string) []string {
cleanPath := path.Clean(fullPath)
parts := strings.Split(cleanPath, "/")
var result []string
currentPath := ""
for _, part := range parts {
if part == "" {
currentPath = "/"
result = append(result, currentPath)
continue
}
if currentPath == "/" {
currentPath = path.Join(currentPath, part)
} else {
currentPath = path.Join(currentPath, part)
}
result = append(result, currentPath)
}
return result
}
func FixSlashes(s string) string {
if !strings.HasPrefix(s, "/") {
s = "/" + s
}
return FixSlash(s)
}
func Join(path0 string, path1 string) string {
return strings.TrimSuffix(path0, "/") + "/" + strings.TrimPrefix(path1, "/")
}
func String(r io.Reader) string {
buf := new(bytes.Buffer)
// TODO - make String return an error as well
_, _ = buf.ReadFrom(r)
return buf.String()
}
func parseInt64(s *string) int64 {
if n, e := strconv.ParseInt(*s, 10, 64); e == nil {
return n
}
return 0
}
func parseXML(data io.Reader, resp interface{}, parse func(resp interface{}) error) error {
decoder := xml.NewDecoder(data)
for t, _ := decoder.Token(); t != nil; t, _ = decoder.Token() {
switch se := t.(type) {
case xml.StartElement:
if se.Name.Local == "response" {
if e := decoder.DecodeElement(resp, &se); e == nil {
if err := parse(resp); err != nil {
return err
}
}
}
}
}
return nil
}

View file

@ -0,0 +1,261 @@
package webdav
import (
"bytes"
"encoding/xml"
"fmt"
"io"
"net/http"
"net/url"
"os"
"path/filepath"
"strings"
)
const XInhibitRedirect = "X-Gowebdav-Inhibit-Redirect"
const template = `<d:propfind xmlns:d='DAV:'>
<d:prop>
<d:displayname/>
<d:resourcetype/>
<d:getcontentlength/>
</d:prop>
</d:propfind>`
type Client struct {
root string
headers http.Header
c *http.Client
auth Authorizer
}
func NewClient(uri, user, pw string) *Client {
return NewAuthClient(uri, NewAutoAuth(user, pw))
}
func NewAuthClient(uri string, auth Authorizer) *Client {
c := &http.Client{
CheckRedirect: func(rq *http.Request, via []*http.Request) error {
if len(via) >= 10 {
return ErrTooManyRedirects
}
if via[0].Header.Get(XInhibitRedirect) != "" {
return http.ErrUseLastResponse
}
return nil
},
}
return &Client{root: FixSlash(uri), headers: make(http.Header), c: c, auth: auth}
}
func (c *Client) SetTransport(transport http.RoundTripper) {
c.c.Transport = transport
}
func (c *Client) Connect() error {
rs, err := c.req("OPTIONS", "/", nil, func(rq *http.Request) { rq.Header.Add("Depth", "0") })
if err != nil {
return err
}
defer rs.Body.Close()
if rs.StatusCode != 200 && rs.StatusCode != 204 {
return fmt.Errorf("check conn failed, code: %d, err: %v", rs.StatusCode, rs.Status)
}
return nil
}
type props struct {
Status string `xml:"DAV: status"`
Name string `xml:"DAV: prop>displayname,omitempty"`
Type xml.Name `xml:"DAV: prop>resourcetype>collection,omitempty"`
Size string `xml:"DAV: prop>getcontentlength,omitempty"`
}
type response struct {
Href string `xml:"DAV: href"`
Props []props `xml:"DAV: propstat"`
}
func getProps(r *response, status string) *props {
for _, prop := range r.Props {
if strings.Contains(prop.Status, status) {
return &prop
}
}
return nil
}
func (c *Client) ReadDir(path string) ([]os.FileInfo, error) {
path = FixSlashes(path)
files := make([]os.FileInfo, 0)
skipSelf := true
parse := func(resp interface{}) error {
r := resp.(*response)
if skipSelf {
skipSelf = false
if p := getProps(r, "200"); p != nil && p.Type.Local == "collection" {
r.Props = nil
return nil
}
return NewPathError("ReadDir", path, 405)
}
if p := getProps(r, "200"); p != nil {
f := new(File)
if ps, err := url.PathUnescape(r.Href); err == nil {
f.name = filepath.Base(ps)
} else {
f.name = p.Name
}
f.path = path + f.name
if p.Type.Local == "collection" {
f.path += "/"
f.size = 0
f.isdir = true
} else {
f.size = parseInt64(&p.Size)
f.isdir = false
}
files = append(files, *f)
}
r.Props = nil
return nil
}
if err := c.propfind(path, false, template, &response{}, parse); err != nil {
if _, ok := err.(*os.PathError); !ok {
return files, fmt.Errorf("load files from %s failed, err: %v", path, err)
}
}
return files, nil
}
func (c *Client) Stat(path string) (os.FileInfo, error) {
var f *File
parse := func(resp interface{}) error {
r := resp.(*response)
if p := getProps(r, "200"); p != nil && f == nil {
f = new(File)
f.name = p.Name
f.path = path
if p.Type.Local == "collection" {
if !strings.HasSuffix(f.path, "/") {
f.path += "/"
}
f.size = 0
f.isdir = true
} else {
f.size = parseInt64(&p.Size)
f.isdir = false
}
}
r.Props = nil
return nil
}
if err := c.propfind(path, true, template, &response{}, parse); err != nil {
if _, ok := err.(*os.PathError); !ok {
return f, fmt.Errorf("load file %s failed, path err: %v", path, err)
}
return f, fmt.Errorf("load file %s failed, err: %v", path, err)
}
return f, nil
}
func (c *Client) RemoveAll(path string) error {
rs, err := c.req("DELETE", path, nil, nil)
if err != nil {
return fmt.Errorf("handle remove file %s failed, err: %s", path, err)
}
defer rs.Body.Close()
if rs.StatusCode == 200 || rs.StatusCode == 204 || rs.StatusCode == 404 {
return nil
}
return fmt.Errorf("handle remove file %s failed, code: %d, err: %s", path, rs.StatusCode, rs.Status)
}
func (c *Client) MkdirAll(path string, _ os.FileMode) (err error) {
parentPath := filepath.Dir(path)
if parentPath == "." || parentPath == "/" {
return nil
}
paths := SplitPathToHierarchy(parentPath)
for _, item := range paths {
itemFile, err := c.Stat(item)
if err == nil && itemFile.IsDir() {
continue
}
rs, err := c.req("MKCOL", item, nil, nil)
if err != nil {
return fmt.Errorf("mkdir %s failed, err: %v", item, err)
}
defer rs.Body.Close()
if rs.StatusCode != 201 || rs.StatusCode == 200 {
return fmt.Errorf("mkdir %s failed, code: %d, err: %v", item, rs.StatusCode, rs.Status)
}
}
return nil
}
func (c *Client) ReadStream(path string) (io.ReadCloser, error) {
rs, err := c.req("GET", path, nil, nil)
if err != nil {
return nil, NewPathErrorErr("ReadStream", path, err)
}
if rs.StatusCode == 200 {
return rs.Body, nil
}
rs.Body.Close()
return nil, NewPathError("ReadStream", path, rs.StatusCode)
}
func (c *Client) WriteStream(path string, stream io.Reader, _ os.FileMode) (err error) {
err = c.MkdirAll(path, 0755)
if err != nil {
return err
}
contentLength := int64(0)
if seeker, ok := stream.(io.Seeker); ok {
contentLength, err = seeker.Seek(0, io.SeekEnd)
if err != nil {
return err
}
_, err = seeker.Seek(0, io.SeekStart)
if err != nil {
return err
}
} else {
buffer := bytes.NewBuffer(make([]byte, 0, 1024*1024 /* 1MB */))
contentLength, err = io.Copy(buffer, stream)
if err != nil {
return err
}
stream = buffer
}
s, err := c.put(path, stream, contentLength)
if err != nil {
return err
}
switch s {
case 200, 201, 204:
return nil
default:
return NewPathError("WriteStream", path, s)
}
}

View file

@ -3,19 +3,19 @@ package client
import (
"crypto/tls"
"fmt"
"github.com/1Panel-dev/1Panel/agent/constant"
"io"
"net/http"
"os"
"path"
"strings"
"github.com/studio-b12/gowebdav"
"github.com/1Panel-dev/1Panel/agent/constant"
"github.com/1Panel-dev/1Panel/agent/utils/cloud_storage/client/helper/webdav"
)
type webDAVClient struct {
Bucket string
client *gowebdav.Client
client *webdav.Client
}
func NewWebDAVClient(vars map[string]interface{}) (*webDAVClient, error) {
@ -29,7 +29,7 @@ func NewWebDAVClient(vars map[string]interface{}) (*webDAVClient, error) {
if len(port) == 0 {
url = address
}
client := gowebdav.NewClient(url, username, password)
client := webdav.NewClient(url, username, password)
tlsConfig := &tls.Config{}
if strings.HasPrefix(address, "https") {
tlsConfig.InsecureSkipVerify = true
@ -103,7 +103,7 @@ func (s webDAVClient) Size(pathItem string) (int64, error) {
}
func (s webDAVClient) Delete(pathItem string) (bool, error) {
if err := s.client.Remove(path.Join(s.Bucket, pathItem)); err != nil {
if err := s.client.RemoveAll(path.Join(s.Bucket, pathItem)); err != nil {
return false, err
}
return true, nil