mirror of
https://github.com/1Panel-dev/1Panel.git
synced 2025-09-10 08:35:29 +08:00
fix: Fix partial WebDAV upload failures (#10077)
This commit is contained in:
parent
d09d686378
commit
75197db3e6
12 changed files with 1226 additions and 8 deletions
|
@ -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
|
||||
|
|
|
@ -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=
|
||||
|
|
306
agent/utils/cloud_storage/client/helper/webdav/auth.go
Normal file
306
agent/utils/cloud_storage/client/helper/webdav/auth.go
Normal 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")
|
||||
}
|
36
agent/utils/cloud_storage/client/helper/webdav/auth_basic.go
Normal file
36
agent/utils/cloud_storage/client/helper/webdav/auth_basic.go
Normal 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)
|
||||
}
|
172
agent/utils/cloud_storage/client/helper/webdav/auth_digest.go
Normal file
172
agent/utils/cloud_storage/client/helper/webdav/auth_digest.go
Normal 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
|
||||
}
|
160
agent/utils/cloud_storage/client/helper/webdav/auth_passport.go
Normal file
160
agent/utils/cloud_storage/client/helper/webdav/auth_passport.go
Normal 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
|
||||
}
|
35
agent/utils/cloud_storage/client/helper/webdav/errors.go
Normal file
35
agent/utils/cloud_storage/client/helper/webdav/errors.go
Normal 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,
|
||||
}
|
||||
}
|
61
agent/utils/cloud_storage/client/helper/webdav/file.go
Normal file
61
agent/utils/cloud_storage/client/helper/webdav/file.go
Normal 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)
|
||||
}
|
95
agent/utils/cloud_storage/client/helper/webdav/reques.go
Normal file
95
agent/utils/cloud_storage/client/helper/webdav/reques.go
Normal 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
|
||||
}
|
95
agent/utils/cloud_storage/client/helper/webdav/utils.go
Normal file
95
agent/utils/cloud_storage/client/helper/webdav/utils.go
Normal 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
|
||||
}
|
261
agent/utils/cloud_storage/client/helper/webdav/webdav.go
Normal file
261
agent/utils/cloud_storage/client/helper/webdav/webdav.go
Normal 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)
|
||||
}
|
||||
}
|
|
@ -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
|
||||
|
|
Loading…
Add table
Reference in a new issue