mirror of https://gitee.com/godoos/godoos.git
18 changed files with 2130 additions and 63 deletions
@ -0,0 +1,409 @@ |
|||||
|
package webdav |
||||
|
|
||||
|
import ( |
||||
|
"bytes" |
||||
|
"errors" |
||||
|
"io" |
||||
|
"net/http" |
||||
|
"strings" |
||||
|
"sync" |
||||
|
) |
||||
|
|
||||
|
// AuthFactory prototype function to create a new Authenticator
|
||||
|
type AuthFactory func(c *http.Client, rs *http.Response, path string) (auth Authenticator, err error) |
||||
|
|
||||
|
// Authorizer our Authenticator factory which creates an
|
||||
|
// `Authenticator` per action/request.
|
||||
|
type Authorizer interface { |
||||
|
// Creates a new Authenticator Shim per request.
|
||||
|
// It may track request related states and perform payload buffering
|
||||
|
// for authentication round trips.
|
||||
|
// The underlying Authenticator will perform the real authentication.
|
||||
|
NewAuthenticator(body io.Reader) (Authenticator, io.Reader) |
||||
|
// Registers a new Authenticator factory to a key.
|
||||
|
AddAuthenticator(key string, fn AuthFactory) |
||||
|
} |
||||
|
|
||||
|
// An Authenticator implements a specific way to authorize requests.
|
||||
|
// Each request is bound to a separate Authenticator instance.
|
||||
|
//
|
||||
|
// The authentication flow itself is broken down into `Authorize`
|
||||
|
// and `Verify` steps. The former method runs before, and the latter
|
||||
|
// runs after the `Request` is submitted.
|
||||
|
// This makes it easy to encapsulate and control complex
|
||||
|
// authentication challenges.
|
||||
|
//
|
||||
|
// Some authentication flows causing authentication round trips,
|
||||
|
// which can be archived by returning the `redo` of the Verify
|
||||
|
// method. `True` restarts the authentication process for the
|
||||
|
// current action: A new `Request` is spawned, which must be
|
||||
|
// authorized, sent, and re-verified again, until the action
|
||||
|
// is successfully submitted.
|
||||
|
// The preferred way is to handle the authentication ping-pong
|
||||
|
// within `Verify`, and then `redo` with fresh credentials.
|
||||
|
//
|
||||
|
// The result of the `Verify` method can also trigger an
|
||||
|
// `Authenticator` change by returning the `ErrAuthChanged`
|
||||
|
// as an error. Depending on the `Authorizer` this may trigger
|
||||
|
// an `Authenticator` negotiation.
|
||||
|
//
|
||||
|
// Set the `XInhibitRedirect` header to '1' in the `Authorize`
|
||||
|
// method to get control over request redirection.
|
||||
|
// Attention! You must handle the incoming request yourself.
|
||||
|
//
|
||||
|
// To store a shared session state the `Clone` method **must**
|
||||
|
// return a new instance, initialized with the shared state.
|
||||
|
type Authenticator interface { |
||||
|
// Authorizes a request. Usually by adding some authorization headers.
|
||||
|
Authorize(c *http.Client, rq *http.Request, path string) error |
||||
|
// Verifies the response if the authorization was successful.
|
||||
|
// May trigger some round trips to pass the authentication.
|
||||
|
// May also trigger a new Authenticator negotiation by returning `ErrAuthChenged`
|
||||
|
Verify(c *http.Client, rs *http.Response, path string) (redo bool, err error) |
||||
|
// Creates a copy of the underlying Authenticator.
|
||||
|
Clone() Authenticator |
||||
|
io.Closer |
||||
|
} |
||||
|
|
||||
|
type authfactory struct { |
||||
|
key string |
||||
|
create AuthFactory |
||||
|
} |
||||
|
|
||||
|
// authorizer structure holds our Authenticator create functions
|
||||
|
type authorizer struct { |
||||
|
factories []authfactory |
||||
|
defAuthMux sync.Mutex |
||||
|
defAuth Authenticator |
||||
|
} |
||||
|
|
||||
|
// preemptiveAuthorizer structure holds the preemptive Authenticator
|
||||
|
type preemptiveAuthorizer struct { |
||||
|
auth Authenticator |
||||
|
} |
||||
|
|
||||
|
// authShim structure that wraps the real Authenticator
|
||||
|
type authShim struct { |
||||
|
factory AuthFactory |
||||
|
body io.Reader |
||||
|
auth Authenticator |
||||
|
} |
||||
|
|
||||
|
// negoAuth structure holds the authenticators that are going to be negotiated
|
||||
|
type negoAuth struct { |
||||
|
auths []Authenticator |
||||
|
setDefaultAuthenticator func(auth Authenticator) |
||||
|
} |
||||
|
|
||||
|
// nullAuth initializes the whole authentication flow
|
||||
|
type nullAuth struct{} |
||||
|
|
||||
|
// noAuth structure to perform no authentication at all
|
||||
|
type noAuth struct{} |
||||
|
|
||||
|
// NewAutoAuth creates an auto Authenticator factory.
|
||||
|
// It negotiates the default authentication method
|
||||
|
// based on the order of the registered Authenticators
|
||||
|
// and the remotely offered authentication methods.
|
||||
|
// First In, First Out.
|
||||
|
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 |
||||
|
} |
||||
|
|
||||
|
// NewEmptyAuth creates an empty Authenticator factory
|
||||
|
// The order of adding the Authenticator matters.
|
||||
|
// First In, First Out.
|
||||
|
// It offers the `NewAutoAuth` features.
|
||||
|
func NewEmptyAuth() Authorizer { |
||||
|
fmap := make([]authfactory, 0) |
||||
|
az := &authorizer{factories: fmap, defAuthMux: sync.Mutex{}, defAuth: &nullAuth{}} |
||||
|
return az |
||||
|
} |
||||
|
|
||||
|
// NewPreemptiveAuth creates a preemptive Authenticator
|
||||
|
// The preemptive authorizer uses the provided Authenticator
|
||||
|
// for every request regardless of any `Www-Authenticate` header.
|
||||
|
//
|
||||
|
// It may only have one authentication method,
|
||||
|
// so calling `AddAuthenticator` **will panic**!
|
||||
|
//
|
||||
|
// Look out!! This offers the skinniest and slickest implementation
|
||||
|
// without any synchronisation!!
|
||||
|
// Still applicable with `BasicAuth` within go routines.
|
||||
|
func NewPreemptiveAuth(auth Authenticator) Authorizer { |
||||
|
return &preemptiveAuthorizer{auth: auth} |
||||
|
} |
||||
|
|
||||
|
// NewAuthenticator creates an Authenticator (Shim) per request
|
||||
|
func (a *authorizer) NewAuthenticator(body io.Reader) (Authenticator, io.Reader) { |
||||
|
var retryBuf io.Reader = body |
||||
|
if body != nil { |
||||
|
// If the authorization fails, we will need to restart reading
|
||||
|
// from the passed body stream.
|
||||
|
// When body is seekable, use seek to reset the streams
|
||||
|
// cursor to the start.
|
||||
|
// Otherwise, copy the stream into a buffer while uploading
|
||||
|
// and use the buffers content on retry.
|
||||
|
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 |
||||
|
} |
||||
|
|
||||
|
// AddAuthenticator appends the AuthFactory to our factories.
|
||||
|
// It converts the key to lower case and preserves the order.
|
||||
|
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}) |
||||
|
} |
||||
|
|
||||
|
// factory picks all valid Authenticators based on Www-Authenticate headers
|
||||
|
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 |
||||
|
} |
||||
|
|
||||
|
// setDefaultAuthenticator sets the default Authenticator
|
||||
|
func (a *authorizer) setDefaultAuthenticator(auth Authenticator) { |
||||
|
a.defAuthMux.Lock() |
||||
|
a.defAuth.Close() |
||||
|
a.defAuth = auth |
||||
|
a.defAuthMux.Unlock() |
||||
|
} |
||||
|
|
||||
|
// Authorize the current request
|
||||
|
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 |
||||
|
} |
||||
|
|
||||
|
// Verify checks for authentication issues and may trigger a re-authentication.
|
||||
|
// Catches AlgoChangedErr to update the current Authenticator
|
||||
|
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 |
||||
|
} |
||||
|
|
||||
|
// Close closes all resources
|
||||
|
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 |
||||
|
} |
||||
|
|
||||
|
// It's not intend to Clone the shim
|
||||
|
// therefore it returns a noAuth instance
|
||||
|
func (s *authShim) Clone() Authenticator { |
||||
|
return &noAuth{} |
||||
|
} |
||||
|
|
||||
|
// String toString
|
||||
|
func (s *authShim) String() string { |
||||
|
return "AuthShim" |
||||
|
} |
||||
|
|
||||
|
// Authorize authorizes the current request with the top most Authorizer
|
||||
|
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) |
||||
|
} |
||||
|
|
||||
|
// Verify verifies the authentication and selects the next one based on the result
|
||||
|
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) |
||||
|
} |
||||
|
|
||||
|
// Close will close the underlying authenticators.
|
||||
|
func (n *negoAuth) Close() error { |
||||
|
for _, a := range n.auths { |
||||
|
a.Close() |
||||
|
} |
||||
|
n.setDefaultAuthenticator = nil |
||||
|
return nil |
||||
|
} |
||||
|
|
||||
|
// Clone clones the underlying authenticators.
|
||||
|
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" |
||||
|
} |
||||
|
|
||||
|
// Authorize the current request
|
||||
|
func (n *noAuth) Authorize(c *http.Client, rq *http.Request, path string) error { |
||||
|
return nil |
||||
|
} |
||||
|
|
||||
|
// Verify checks for authentication issues and may trigger a re-authentication
|
||||
|
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 |
||||
|
} |
||||
|
|
||||
|
// Close closes all resources
|
||||
|
func (n *noAuth) Close() error { |
||||
|
return nil |
||||
|
} |
||||
|
|
||||
|
// Clone creates a copy of itself
|
||||
|
func (n *noAuth) Clone() Authenticator { |
||||
|
// no copy due to read only access
|
||||
|
return n |
||||
|
} |
||||
|
|
||||
|
// String toString
|
||||
|
func (n *noAuth) String() string { |
||||
|
return "NoAuth" |
||||
|
} |
||||
|
|
||||
|
// Authorize the current request
|
||||
|
func (n *nullAuth) Authorize(c *http.Client, rq *http.Request, path string) error { |
||||
|
rq.Header.Set(XInhibitRedirect, "1") |
||||
|
return nil |
||||
|
} |
||||
|
|
||||
|
// Verify checks for authentication issues and may trigger a re-authentication
|
||||
|
func (n *nullAuth) Verify(c *http.Client, rs *http.Response, path string) (redo bool, err error) { |
||||
|
return true, ErrAuthChanged |
||||
|
} |
||||
|
|
||||
|
// Close closes all resources
|
||||
|
func (n *nullAuth) Close() error { |
||||
|
return nil |
||||
|
} |
||||
|
|
||||
|
// Clone creates a copy of itself
|
||||
|
func (n *nullAuth) Clone() Authenticator { |
||||
|
// no copy due to read only access
|
||||
|
return n |
||||
|
} |
||||
|
|
||||
|
// String toString
|
||||
|
func (n *nullAuth) String() string { |
||||
|
return "NullAuth" |
||||
|
} |
||||
|
|
||||
|
// NewAuthenticator creates an Authenticator (Shim) per request
|
||||
|
func (b *preemptiveAuthorizer) NewAuthenticator(body io.Reader) (Authenticator, io.Reader) { |
||||
|
return b.auth.Clone(), body |
||||
|
} |
||||
|
|
||||
|
// AddAuthenticator Will PANIC because it may only have a single authentication method
|
||||
|
func (b *preemptiveAuthorizer) AddAuthenticator(key string, fn AuthFactory) { |
||||
|
panic("You're funny! A preemptive authorizer may only have a single authentication method") |
||||
|
} |
@ -0,0 +1,42 @@ |
|||||
|
package webdav |
||||
|
|
||||
|
import ( |
||||
|
"fmt" |
||||
|
"net/http" |
||||
|
) |
||||
|
|
||||
|
// BasicAuth structure holds our credentials
|
||||
|
type BasicAuth struct { |
||||
|
user string |
||||
|
pw string |
||||
|
} |
||||
|
|
||||
|
// Authorize the current request
|
||||
|
func (b *BasicAuth) Authorize(c *http.Client, rq *http.Request, path string) error { |
||||
|
rq.SetBasicAuth(b.user, b.pw) |
||||
|
return nil |
||||
|
} |
||||
|
|
||||
|
// Verify verifies if the authentication
|
||||
|
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 |
||||
|
} |
||||
|
|
||||
|
// Close cleans up all resources
|
||||
|
func (b *BasicAuth) Close() error { |
||||
|
return nil |
||||
|
} |
||||
|
|
||||
|
// Clone creates a Copy of itself
|
||||
|
func (b *BasicAuth) Clone() Authenticator { |
||||
|
// no copy due to read only access
|
||||
|
return b |
||||
|
} |
||||
|
|
||||
|
// String toString
|
||||
|
func (b *BasicAuth) String() string { |
||||
|
return fmt.Sprintf("BasicAuth login: %s", b.user) |
||||
|
} |
@ -0,0 +1,438 @@ |
|||||
|
package webdav |
||||
|
|
||||
|
import ( |
||||
|
"bytes" |
||||
|
"encoding/xml" |
||||
|
"fmt" |
||||
|
"io" |
||||
|
"net/http" |
||||
|
"net/url" |
||||
|
"os" |
||||
|
pathpkg "path" |
||||
|
"strings" |
||||
|
"time" |
||||
|
) |
||||
|
|
||||
|
const XInhibitRedirect = "X-Gowebdav-Inhibit-Redirect" |
||||
|
|
||||
|
// Client defines our structure
|
||||
|
type Client struct { |
||||
|
root string |
||||
|
headers http.Header |
||||
|
interceptor func(method string, rq *http.Request) |
||||
|
c *http.Client |
||||
|
auth Authorizer |
||||
|
} |
||||
|
|
||||
|
// NewClient creates a new instance of client
|
||||
|
func NewClient(uri, user, pw string) *Client { |
||||
|
return NewAuthClient(uri, NewAutoAuth(user, pw)) |
||||
|
} |
||||
|
|
||||
|
// NewAuthClient creates a new client instance with a custom Authorizer
|
||||
|
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), interceptor: nil, c: c, auth: auth} |
||||
|
} |
||||
|
|
||||
|
// SetHeader lets us set arbitrary headers for a given client
|
||||
|
func (c *Client) SetHeader(key, value string) { |
||||
|
c.headers.Add(key, value) |
||||
|
} |
||||
|
|
||||
|
// SetInterceptor lets us set an arbitrary interceptor for a given client
|
||||
|
func (c *Client) SetInterceptor(interceptor func(method string, rq *http.Request)) { |
||||
|
c.interceptor = interceptor |
||||
|
} |
||||
|
|
||||
|
// SetTimeout exposes the ability to set a time limit for requests
|
||||
|
func (c *Client) SetTimeout(timeout time.Duration) { |
||||
|
c.c.Timeout = timeout |
||||
|
} |
||||
|
|
||||
|
// SetTransport exposes the ability to define custom transports
|
||||
|
func (c *Client) SetTransport(transport http.RoundTripper) { |
||||
|
c.c.Transport = transport |
||||
|
} |
||||
|
|
||||
|
// SetJar exposes the ability to set a cookie jar to the client.
|
||||
|
func (c *Client) SetJar(jar http.CookieJar) { |
||||
|
c.c.Jar = jar |
||||
|
} |
||||
|
|
||||
|
// Connect connects to our dav server
|
||||
|
func (c *Client) Connect() error { |
||||
|
rs, err := c.options("/") |
||||
|
if err != nil { |
||||
|
return err |
||||
|
} |
||||
|
|
||||
|
err = rs.Body.Close() |
||||
|
if err != nil { |
||||
|
return err |
||||
|
} |
||||
|
|
||||
|
if rs.StatusCode != 200 { |
||||
|
return NewPathError("Connect", c.root, rs.StatusCode) |
||||
|
} |
||||
|
|
||||
|
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"` |
||||
|
ContentType string `xml:"DAV: prop>getcontenttype,omitempty"` |
||||
|
ETag string `xml:"DAV: prop>getetag,omitempty"` |
||||
|
Modified string `xml:"DAV: prop>getlastmodified,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 |
||||
|
} |
||||
|
|
||||
|
// ReadDir reads the contents of a remote directory
|
||||
|
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 = pathpkg.Base(ps) |
||||
|
} else { |
||||
|
f.name = p.Name |
||||
|
} |
||||
|
f.path = path + f.name |
||||
|
f.modified = parseModified(&p.Modified) |
||||
|
f.etag = p.ETag |
||||
|
f.contentType = p.ContentType |
||||
|
|
||||
|
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 |
||||
|
} |
||||
|
|
||||
|
err := c.propfind(path, false, |
||||
|
`<d:propfind xmlns:d='DAV:'> |
||||
|
<d:prop> |
||||
|
<d:displayname/> |
||||
|
<d:resourcetype/> |
||||
|
<d:getcontentlength/> |
||||
|
<d:getcontenttype/> |
||||
|
<d:getetag/> |
||||
|
<d:getlastmodified/> |
||||
|
</d:prop> |
||||
|
</d:propfind>`, |
||||
|
&response{}, |
||||
|
parse) |
||||
|
|
||||
|
if err != nil { |
||||
|
if _, ok := err.(*os.PathError); !ok { |
||||
|
err = NewPathErrorErr("ReadDir", path, err) |
||||
|
} |
||||
|
} |
||||
|
return files, err |
||||
|
} |
||||
|
|
||||
|
// Stat returns the file stats for a specified path
|
||||
|
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 |
||||
|
f.etag = p.ETag |
||||
|
f.contentType = p.ContentType |
||||
|
|
||||
|
if p.Type.Local == "collection" { |
||||
|
if !strings.HasSuffix(f.path, "/") { |
||||
|
f.path += "/" |
||||
|
} |
||||
|
f.size = 0 |
||||
|
f.modified = parseModified(&p.Modified) |
||||
|
f.isdir = true |
||||
|
} else { |
||||
|
f.size = parseInt64(&p.Size) |
||||
|
f.modified = parseModified(&p.Modified) |
||||
|
f.isdir = false |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
r.Props = nil |
||||
|
return nil |
||||
|
} |
||||
|
|
||||
|
err := c.propfind(path, true, |
||||
|
`<d:propfind xmlns:d='DAV:'> |
||||
|
<d:prop> |
||||
|
<d:displayname/> |
||||
|
<d:resourcetype/> |
||||
|
<d:getcontentlength/> |
||||
|
<d:getcontenttype/> |
||||
|
<d:getetag/> |
||||
|
<d:getlastmodified/> |
||||
|
</d:prop> |
||||
|
</d:propfind>`, |
||||
|
&response{}, |
||||
|
parse) |
||||
|
|
||||
|
if err != nil { |
||||
|
if _, ok := err.(*os.PathError); !ok { |
||||
|
err = NewPathErrorErr("ReadDir", path, err) |
||||
|
} |
||||
|
} |
||||
|
return f, err |
||||
|
} |
||||
|
|
||||
|
// Remove removes a remote file
|
||||
|
func (c *Client) Remove(path string) error { |
||||
|
return c.RemoveAll(path) |
||||
|
} |
||||
|
|
||||
|
// RemoveAll removes remote files
|
||||
|
func (c *Client) RemoveAll(path string) error { |
||||
|
rs, err := c.req("DELETE", path, nil, nil) |
||||
|
if err != nil { |
||||
|
return NewPathError("Remove", path, 400) |
||||
|
} |
||||
|
err = rs.Body.Close() |
||||
|
if err != nil { |
||||
|
return err |
||||
|
} |
||||
|
|
||||
|
if rs.StatusCode == 200 || rs.StatusCode == 204 || rs.StatusCode == 404 { |
||||
|
return nil |
||||
|
} |
||||
|
|
||||
|
return NewPathError("Remove", path, rs.StatusCode) |
||||
|
} |
||||
|
|
||||
|
// Mkdir makes a directory
|
||||
|
func (c *Client) Mkdir(path string, _ os.FileMode) (err error) { |
||||
|
path = FixSlashes(path) |
||||
|
status, err := c.mkcol(path) |
||||
|
if err != nil { |
||||
|
return |
||||
|
} |
||||
|
if status == 201 { |
||||
|
return nil |
||||
|
} |
||||
|
|
||||
|
return NewPathError("Mkdir", path, status) |
||||
|
} |
||||
|
|
||||
|
// MkdirAll like mkdir -p, but for webdav
|
||||
|
func (c *Client) MkdirAll(path string, _ os.FileMode) (err error) { |
||||
|
path = FixSlashes(path) |
||||
|
status, err := c.mkcol(path) |
||||
|
if err != nil { |
||||
|
return |
||||
|
} |
||||
|
if status == 201 { |
||||
|
return nil |
||||
|
} |
||||
|
if status == 409 { |
||||
|
paths := strings.Split(path, "/") |
||||
|
sub := "/" |
||||
|
for _, e := range paths { |
||||
|
if e == "" { |
||||
|
continue |
||||
|
} |
||||
|
sub += e + "/" |
||||
|
status, err = c.mkcol(sub) |
||||
|
if err != nil { |
||||
|
return |
||||
|
} |
||||
|
if status != 201 { |
||||
|
return NewPathError("MkdirAll", sub, status) |
||||
|
} |
||||
|
} |
||||
|
return nil |
||||
|
} |
||||
|
|
||||
|
return NewPathError("MkdirAll", path, status) |
||||
|
} |
||||
|
|
||||
|
// Rename moves a file from A to B
|
||||
|
func (c *Client) Rename(oldpath, newpath string, overwrite bool) error { |
||||
|
return c.copymove("MOVE", oldpath, newpath, overwrite) |
||||
|
} |
||||
|
|
||||
|
// Copy copies a file from A to B
|
||||
|
func (c *Client) Copy(oldpath, newpath string, overwrite bool) error { |
||||
|
return c.copymove("COPY", oldpath, newpath, overwrite) |
||||
|
} |
||||
|
|
||||
|
// Read reads the contents of a remote file
|
||||
|
func (c *Client) Read(path string) ([]byte, error) { |
||||
|
var stream io.ReadCloser |
||||
|
var err error |
||||
|
|
||||
|
if stream, err = c.ReadStream(path); err != nil { |
||||
|
return nil, err |
||||
|
} |
||||
|
defer stream.Close() |
||||
|
|
||||
|
buf := new(bytes.Buffer) |
||||
|
_, err = buf.ReadFrom(stream) |
||||
|
if err != nil { |
||||
|
return nil, err |
||||
|
} |
||||
|
return buf.Bytes(), nil |
||||
|
} |
||||
|
|
||||
|
// ReadStream reads the stream for a given path
|
||||
|
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) |
||||
|
} |
||||
|
|
||||
|
// ReadStreamRange reads the stream representing a subset of bytes for a given path,
|
||||
|
// utilizing HTTP Range Requests if the server supports it.
|
||||
|
// The range is expressed as offset from the start of the file and length, for example
|
||||
|
// offset=10, length=10 will return bytes 10 through 19.
|
||||
|
//
|
||||
|
// If the server does not support partial content requests and returns full content instead,
|
||||
|
// this function will emulate the behavior by skipping `offset` bytes and limiting the result
|
||||
|
// to `length`.
|
||||
|
func (c *Client) ReadStreamRange(path string, offset, length int64) (io.ReadCloser, error) { |
||||
|
rs, err := c.req("GET", path, nil, func(r *http.Request) { |
||||
|
if length > 0 { |
||||
|
r.Header.Add("Range", fmt.Sprintf("bytes=%d-%d", offset, offset+length-1)) |
||||
|
} else { |
||||
|
r.Header.Add("Range", fmt.Sprintf("bytes=%d-", offset)) |
||||
|
} |
||||
|
}) |
||||
|
if err != nil { |
||||
|
return nil, NewPathErrorErr("ReadStreamRange", path, err) |
||||
|
} |
||||
|
|
||||
|
if rs.StatusCode == http.StatusPartialContent { |
||||
|
// server supported partial content, return as-is.
|
||||
|
return rs.Body, nil |
||||
|
} |
||||
|
|
||||
|
// server returned success, but did not support partial content, so we have the whole
|
||||
|
// stream in rs.Body
|
||||
|
if rs.StatusCode == 200 { |
||||
|
// discard first 'offset' bytes.
|
||||
|
if _, err := io.Copy(io.Discard, io.LimitReader(rs.Body, offset)); err != nil { |
||||
|
return nil, NewPathErrorErr("ReadStreamRange", path, err) |
||||
|
} |
||||
|
|
||||
|
// return a io.ReadCloser that is limited to `length` bytes.
|
||||
|
return &limitedReadCloser{rc: rs.Body, remaining: int(length)}, nil |
||||
|
} |
||||
|
|
||||
|
rs.Body.Close() |
||||
|
return nil, NewPathError("ReadStream", path, rs.StatusCode) |
||||
|
} |
||||
|
|
||||
|
// Write writes data to a given path
|
||||
|
func (c *Client) Write(path string, data []byte, _ os.FileMode) (err error) { |
||||
|
s, err := c.put(path, bytes.NewReader(data)) |
||||
|
if err != nil { |
||||
|
return |
||||
|
} |
||||
|
|
||||
|
switch s { |
||||
|
|
||||
|
case 200, 201, 204: |
||||
|
return nil |
||||
|
|
||||
|
case 404, 409: |
||||
|
err = c.createParentCollection(path) |
||||
|
if err != nil { |
||||
|
return |
||||
|
} |
||||
|
|
||||
|
s, err = c.put(path, bytes.NewReader(data)) |
||||
|
if err != nil { |
||||
|
return |
||||
|
} |
||||
|
if s == 200 || s == 201 || s == 204 { |
||||
|
return |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
return NewPathError("Write", path, s) |
||||
|
} |
||||
|
|
||||
|
// WriteStream writes a stream
|
||||
|
func (c *Client) WriteStream(path string, stream io.Reader, _ os.FileMode) (err error) { |
||||
|
|
||||
|
err = c.createParentCollection(path) |
||||
|
if err != nil { |
||||
|
return err |
||||
|
} |
||||
|
|
||||
|
s, err := c.put(path, stream) |
||||
|
if err != nil { |
||||
|
return err |
||||
|
} |
||||
|
|
||||
|
switch s { |
||||
|
case 200, 201, 204: |
||||
|
return nil |
||||
|
|
||||
|
default: |
||||
|
return NewPathError("WriteStream", path, s) |
||||
|
} |
||||
|
} |
@ -0,0 +1,183 @@ |
|||||
|
package webdav |
||||
|
|
||||
|
import ( |
||||
|
"crypto/md5" |
||||
|
"crypto/rand" |
||||
|
"encoding/hex" |
||||
|
"fmt" |
||||
|
"io" |
||||
|
"net/http" |
||||
|
"strings" |
||||
|
) |
||||
|
|
||||
|
// DigestAuth structure holds our credentials
|
||||
|
type DigestAuth struct { |
||||
|
user string |
||||
|
pw string |
||||
|
digestParts map[string]string |
||||
|
} |
||||
|
|
||||
|
// NewDigestAuth creates a new instance of our Digest Authenticator
|
||||
|
func NewDigestAuth(login, secret string, rs *http.Response) (Authenticator, error) { |
||||
|
return &DigestAuth{user: login, pw: secret, digestParts: digestParts(rs)}, nil |
||||
|
} |
||||
|
|
||||
|
// Authorize the current request
|
||||
|
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 |
||||
|
} |
||||
|
|
||||
|
// Verify checks for authentication issues and may trigger a re-authentication
|
||||
|
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 |
||||
|
} |
||||
|
|
||||
|
// Close cleans up all resources
|
||||
|
func (d *DigestAuth) Close() error { |
||||
|
return nil |
||||
|
} |
||||
|
|
||||
|
// Clone creates a copy of itself
|
||||
|
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} |
||||
|
} |
||||
|
|
||||
|
// String toString
|
||||
|
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 |
||||
|
// These are the correct ha1 and ha2 for qop=auth. We should probably check for other types of qop.
|
||||
|
|
||||
|
var ( |
||||
|
ha1 string |
||||
|
ha2 string |
||||
|
nonceCount = 00000001 |
||||
|
cnonce = getCnonce() |
||||
|
response string |
||||
|
) |
||||
|
|
||||
|
// 'ha1' value depends on value of "algorithm" field
|
||||
|
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, |
||||
|
), |
||||
|
) |
||||
|
} |
||||
|
|
||||
|
// 'ha2' value depends on value of "qop" field
|
||||
|
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"])) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// 'response' value depends on value of "qop" field
|
||||
|
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 |
||||
|
} |
@ -0,0 +1,57 @@ |
|||||
|
package webdav |
||||
|
|
||||
|
import ( |
||||
|
"errors" |
||||
|
"fmt" |
||||
|
"os" |
||||
|
) |
||||
|
|
||||
|
// ErrAuthChanged must be returned from the Verify method as an error
|
||||
|
// to trigger a re-authentication / negotiation with a new authenticator.
|
||||
|
var ErrAuthChanged = errors.New("authentication failed, change algorithm") |
||||
|
|
||||
|
// ErrTooManyRedirects will be used as return error if a request exceeds 10 redirects.
|
||||
|
var ErrTooManyRedirects = errors.New("stopped after 10 redirects") |
||||
|
|
||||
|
// StatusError implements error and wraps
|
||||
|
// an erroneous status code.
|
||||
|
type StatusError struct { |
||||
|
Status int |
||||
|
} |
||||
|
|
||||
|
func (se StatusError) Error() string { |
||||
|
return fmt.Sprintf("%d", se.Status) |
||||
|
} |
||||
|
|
||||
|
// IsErrCode returns true if the given error
|
||||
|
// is an os.PathError wrapping a StatusError
|
||||
|
// with the given status code.
|
||||
|
func IsErrCode(err error, code int) bool { |
||||
|
if pe, ok := err.(*os.PathError); ok { |
||||
|
se, ok := pe.Err.(StatusError) |
||||
|
return ok && se.Status == code |
||||
|
} |
||||
|
return false |
||||
|
} |
||||
|
|
||||
|
// IsErrNotFound is shorthand for IsErrCode
|
||||
|
// for status 404.
|
||||
|
func IsErrNotFound(err error) bool { |
||||
|
return IsErrCode(err, 404) |
||||
|
} |
||||
|
|
||||
|
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, |
||||
|
} |
||||
|
} |
@ -0,0 +1,77 @@ |
|||||
|
package webdav |
||||
|
|
||||
|
import ( |
||||
|
"fmt" |
||||
|
"os" |
||||
|
"time" |
||||
|
) |
||||
|
|
||||
|
// File is our structure for a given file
|
||||
|
type File struct { |
||||
|
path string |
||||
|
name string |
||||
|
contentType string |
||||
|
size int64 |
||||
|
modified time.Time |
||||
|
etag string |
||||
|
isdir bool |
||||
|
} |
||||
|
|
||||
|
// Path returns the full path of a file
|
||||
|
func (f File) Path() string { |
||||
|
return f.path |
||||
|
} |
||||
|
|
||||
|
// Name returns the name of a file
|
||||
|
func (f File) Name() string { |
||||
|
return f.name |
||||
|
} |
||||
|
|
||||
|
// ContentType returns the content type of a file
|
||||
|
func (f File) ContentType() string { |
||||
|
return f.contentType |
||||
|
} |
||||
|
|
||||
|
// Size returns the size of a file
|
||||
|
func (f File) Size() int64 { |
||||
|
return f.size |
||||
|
} |
||||
|
|
||||
|
// Mode will return the mode of a given file
|
||||
|
func (f File) Mode() os.FileMode { |
||||
|
// TODO check webdav perms
|
||||
|
if f.isdir { |
||||
|
return 0775 | os.ModeDir |
||||
|
} |
||||
|
|
||||
|
return 0664 |
||||
|
} |
||||
|
|
||||
|
// ModTime returns the modified time of a file
|
||||
|
func (f File) ModTime() time.Time { |
||||
|
return f.modified |
||||
|
} |
||||
|
|
||||
|
// ETag returns the ETag of a file
|
||||
|
func (f File) ETag() string { |
||||
|
return f.etag |
||||
|
} |
||||
|
|
||||
|
// IsDir let us see if a given file is a directory or not
|
||||
|
func (f File) IsDir() bool { |
||||
|
return f.isdir |
||||
|
} |
||||
|
|
||||
|
// Sys ????
|
||||
|
func (f File) Sys() interface{} { |
||||
|
return nil |
||||
|
} |
||||
|
|
||||
|
// String lets us see file information
|
||||
|
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) |
||||
|
} |
@ -0,0 +1,229 @@ |
|||||
|
package webdav |
||||
|
|
||||
|
import ( |
||||
|
"fmt" |
||||
|
"godo/libs" |
||||
|
"io" |
||||
|
"net/http" |
||||
|
|
||||
|
"github.com/gorilla/mux" |
||||
|
) |
||||
|
|
||||
|
var client *Client |
||||
|
|
||||
|
func InitWebdav() error { |
||||
|
clientInfo, ok := libs.GetConfig("webdavClient") |
||||
|
if !ok { |
||||
|
return fmt.Errorf("failed to find configuration for webdavClient") |
||||
|
} |
||||
|
|
||||
|
infoMap, ok := clientInfo.(map[string]string) |
||||
|
if !ok { |
||||
|
return fmt.Errorf("configuration for webdavClient is not a map[string]string") |
||||
|
} |
||||
|
|
||||
|
url, ok := infoMap["url"] |
||||
|
if !ok { |
||||
|
return fmt.Errorf("missing 'url' in webdavClient configuration") |
||||
|
} |
||||
|
|
||||
|
username, ok := infoMap["username"] |
||||
|
if !ok { |
||||
|
return fmt.Errorf("missing 'username' in webdavClient configuration") |
||||
|
} |
||||
|
|
||||
|
password, ok := infoMap["password"] |
||||
|
if !ok { |
||||
|
return fmt.Errorf("missing 'password' in webdavClient configuration") |
||||
|
} |
||||
|
|
||||
|
client = NewClient(url, username, password) |
||||
|
|
||||
|
if err := client.Connect(); err != nil { |
||||
|
return fmt.Errorf("failed to connect to WebDAV server: %v", err) |
||||
|
} |
||||
|
return nil |
||||
|
} |
||||
|
func HandlePing(w http.ResponseWriter, r *http.Request) { |
||||
|
err := client.Connect() |
||||
|
if err != nil { |
||||
|
w.WriteHeader(http.StatusInternalServerError) |
||||
|
w.Write([]byte(err.Error())) |
||||
|
return |
||||
|
} |
||||
|
w.WriteHeader(http.StatusOK) |
||||
|
} |
||||
|
|
||||
|
// HandleReadDir: 读取目录内容
|
||||
|
func HandleReadDir(w http.ResponseWriter, r *http.Request) { |
||||
|
vars := mux.Vars(r) |
||||
|
path := vars["path"] |
||||
|
files, err := client.ReadDir(path) |
||||
|
if err != nil { |
||||
|
http.Error(w, err.Error(), http.StatusInternalServerError) |
||||
|
return |
||||
|
} |
||||
|
for _, f := range files { |
||||
|
w.Write([]byte(f.Name() + "\n")) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// HandleStat: 获取文件状态
|
||||
|
func HandleStat(w http.ResponseWriter, r *http.Request) { |
||||
|
vars := mux.Vars(r) |
||||
|
path := vars["path"] |
||||
|
fi, err := client.Stat(path) |
||||
|
if err != nil { |
||||
|
http.Error(w, err.Error(), http.StatusInternalServerError) |
||||
|
return |
||||
|
} |
||||
|
fileInfo, ok := fi.(*File) |
||||
|
if !ok { |
||||
|
http.Error(w, "Unexpected file info type", http.StatusInternalServerError) |
||||
|
return |
||||
|
} |
||||
|
// 直接调用 fi.String(),因为 fi 应该是 *File 类型
|
||||
|
w.Write([]byte(fileInfo.String())) |
||||
|
} |
||||
|
|
||||
|
// HandleChmod: 改变文件权限 (不支持)
|
||||
|
func HandleChmod(w http.ResponseWriter, r *http.Request) { |
||||
|
http.Error(w, "Not supported", http.StatusNotImplemented) |
||||
|
} |
||||
|
|
||||
|
// HandleExists: 检查文件是否存在
|
||||
|
func HandleExists(w http.ResponseWriter, r *http.Request) { |
||||
|
vars := mux.Vars(r) |
||||
|
path := vars["path"] |
||||
|
_, err := client.Stat(path) |
||||
|
if err != nil { |
||||
|
http.Error(w, "File does not exist", http.StatusNotFound) |
||||
|
return |
||||
|
} |
||||
|
w.Write([]byte("File exists")) |
||||
|
} |
||||
|
|
||||
|
// HandleReadFile: 读取文件内容
|
||||
|
func HandleReadFile(w http.ResponseWriter, r *http.Request) { |
||||
|
vars := mux.Vars(r) |
||||
|
path := vars["path"] |
||||
|
data, err := client.Read(path) |
||||
|
if err != nil { |
||||
|
http.Error(w, err.Error(), http.StatusInternalServerError) |
||||
|
return |
||||
|
} |
||||
|
w.Write(data) |
||||
|
} |
||||
|
|
||||
|
// HandleUnlink: 删除文件
|
||||
|
func HandleUnlink(w http.ResponseWriter, r *http.Request) { |
||||
|
vars := mux.Vars(r) |
||||
|
path := vars["path"] |
||||
|
err := client.Remove(path) |
||||
|
if err != nil { |
||||
|
http.Error(w, err.Error(), http.StatusInternalServerError) |
||||
|
return |
||||
|
} |
||||
|
w.Write([]byte("File deleted")) |
||||
|
} |
||||
|
|
||||
|
// HandleClear: 清空目录 (不支持)
|
||||
|
func HandleClear(w http.ResponseWriter, r *http.Request) { |
||||
|
http.Error(w, "Not supported", http.StatusNotImplemented) |
||||
|
} |
||||
|
|
||||
|
// HandleRename: 重命名文件或目录
|
||||
|
func HandleRename(w http.ResponseWriter, r *http.Request) { |
||||
|
vars := mux.Vars(r) |
||||
|
oldPath := vars["oldPath"] |
||||
|
newPath := vars["newPath"] |
||||
|
err := client.Rename(oldPath, newPath, false) |
||||
|
if err != nil { |
||||
|
http.Error(w, err.Error(), http.StatusInternalServerError) |
||||
|
return |
||||
|
} |
||||
|
w.Write([]byte("File renamed")) |
||||
|
} |
||||
|
|
||||
|
// HandleMkdir: 创建目录
|
||||
|
func HandleMkdir(w http.ResponseWriter, r *http.Request) { |
||||
|
vars := mux.Vars(r) |
||||
|
path := vars["path"] |
||||
|
err := client.Mkdir(path, 0) |
||||
|
if err != nil { |
||||
|
http.Error(w, err.Error(), http.StatusInternalServerError) |
||||
|
return |
||||
|
} |
||||
|
w.Write([]byte("Directory created")) |
||||
|
} |
||||
|
|
||||
|
// HandleRmdir: 删除目录
|
||||
|
func HandleRmdir(w http.ResponseWriter, r *http.Request) { |
||||
|
vars := mux.Vars(r) |
||||
|
path := vars["path"] |
||||
|
err := client.RemoveAll(path) |
||||
|
if err != nil { |
||||
|
http.Error(w, err.Error(), http.StatusInternalServerError) |
||||
|
return |
||||
|
} |
||||
|
w.Write([]byte("Directory removed")) |
||||
|
} |
||||
|
|
||||
|
// HandleCopyFile: 复制文件
|
||||
|
func HandleCopyFile(w http.ResponseWriter, r *http.Request) { |
||||
|
vars := mux.Vars(r) |
||||
|
oldPath := vars["oldPath"] |
||||
|
newPath := vars["newPath"] |
||||
|
err := client.Copy(oldPath, newPath, false) |
||||
|
if err != nil { |
||||
|
http.Error(w, err.Error(), http.StatusInternalServerError) |
||||
|
return |
||||
|
} |
||||
|
w.Write([]byte("File copied")) |
||||
|
} |
||||
|
|
||||
|
// HandleWriteFile: 写入文件
|
||||
|
func HandleWriteFile(w http.ResponseWriter, r *http.Request) { |
||||
|
vars := mux.Vars(r) |
||||
|
path := vars["path"] |
||||
|
body, err := io.ReadAll(r.Body) |
||||
|
if err != nil { |
||||
|
http.Error(w, err.Error(), http.StatusInternalServerError) |
||||
|
return |
||||
|
} |
||||
|
err = client.Write(path, body, 0) |
||||
|
if err != nil { |
||||
|
http.Error(w, err.Error(), http.StatusInternalServerError) |
||||
|
return |
||||
|
} |
||||
|
w.Write([]byte("File written")) |
||||
|
} |
||||
|
|
||||
|
// HandleAppendFile: 追加到文件
|
||||
|
func HandleAppendFile(w http.ResponseWriter, r *http.Request) { |
||||
|
vars := mux.Vars(r) |
||||
|
path := vars["path"] |
||||
|
body, err := io.ReadAll(r.Body) |
||||
|
if err != nil { |
||||
|
http.Error(w, err.Error(), http.StatusInternalServerError) |
||||
|
return |
||||
|
} |
||||
|
// 读取现有文件内容
|
||||
|
existingContent, err := client.Read(path) |
||||
|
if err != nil && !IsErrNotFound(err) { |
||||
|
http.Error(w, err.Error(), http.StatusInternalServerError) |
||||
|
return |
||||
|
} |
||||
|
// 如果文件不存在,则创建新文件
|
||||
|
if IsErrNotFound(err) { |
||||
|
err = client.Write(path, body, 0) |
||||
|
} else { |
||||
|
// 如果文件存在,则追加内容
|
||||
|
err = client.Write(path, append(existingContent, body...), 0) |
||||
|
} |
||||
|
if err != nil { |
||||
|
http.Error(w, err.Error(), http.StatusInternalServerError) |
||||
|
return |
||||
|
} |
||||
|
w.Write([]byte("File appended")) |
||||
|
} |
@ -0,0 +1,54 @@ |
|||||
|
package webdav |
||||
|
|
||||
|
import ( |
||||
|
"bufio" |
||||
|
"fmt" |
||||
|
"net/url" |
||||
|
"os" |
||||
|
"regexp" |
||||
|
"strings" |
||||
|
) |
||||
|
|
||||
|
func parseLine(s string) (login, pass string) { |
||||
|
fields := strings.Fields(s) |
||||
|
for i, f := range fields { |
||||
|
if f == "login" { |
||||
|
login = fields[i+1] |
||||
|
} |
||||
|
if f == "password" { |
||||
|
pass = fields[i+1] |
||||
|
} |
||||
|
} |
||||
|
return login, pass |
||||
|
} |
||||
|
|
||||
|
// ReadConfig reads login and password configuration from ~/.netrc
|
||||
|
// machine foo.com login username password 123456
|
||||
|
func ReadConfig(uri, netrc string) (string, string) { |
||||
|
u, err := url.Parse(uri) |
||||
|
if err != nil { |
||||
|
return "", "" |
||||
|
} |
||||
|
|
||||
|
file, err := os.Open(netrc) |
||||
|
if err != nil { |
||||
|
return "", "" |
||||
|
} |
||||
|
defer file.Close() |
||||
|
|
||||
|
re := fmt.Sprintf(`^.*machine %s.*$`, u.Host) |
||||
|
scanner := bufio.NewScanner(file) |
||||
|
for scanner.Scan() { |
||||
|
s := scanner.Text() |
||||
|
|
||||
|
matched, err := regexp.MatchString(re, s) |
||||
|
if err != nil { |
||||
|
return "", "" |
||||
|
} |
||||
|
if matched { |
||||
|
return parseLine(s) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
return "", "" |
||||
|
} |
@ -0,0 +1,181 @@ |
|||||
|
package webdav |
||||
|
|
||||
|
import ( |
||||
|
"fmt" |
||||
|
"io" |
||||
|
"net/http" |
||||
|
"net/url" |
||||
|
"strings" |
||||
|
) |
||||
|
|
||||
|
// PassportAuth structure holds our credentials
|
||||
|
type PassportAuth struct { |
||||
|
user string |
||||
|
pw string |
||||
|
cookies []http.Cookie |
||||
|
inhibitRedirect bool |
||||
|
} |
||||
|
|
||||
|
// constructor for PassportAuth creates a new PassportAuth object and
|
||||
|
// automatically authenticates against the given partnerURL
|
||||
|
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 |
||||
|
} |
||||
|
|
||||
|
// Authorize the current request
|
||||
|
func (p *PassportAuth) Authorize(c *http.Client, rq *http.Request, path string) error { |
||||
|
// prevent redirects to detect subsequent authentication requests
|
||||
|
if p.inhibitRedirect { |
||||
|
rq.Header.Set(XInhibitRedirect, "1") |
||||
|
} else { |
||||
|
p.inhibitRedirect = true |
||||
|
} |
||||
|
for _, cookie := range p.cookies { |
||||
|
rq.AddCookie(&cookie) |
||||
|
} |
||||
|
return nil |
||||
|
} |
||||
|
|
||||
|
// Verify verifies if the authentication is good
|
||||
|
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") != "" { |
||||
|
// re-authentication required as we are redirected to the login page
|
||||
|
err = p.genCookies(c, rs.Request.URL.String(), &rs.Header) |
||||
|
} else { |
||||
|
// just a redirect, follow it
|
||||
|
p.inhibitRedirect = false |
||||
|
} |
||||
|
case 401: |
||||
|
err = NewPathError("Authorize", path, rs.StatusCode) |
||||
|
} |
||||
|
return |
||||
|
} |
||||
|
|
||||
|
// Close cleans up all resources
|
||||
|
func (p *PassportAuth) Close() error { |
||||
|
return nil |
||||
|
} |
||||
|
|
||||
|
// Clone creates a Copy of itself
|
||||
|
func (p *PassportAuth) Clone() Authenticator { |
||||
|
// create a copy to allow independent cookie updates
|
||||
|
clonedCookies := make([]http.Cookie, len(p.cookies)) |
||||
|
copy(clonedCookies, p.cookies) |
||||
|
|
||||
|
return &PassportAuth{ |
||||
|
user: p.user, |
||||
|
pw: p.pw, |
||||
|
cookies: clonedCookies, |
||||
|
inhibitRedirect: true, |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// String toString
|
||||
|
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 { |
||||
|
// For more details refer to:
|
||||
|
// https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-pass/2c80637d-438c-4d4b-adc5-903170a779f3
|
||||
|
// Skipping step 1 and 2 as we already have the partner server challenge
|
||||
|
|
||||
|
baseAuthenticationServer := header.Get("Location") |
||||
|
baseAuthenticationServerURL, err := url.Parse(baseAuthenticationServer) |
||||
|
if err != nil { |
||||
|
return err |
||||
|
} |
||||
|
|
||||
|
// Skipping step 3 and 4 as we already know that we need and have the user's credentials
|
||||
|
// Step 5 (Sign-in request)
|
||||
|
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) |
||||
|
} |
||||
|
|
||||
|
// Step 6 (Token Response from Authentication Server)
|
||||
|
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) |
||||
|
} |
||||
|
|
||||
|
// Step 7 (First Authentication Request to Partner Server)
|
||||
|
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) |
||||
|
} |
||||
|
|
||||
|
// Step 8 (Set Token Message from Partner Server)
|
||||
|
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 |
||||
|
} |
@ -0,0 +1,181 @@ |
|||||
|
package webdav |
||||
|
|
||||
|
import ( |
||||
|
"io" |
||||
|
"log" |
||||
|
"net/http" |
||||
|
"path" |
||||
|
"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 { // TODO auth.continue() strategy(true|n times|until)?
|
||||
|
if r, err = http.NewRequest(method, uri, body); err != nil { |
||||
|
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 c.interceptor != nil { |
||||
|
c.interceptor(method, r) |
||||
|
} |
||||
|
|
||||
|
if rs, err = c.c.Do(r); err != nil { |
||||
|
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) mkcol(path string) (status int, err error) { |
||||
|
rs, err := c.req("MKCOL", path, nil, nil) |
||||
|
if err != nil { |
||||
|
return |
||||
|
} |
||||
|
defer rs.Body.Close() |
||||
|
|
||||
|
status = rs.StatusCode |
||||
|
if status == 405 { |
||||
|
status = 201 |
||||
|
} |
||||
|
|
||||
|
return |
||||
|
} |
||||
|
|
||||
|
func (c *Client) options(path string) (*http.Response, error) { |
||||
|
return c.req("OPTIONS", path, nil, func(rq *http.Request) { |
||||
|
rq.Header.Add("Depth", "0") |
||||
|
}) |
||||
|
} |
||||
|
|
||||
|
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) doCopyMove( |
||||
|
method string, |
||||
|
oldpath string, |
||||
|
newpath string, |
||||
|
overwrite bool, |
||||
|
) ( |
||||
|
status int, |
||||
|
r io.ReadCloser, |
||||
|
err error, |
||||
|
) { |
||||
|
rs, err := c.req(method, oldpath, nil, func(rq *http.Request) { |
||||
|
rq.Header.Add("Destination", PathEscape(Join(c.root, newpath))) |
||||
|
if overwrite { |
||||
|
rq.Header.Add("Overwrite", "T") |
||||
|
} else { |
||||
|
rq.Header.Add("Overwrite", "F") |
||||
|
} |
||||
|
}) |
||||
|
if err != nil { |
||||
|
return |
||||
|
} |
||||
|
status = rs.StatusCode |
||||
|
r = rs.Body |
||||
|
return |
||||
|
} |
||||
|
|
||||
|
func (c *Client) copymove(method string, oldpath string, newpath string, overwrite bool) (err error) { |
||||
|
s, data, err := c.doCopyMove(method, oldpath, newpath, overwrite) |
||||
|
if err != nil { |
||||
|
return |
||||
|
} |
||||
|
if data != nil { |
||||
|
defer data.Close() |
||||
|
} |
||||
|
|
||||
|
switch s { |
||||
|
case 201, 204: |
||||
|
return nil |
||||
|
|
||||
|
case 207: |
||||
|
// TODO handle multistat errors, worst case ...
|
||||
|
log.Printf("TODO handle %s - %s multistatus result %s\n", method, oldpath, String(data)) |
||||
|
|
||||
|
case 409: |
||||
|
err := c.createParentCollection(newpath) |
||||
|
if err != nil { |
||||
|
return err |
||||
|
} |
||||
|
|
||||
|
return c.copymove(method, oldpath, newpath, overwrite) |
||||
|
} |
||||
|
|
||||
|
return NewPathError(method, oldpath, s) |
||||
|
} |
||||
|
|
||||
|
func (c *Client) put(path string, stream io.Reader) (status int, err error) { |
||||
|
rs, err := c.req("PUT", path, stream, nil) |
||||
|
if err != nil { |
||||
|
return |
||||
|
} |
||||
|
defer rs.Body.Close() |
||||
|
|
||||
|
status = rs.StatusCode |
||||
|
return |
||||
|
} |
||||
|
|
||||
|
func (c *Client) createParentCollection(itemPath string) (err error) { |
||||
|
parentPath := path.Dir(itemPath) |
||||
|
if parentPath == "." || parentPath == "/" { |
||||
|
return nil |
||||
|
} |
||||
|
|
||||
|
return c.MkdirAll(parentPath, 0755) |
||||
|
} |
@ -0,0 +1,113 @@ |
|||||
|
package webdav |
||||
|
|
||||
|
import ( |
||||
|
"bytes" |
||||
|
"encoding/xml" |
||||
|
"io" |
||||
|
"net/url" |
||||
|
"strconv" |
||||
|
"strings" |
||||
|
"time" |
||||
|
) |
||||
|
|
||||
|
// PathEscape escapes all segments of a given path
|
||||
|
func PathEscape(path string) string { |
||||
|
s := strings.Split(path, "/") |
||||
|
for i, e := range s { |
||||
|
s[i] = url.PathEscape(e) |
||||
|
} |
||||
|
return strings.Join(s, "/") |
||||
|
} |
||||
|
|
||||
|
// FixSlash appends a trailing / to our string
|
||||
|
func FixSlash(s string) string { |
||||
|
if !strings.HasSuffix(s, "/") { |
||||
|
s += "/" |
||||
|
} |
||||
|
return s |
||||
|
} |
||||
|
|
||||
|
// FixSlashes appends and prepends a / if they are missing
|
||||
|
func FixSlashes(s string) string { |
||||
|
if !strings.HasPrefix(s, "/") { |
||||
|
s = "/" + s |
||||
|
} |
||||
|
|
||||
|
return FixSlash(s) |
||||
|
} |
||||
|
|
||||
|
// Join joins two paths
|
||||
|
func Join(path0 string, path1 string) string { |
||||
|
return strings.TrimSuffix(path0, "/") + "/" + strings.TrimPrefix(path1, "/") |
||||
|
} |
||||
|
|
||||
|
// String pulls a string out of our io.Reader
|
||||
|
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 parseUint(s *string) uint { |
||||
|
if n, e := strconv.ParseUint(*s, 10, 32); e == nil { |
||||
|
return uint(n) |
||||
|
} |
||||
|
return 0 |
||||
|
} |
||||
|
|
||||
|
func parseInt64(s *string) int64 { |
||||
|
if n, e := strconv.ParseInt(*s, 10, 64); e == nil { |
||||
|
return n |
||||
|
} |
||||
|
return 0 |
||||
|
} |
||||
|
|
||||
|
func parseModified(s *string) time.Time { |
||||
|
if t, e := time.Parse(time.RFC1123, *s); e == nil { |
||||
|
return t |
||||
|
} |
||||
|
return time.Unix(0, 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 |
||||
|
} |
||||
|
|
||||
|
// limitedReadCloser wraps a io.ReadCloser and limits the number of bytes that can be read from it.
|
||||
|
type limitedReadCloser struct { |
||||
|
rc io.ReadCloser |
||||
|
remaining int |
||||
|
} |
||||
|
|
||||
|
func (l *limitedReadCloser) Read(buf []byte) (int, error) { |
||||
|
if l.remaining <= 0 { |
||||
|
return 0, io.EOF |
||||
|
} |
||||
|
|
||||
|
if len(buf) > l.remaining { |
||||
|
buf = buf[0:l.remaining] |
||||
|
} |
||||
|
|
||||
|
n, err := l.rc.Read(buf) |
||||
|
l.remaining -= n |
||||
|
|
||||
|
return n, err |
||||
|
} |
||||
|
|
||||
|
func (l *limitedReadCloser) Close() error { |
||||
|
return l.rc.Close() |
||||
|
} |
Loading…
Reference in new issue