mirror of https://github.com/Xhofe/alist
				
				
				
			
		
			
				
	
	
		
			486 lines
		
	
	
		
			11 KiB
		
	
	
	
		
			Go
		
	
	
			
		
		
	
	
			486 lines
		
	
	
		
			11 KiB
		
	
	
	
		
			Go
		
	
	
| package gowebdav
 | |
| 
 | |
| import (
 | |
| 	"bytes"
 | |
| 	"encoding/xml"
 | |
| 	"fmt"
 | |
| 	"github.com/alist-org/alist/v3/pkg/utils"
 | |
| 	"io"
 | |
| 	"net/http"
 | |
| 	"net/url"
 | |
| 	"os"
 | |
| 	pathpkg "path"
 | |
| 	"strings"
 | |
| 	"sync"
 | |
| 	"time"
 | |
| )
 | |
| 
 | |
| // Client defines our structure
 | |
| type Client struct {
 | |
| 	root        string
 | |
| 	headers     http.Header
 | |
| 	interceptor func(method string, rq *http.Request)
 | |
| 	c           *http.Client
 | |
| 
 | |
| 	authMutex sync.Mutex
 | |
| 	auth      Authenticator
 | |
| }
 | |
| 
 | |
| // Authenticator stub
 | |
| type Authenticator interface {
 | |
| 	Type() string
 | |
| 	User() string
 | |
| 	Pass() string
 | |
| 	Authorize(*http.Request, string, string)
 | |
| }
 | |
| 
 | |
| // NoAuth structure holds our credentials
 | |
| type NoAuth struct {
 | |
| 	user string
 | |
| 	pw   string
 | |
| }
 | |
| 
 | |
| // Type identifies the authenticator
 | |
| func (n *NoAuth) Type() string {
 | |
| 	return "NoAuth"
 | |
| }
 | |
| 
 | |
| // User returns the current user
 | |
| func (n *NoAuth) User() string {
 | |
| 	return n.user
 | |
| }
 | |
| 
 | |
| // Pass returns the current password
 | |
| func (n *NoAuth) Pass() string {
 | |
| 	return n.pw
 | |
| }
 | |
| 
 | |
| // Authorize the current request
 | |
| func (n *NoAuth) Authorize(req *http.Request, method string, path string) {
 | |
| }
 | |
| 
 | |
| // NewClient creates a new instance of client
 | |
| func NewClient(uri, user, pw string) *Client {
 | |
| 	return &Client{FixSlash(uri), make(http.Header), nil, &http.Client{}, sync.Mutex{}, &NoAuth{user, pw}}
 | |
| }
 | |
| 
 | |
| // 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 = time.Unix(0, 0)
 | |
| 				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, nil); 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
 | |
| }
 | |
| 
 | |
| func (c *Client) Link(path string) (string, http.Header, error) {
 | |
| 	method := "GET"
 | |
| 	u := PathEscape(Join(c.root, path))
 | |
| 	r, err := http.NewRequest(method, u, nil)
 | |
| 
 | |
| 	if err != nil {
 | |
| 		return "", nil, newPathErrorErr("Link", path, err)
 | |
| 	}
 | |
| 
 | |
| 	if c.c.Jar != nil {
 | |
| 		for _, cookie := range c.c.Jar.Cookies(r.URL) {
 | |
| 			r.AddCookie(cookie)
 | |
| 		}
 | |
| 	}
 | |
| 	for k, vals := range c.headers {
 | |
| 		for _, v := range vals {
 | |
| 			r.Header.Add(k, v)
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	c.authMutex.Lock()
 | |
| 	auth := c.auth
 | |
| 	c.authMutex.Unlock()
 | |
| 
 | |
| 	auth.Authorize(r, method, path)
 | |
| 
 | |
| 	if c.interceptor != nil {
 | |
| 		c.interceptor(method, r)
 | |
| 	}
 | |
| 	return r.URL.String(), r.Header, nil
 | |
| }
 | |
| 
 | |
| // ReadStream reads the stream for a given path
 | |
| func (c *Client) ReadStream(path string, callback func(rq *http.Request)) (io.ReadCloser, http.Header, error) {
 | |
| 	rs, err := c.req("GET", path, nil, callback)
 | |
| 	if err != nil {
 | |
| 		return nil, nil, newPathErrorErr("ReadStream", path, err)
 | |
| 	}
 | |
| 
 | |
| 	if rs.StatusCode < 400 {
 | |
| 		return rs.Body, rs.Header, nil
 | |
| 	}
 | |
| 
 | |
| 	rs.Body.Close()
 | |
| 	return nil, 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) {
 | |
| 		r.Header.Add("Range", fmt.Sprintf("bytes=%v-%v", offset, offset+length-1))
 | |
| 	})
 | |
| 	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 := utils.CopyWithBuffer(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{rs.Body, 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), nil)
 | |
| 	if err != nil {
 | |
| 		return
 | |
| 	}
 | |
| 
 | |
| 	switch s {
 | |
| 
 | |
| 	case 200, 201, 204:
 | |
| 		return nil
 | |
| 
 | |
| 	case 409:
 | |
| 		err = c.createParentCollection(path)
 | |
| 		if err != nil {
 | |
| 			return
 | |
| 		}
 | |
| 
 | |
| 		s, err = c.put(path, bytes.NewReader(data), nil)
 | |
| 		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, callback func(r *http.Request)) (err error) {
 | |
| 
 | |
| 	err = c.createParentCollection(path)
 | |
| 	if err != nil {
 | |
| 		return err
 | |
| 	}
 | |
| 
 | |
| 	s, err := c.put(path, stream, callback)
 | |
| 	if err != nil {
 | |
| 		return err
 | |
| 	}
 | |
| 
 | |
| 	switch s {
 | |
| 	case 200, 201, 204:
 | |
| 		return nil
 | |
| 
 | |
| 	default:
 | |
| 		return newPathError("WriteStream", path, s)
 | |
| 	}
 | |
| }
 |