2020-01-14 17:51:17 +00:00
|
|
|
package webdav
|
|
|
|
|
|
|
|
import (
|
2023-12-13 13:37:38 +00:00
|
|
|
"context"
|
2020-01-20 09:56:25 +00:00
|
|
|
"fmt"
|
2020-01-21 17:47:29 +00:00
|
|
|
"io"
|
2020-01-20 12:17:19 +00:00
|
|
|
"net/http"
|
2020-01-21 17:41:46 +00:00
|
|
|
"time"
|
2020-01-14 17:51:17 +00:00
|
|
|
|
|
|
|
"github.com/emersion/go-webdav/internal"
|
|
|
|
)
|
|
|
|
|
2020-02-19 15:02:49 +00:00
|
|
|
// HTTPClient performs HTTP requests. It's implemented by *http.Client.
|
|
|
|
type HTTPClient interface {
|
|
|
|
Do(req *http.Request) (*http.Response, error)
|
|
|
|
}
|
|
|
|
|
|
|
|
type basicAuthHTTPClient struct {
|
|
|
|
c HTTPClient
|
|
|
|
username, password string
|
|
|
|
}
|
|
|
|
|
|
|
|
func (c *basicAuthHTTPClient) Do(req *http.Request) (*http.Response, error) {
|
|
|
|
req.SetBasicAuth(c.username, c.password)
|
|
|
|
return c.c.Do(req)
|
|
|
|
}
|
|
|
|
|
|
|
|
// HTTPClientWithBasicAuth returns an HTTP client that adds basic
|
|
|
|
// authentication to all outgoing requests. If c is nil, http.DefaultClient is
|
|
|
|
// used.
|
|
|
|
func HTTPClientWithBasicAuth(c HTTPClient, username, password string) HTTPClient {
|
|
|
|
if c == nil {
|
|
|
|
c = http.DefaultClient
|
|
|
|
}
|
|
|
|
return &basicAuthHTTPClient{c, username, password}
|
|
|
|
}
|
|
|
|
|
2020-01-21 20:01:18 +00:00
|
|
|
// Client provides access to a remote WebDAV filesystem.
|
2020-01-14 17:51:17 +00:00
|
|
|
type Client struct {
|
2020-01-21 17:41:46 +00:00
|
|
|
ic *internal.Client
|
2020-01-14 17:51:17 +00:00
|
|
|
}
|
|
|
|
|
2024-01-08 13:35:19 +00:00
|
|
|
// NewClient creates a new WebDAV client.
|
|
|
|
//
|
|
|
|
// If the HTTPClient is nil, http.DefaultClient is used.
|
|
|
|
//
|
|
|
|
// To use HTTP basic authentication, HTTPClientWithBasicAuth can be used.
|
2020-02-19 15:02:49 +00:00
|
|
|
func NewClient(c HTTPClient, endpoint string) (*Client, error) {
|
2020-01-14 17:51:17 +00:00
|
|
|
ic, err := internal.NewClient(c, endpoint)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
return &Client{ic}, nil
|
|
|
|
}
|
|
|
|
|
2024-01-08 13:35:19 +00:00
|
|
|
// FindCurrentUserPrincipal finds the current user's principal path.
|
2023-12-13 13:37:38 +00:00
|
|
|
func (c *Client) FindCurrentUserPrincipal(ctx context.Context) (string, error) {
|
2022-05-31 16:32:12 +01:00
|
|
|
propfind := internal.NewPropNamePropFind(internal.CurrentUserPrincipalName)
|
2020-01-14 19:00:54 +00:00
|
|
|
|
2022-05-25 14:07:20 +01:00
|
|
|
// TODO: consider retrying on the root URI "/" if this fails, as suggested
|
|
|
|
// by the RFC?
|
2023-12-13 13:37:38 +00:00
|
|
|
resp, err := c.ic.PropFindFlat(ctx, "", propfind)
|
2020-01-14 20:29:54 +00:00
|
|
|
if err != nil {
|
|
|
|
return "", err
|
2020-01-14 17:51:17 +00:00
|
|
|
}
|
|
|
|
|
2020-01-20 09:56:25 +00:00
|
|
|
var prop internal.CurrentUserPrincipal
|
2020-01-15 10:44:27 +00:00
|
|
|
if err := resp.DecodeProp(&prop); err != nil {
|
2020-01-14 17:51:17 +00:00
|
|
|
return "", err
|
|
|
|
}
|
2020-01-20 09:56:25 +00:00
|
|
|
if prop.Unauthenticated != nil {
|
|
|
|
return "", fmt.Errorf("webdav: unauthenticated")
|
|
|
|
}
|
2020-01-14 17:51:17 +00:00
|
|
|
|
2020-01-22 10:07:30 +00:00
|
|
|
return prop.Href.Path, nil
|
2020-01-14 17:51:17 +00:00
|
|
|
}
|
2020-01-21 17:41:46 +00:00
|
|
|
|
2022-05-31 16:32:12 +01:00
|
|
|
var fileInfoPropFind = internal.NewPropNamePropFind(
|
2020-01-22 10:51:05 +00:00
|
|
|
internal.ResourceTypeName,
|
|
|
|
internal.GetContentLengthName,
|
|
|
|
internal.GetLastModifiedName,
|
|
|
|
internal.GetContentTypeName,
|
|
|
|
internal.GetETagName,
|
|
|
|
)
|
|
|
|
|
2020-01-21 21:36:42 +00:00
|
|
|
func fileInfoFromResponse(resp *internal.Response) (*FileInfo, error) {
|
2020-01-22 10:07:30 +00:00
|
|
|
path, err := resp.Path()
|
2020-01-21 17:55:29 +00:00
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
2020-01-21 21:36:42 +00:00
|
|
|
|
2020-01-22 10:07:30 +00:00
|
|
|
fi := &FileInfo{Path: path}
|
2020-01-21 17:55:29 +00:00
|
|
|
|
|
|
|
var resType internal.ResourceType
|
|
|
|
if err := resp.DecodeProp(&resType); err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
2021-08-11 10:08:03 +01:00
|
|
|
|
2020-01-21 17:55:29 +00:00
|
|
|
if resType.Is(internal.CollectionName) {
|
2020-01-21 21:36:42 +00:00
|
|
|
fi.IsDir = true
|
2020-01-21 17:55:29 +00:00
|
|
|
} else {
|
|
|
|
var getLen internal.GetContentLength
|
2020-01-21 22:14:57 +00:00
|
|
|
if err := resp.DecodeProp(&getLen); err != nil {
|
2020-01-21 17:55:29 +00:00
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
2020-01-21 21:46:56 +00:00
|
|
|
var getType internal.GetContentType
|
|
|
|
if err := resp.DecodeProp(&getType); err != nil && !internal.IsNotFound(err) {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
2020-01-22 10:51:05 +00:00
|
|
|
var getETag internal.GetETag
|
|
|
|
if err := resp.DecodeProp(&getETag); err != nil && !internal.IsNotFound(err) {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
2020-01-21 21:36:42 +00:00
|
|
|
fi.Size = getLen.Length
|
2020-01-21 21:46:56 +00:00
|
|
|
fi.MIMEType = getType.Type
|
2020-02-03 20:48:31 +00:00
|
|
|
fi.ETag = string(getETag.ETag)
|
2020-01-21 17:55:29 +00:00
|
|
|
}
|
|
|
|
|
2021-08-11 10:08:03 +01:00
|
|
|
var getMod internal.GetLastModified
|
|
|
|
if err := resp.DecodeProp(&getMod); err != nil && !internal.IsNotFound(err) {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
fi.ModTime = time.Time(getMod.LastModified)
|
|
|
|
|
2020-01-21 17:55:29 +00:00
|
|
|
return fi, nil
|
|
|
|
}
|
|
|
|
|
2024-01-08 13:35:19 +00:00
|
|
|
// Stat fetches a FileInfo for a single file.
|
2023-12-13 13:37:38 +00:00
|
|
|
func (c *Client) Stat(ctx context.Context, name string) (*FileInfo, error) {
|
|
|
|
resp, err := c.ic.PropFindFlat(ctx, name, fileInfoPropFind)
|
2020-01-21 17:41:46 +00:00
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
2020-01-21 17:55:29 +00:00
|
|
|
return fileInfoFromResponse(resp)
|
2020-01-21 17:41:46 +00:00
|
|
|
}
|
2020-01-21 17:47:29 +00:00
|
|
|
|
2024-01-08 13:35:19 +00:00
|
|
|
// Open fetches a file's contents.
|
2023-12-13 13:37:38 +00:00
|
|
|
func (c *Client) Open(ctx context.Context, name string) (io.ReadCloser, error) {
|
2020-01-21 17:47:29 +00:00
|
|
|
req, err := c.ic.NewRequest(http.MethodGet, name, nil)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
2023-12-13 13:37:38 +00:00
|
|
|
resp, err := c.ic.Do(req.WithContext(ctx))
|
2020-01-21 17:47:29 +00:00
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
return resp.Body, nil
|
|
|
|
}
|
2020-01-21 17:55:29 +00:00
|
|
|
|
2024-01-08 13:35:56 +00:00
|
|
|
// ReadDir lists files in a directory.
|
|
|
|
func (c *Client) ReadDir(ctx context.Context, name string, recursive bool) ([]FileInfo, error) {
|
2020-01-22 11:06:36 +00:00
|
|
|
depth := internal.DepthOne
|
|
|
|
if recursive {
|
|
|
|
depth = internal.DepthInfinity
|
|
|
|
}
|
2020-01-21 17:55:29 +00:00
|
|
|
|
2023-12-13 13:37:38 +00:00
|
|
|
ms, err := c.ic.PropFind(ctx, name, depth, fileInfoPropFind)
|
2020-01-21 17:55:29 +00:00
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
2020-01-21 21:36:42 +00:00
|
|
|
l := make([]FileInfo, 0, len(ms.Responses))
|
2020-01-21 17:55:29 +00:00
|
|
|
for _, resp := range ms.Responses {
|
|
|
|
fi, err := fileInfoFromResponse(&resp)
|
|
|
|
if err != nil {
|
|
|
|
return l, err
|
|
|
|
}
|
2020-01-21 21:36:42 +00:00
|
|
|
l = append(l, *fi)
|
2020-01-21 17:55:29 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
return l, nil
|
|
|
|
}
|
2020-01-21 20:32:43 +00:00
|
|
|
|
|
|
|
type fileWriter struct {
|
2020-01-21 20:46:01 +00:00
|
|
|
pw *io.PipeWriter
|
2020-01-21 20:32:43 +00:00
|
|
|
done <-chan error
|
|
|
|
}
|
|
|
|
|
|
|
|
func (fw *fileWriter) Write(b []byte) (int, error) {
|
|
|
|
return fw.pw.Write(b)
|
|
|
|
}
|
|
|
|
|
|
|
|
func (fw *fileWriter) Close() error {
|
|
|
|
if err := fw.pw.Close(); err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
return <-fw.done
|
|
|
|
}
|
|
|
|
|
2024-01-08 13:35:19 +00:00
|
|
|
// Create writes a file's contents.
|
2023-12-13 13:37:38 +00:00
|
|
|
func (c *Client) Create(ctx context.Context, name string) (io.WriteCloser, error) {
|
2020-01-21 20:32:43 +00:00
|
|
|
pr, pw := io.Pipe()
|
|
|
|
|
|
|
|
req, err := c.ic.NewRequest(http.MethodPut, name, pr)
|
|
|
|
if err != nil {
|
|
|
|
pw.Close()
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
done := make(chan error, 1)
|
|
|
|
go func() {
|
2023-12-13 13:37:38 +00:00
|
|
|
resp, err := c.ic.Do(req.WithContext(ctx))
|
2022-06-17 19:51:01 +01:00
|
|
|
if err != nil {
|
|
|
|
done <- err
|
|
|
|
return
|
|
|
|
}
|
|
|
|
resp.Body.Close()
|
|
|
|
done <- nil
|
2020-01-21 20:32:43 +00:00
|
|
|
}()
|
|
|
|
|
|
|
|
return &fileWriter{pw, done}, nil
|
|
|
|
}
|
2020-01-21 20:48:07 +00:00
|
|
|
|
2024-01-08 13:35:19 +00:00
|
|
|
// RemoveAll deletes a file. If the file is a directory, all of its descendants
|
|
|
|
// are recursively deleted as well.
|
2023-12-13 13:37:38 +00:00
|
|
|
func (c *Client) RemoveAll(ctx context.Context, name string) error {
|
2020-01-21 20:48:07 +00:00
|
|
|
req, err := c.ic.NewRequest(http.MethodDelete, name, nil)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
2023-12-13 13:37:38 +00:00
|
|
|
resp, err := c.ic.Do(req.WithContext(ctx))
|
2022-06-17 19:51:01 +01:00
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
resp.Body.Close()
|
|
|
|
return nil
|
2020-01-21 20:48:07 +00:00
|
|
|
}
|
2020-01-21 21:06:47 +00:00
|
|
|
|
2024-01-08 13:35:19 +00:00
|
|
|
// Mkdir creates a new directory.
|
2023-12-13 13:37:38 +00:00
|
|
|
func (c *Client) Mkdir(ctx context.Context, name string) error {
|
2020-01-21 21:06:47 +00:00
|
|
|
req, err := c.ic.NewRequest("MKCOL", name, nil)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
2023-12-13 13:37:38 +00:00
|
|
|
resp, err := c.ic.Do(req.WithContext(ctx))
|
2022-06-17 19:51:01 +01:00
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
resp.Body.Close()
|
|
|
|
return nil
|
2020-01-21 21:06:47 +00:00
|
|
|
}
|
2020-01-22 09:15:15 +00:00
|
|
|
|
2023-12-15 14:16:01 +00:00
|
|
|
// Copy copies a file.
|
|
|
|
//
|
|
|
|
// By default, if the file is a directory, all descendants are recursively
|
|
|
|
// copied as well.
|
|
|
|
func (c *Client) Copy(ctx context.Context, name, dest string, options *CopyOptions) error {
|
|
|
|
if options == nil {
|
|
|
|
options = new(CopyOptions)
|
|
|
|
}
|
|
|
|
|
2020-01-22 09:16:48 +00:00
|
|
|
req, err := c.ic.NewRequest("COPY", name, nil)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
2023-12-15 14:16:01 +00:00
|
|
|
depth := internal.DepthInfinity
|
|
|
|
if options.NoRecursive {
|
|
|
|
depth = internal.DepthZero
|
|
|
|
}
|
|
|
|
|
2020-01-22 10:07:30 +00:00
|
|
|
req.Header.Set("Destination", c.ic.ResolveHref(dest).String())
|
2023-12-15 14:16:01 +00:00
|
|
|
req.Header.Set("Overwrite", internal.FormatOverwrite(!options.NoOverwrite))
|
|
|
|
req.Header.Set("Depth", depth.String())
|
2020-01-22 09:16:48 +00:00
|
|
|
|
2023-12-13 13:37:38 +00:00
|
|
|
resp, err := c.ic.Do(req.WithContext(ctx))
|
2022-06-17 19:51:01 +01:00
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
resp.Body.Close()
|
|
|
|
return nil
|
2020-01-22 09:16:48 +00:00
|
|
|
}
|
|
|
|
|
2024-01-18 12:22:19 +00:00
|
|
|
// Move moves a file.
|
2024-01-18 12:25:14 +00:00
|
|
|
func (c *Client) Move(ctx context.Context, name, dest string, options *MoveOptions) error {
|
|
|
|
if options == nil {
|
|
|
|
options = new(MoveOptions)
|
|
|
|
}
|
|
|
|
|
2020-01-22 09:15:15 +00:00
|
|
|
req, err := c.ic.NewRequest("MOVE", name, nil)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
2020-01-22 10:07:30 +00:00
|
|
|
req.Header.Set("Destination", c.ic.ResolveHref(dest).String())
|
2024-01-18 12:25:14 +00:00
|
|
|
req.Header.Set("Overwrite", internal.FormatOverwrite(!options.NoOverwrite))
|
2020-01-22 09:15:15 +00:00
|
|
|
|
2023-12-13 13:37:38 +00:00
|
|
|
resp, err := c.ic.Do(req.WithContext(ctx))
|
2022-06-17 19:51:01 +01:00
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
resp.Body.Close()
|
|
|
|
return nil
|
2020-01-22 09:15:15 +00:00
|
|
|
}
|