diff --git a/auth.go b/auth.go
index 3caec01..cf08f01 100644
--- a/auth.go
+++ b/auth.go
@@ -2,7 +2,10 @@ package gonextcloud
import (
"fmt"
+
req "github.com/levigross/grequests"
+ "github.com/studio-b12/gowebdav"
+
"gitlab.bertha.cloud/partitio/Nextcloud-Partitio/gonextcloud/types"
)
@@ -33,6 +36,8 @@ func (c *Client) Login(username string, password string) error {
e := types.APIError{Message: "authentication failed"}
return &e
}
+ // Create webdav client
+ c.webdav = gowebdav.NewClient(c.baseURL.String()+"/remote.php/webdav", c.username, c.password)
return nil
}
diff --git a/client.go b/client.go
index 79bcfe9..1006e70 100644
--- a/client.go
+++ b/client.go
@@ -1,9 +1,12 @@
package gonextcloud
import (
- req "github.com/levigross/grequests"
- "gitlab.bertha.cloud/partitio/Nextcloud-Partitio/gonextcloud/types"
"net/url"
+
+ req "github.com/levigross/grequests"
+ "github.com/studio-b12/gowebdav"
+
+ "gitlab.bertha.cloud/partitio/Nextcloud-Partitio/gonextcloud/types"
)
// Client is the API client that performs all operations against a Nextcloud server.
@@ -23,6 +26,7 @@ type Client struct {
shares *Shares
users *Users
groups *Groups
+ webdav *gowebdav.Client
}
// NewClient create a new Client from the Nextcloud Instance URL
@@ -42,6 +46,7 @@ func NewClient(hostname string) (*Client, error) {
"Accept": "application/json",
},
}
+
c.apps = &Apps{c}
c.appsConfig = &AppsConfig{c}
c.groupFolders = &GroupFolders{c}
@@ -49,6 +54,9 @@ func NewClient(hostname string) (*Client, error) {
c.shares = &Shares{c}
c.users = &Users{c}
c.groups = &Groups{c}
+ // Create empty webdav client
+ // It will be replaced after login
+ c.webdav = &gowebdav.Client{}
return c, nil
}
@@ -86,3 +94,8 @@ func (c *Client) Users() types.Users {
func (c *Client) Groups() types.Groups {
return c.groups
}
+
+// WebDav return the WebDav client Interface
+func (c *Client) WebDav() types.WebDav {
+ return c.webdav
+}
diff --git a/go.mod b/go.mod
index 8db791e..a4428e4 100644
--- a/go.mod
+++ b/go.mod
@@ -9,6 +9,7 @@ require (
github.com/pkg/errors v0.0.0-20181023235946-059132a15dd0
github.com/sirupsen/logrus v1.4.2
github.com/stretchr/testify v1.2.2
+ github.com/studio-b12/gowebdav v0.0.0-20190103184047-38f79aeaf1ac
golang.org/x/net v0.0.0-20181129055619-fae4c4e3ad76 // indirect
gopkg.in/yaml.v2 v2.2.1
)
diff --git a/go.sum b/go.sum
index 5cf7e9d..0b1c400 100644
--- a/go.sum
+++ b/go.sum
@@ -18,6 +18,8 @@ github.com/stretchr/objx v0.1.1 h1:2vfRuCMp5sSVIDSqO8oNnWJq7mPa6KVP3iPIwFBuy8A=
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.2.2 h1:bSDNvY7ZPG5RlJ8otE/7V6gMiyenm9RtJ7IUVIAoJ1w=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
+github.com/studio-b12/gowebdav v0.0.0-20190103184047-38f79aeaf1ac h1:xQ9gCVzqb939vjhxuES4IXYe4AlHB4Q71/K06aazQmQ=
+github.com/studio-b12/gowebdav v0.0.0-20190103184047-38f79aeaf1ac/go.mod h1:gCcfDlA1Y7GqOaeEKw5l9dOGx1VLdc/HuQSlQAaZ30s=
golang.org/x/net v0.0.0-20181129055619-fae4c4e3ad76 h1:xx5MUFyRQRbPk6VjWjIE1epE/K5AoDD8QUN116NCy8k=
golang.org/x/net v0.0.0-20181129055619-fae4c4e3ad76/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/sys v0.0.0-20190422165155-953cdadca894 h1:Cz4ceDQGXuKRnVBDTS23GTn/pU5OE2C0WrNTOYK1Uuc=
diff --git a/types/interfaces.go b/types/interfaces.go
index 6522983..2f70f1c 100644
--- a/types/interfaces.go
+++ b/types/interfaces.go
@@ -9,6 +9,7 @@ type Client interface {
Shares() Shares
Users() Users
Groups() Groups
+ WebDav() WebDav
Login(username string, password string) error
Logout() error
}
diff --git a/types/webdav.go b/types/webdav.go
new file mode 100644
index 0000000..60e44bc
--- /dev/null
+++ b/types/webdav.go
@@ -0,0 +1,34 @@
+package types
+
+import (
+ "io"
+ "os"
+)
+
+// WebDav available methods
+type WebDav interface {
+ // ReadDir reads the contents of a remote directory
+ ReadDir(path string) ([]os.FileInfo, error)
+ // Stat returns the file stats for a specified path
+ Stat(path string) (os.FileInfo, error)
+ // Remove removes a remote file
+ Remove(path string) error
+ // RemoveAll removes remote files
+ RemoveAll(path string) error
+ // Mkdir makes a directory
+ Mkdir(path string, _ os.FileMode) error
+ // MkdirAll like mkdir -p, but for webdav
+ MkdirAll(path string, _ os.FileMode) error
+ // Rename moves a file from A to B
+ Rename(oldpath, newpath string, overwrite bool) error
+ // Copy copies a file from A to B
+ Copy(oldpath, newpath string, overwrite bool) error
+ // Read reads the contents of a remote file
+ Read(path string) ([]byte, error)
+ // ReadStream reads the stream for a given path
+ ReadStream(path string) (io.ReadCloser, error)
+ // Write writes data to a given path
+ Write(path string, data []byte, _ os.FileMode) error
+ // WriteStream writes a stream
+ WriteStream(path string, stream io.Reader, _ os.FileMode) error
+}
diff --git a/vendor/github.com/studio-b12/gowebdav/.gitignore b/vendor/github.com/studio-b12/gowebdav/.gitignore
new file mode 100644
index 0000000..3ae0f0e
--- /dev/null
+++ b/vendor/github.com/studio-b12/gowebdav/.gitignore
@@ -0,0 +1,19 @@
+# Folders to ignore
+/src
+/bin
+/pkg
+/gowebdav
+/.idea
+
+# Binaries for programs and plugins
+*.exe
+*.exe~
+*.dll
+*.so
+*.dylib
+
+# Test binary, build with `go test -c`
+*.test
+
+# Output of the go coverage tool, specifically when used with LiteIDE
+*.out
diff --git a/vendor/github.com/studio-b12/gowebdav/.travis.yml b/vendor/github.com/studio-b12/gowebdav/.travis.yml
new file mode 100644
index 0000000..76bfb65
--- /dev/null
+++ b/vendor/github.com/studio-b12/gowebdav/.travis.yml
@@ -0,0 +1,10 @@
+language: go
+
+go:
+ - "1.x"
+
+install:
+ - go get ./...
+
+script:
+ - go test -v --short ./...
\ No newline at end of file
diff --git a/vendor/github.com/studio-b12/gowebdav/LICENSE b/vendor/github.com/studio-b12/gowebdav/LICENSE
new file mode 100644
index 0000000..a7cd442
--- /dev/null
+++ b/vendor/github.com/studio-b12/gowebdav/LICENSE
@@ -0,0 +1,27 @@
+Copyright (c) 2014, Studio B12 GmbH
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+
+1. Redistributions of source code must retain the above copyright notice, this
+ list of conditions and the following disclaimer.
+
+2. Redistributions in binary form must reproduce the above copyright notice,
+ this list of conditions and the following disclaimer in the documentation
+ and/or other materials provided with the distribution.
+
+3. Neither the name of the copyright holder nor the names of its contributors
+ may be used to endorse or promote products derived from this software without
+ specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
diff --git a/vendor/github.com/studio-b12/gowebdav/Makefile b/vendor/github.com/studio-b12/gowebdav/Makefile
new file mode 100644
index 0000000..c6a0062
--- /dev/null
+++ b/vendor/github.com/studio-b12/gowebdav/Makefile
@@ -0,0 +1,33 @@
+BIN := gowebdav
+SRC := $(wildcard *.go) cmd/gowebdav/main.go
+
+all: test cmd
+
+cmd: ${BIN}
+
+${BIN}: ${SRC}
+ go build -o $@ ./cmd/gowebdav
+
+test:
+ go test -v --short ./...
+
+api:
+ @sed '/^## API$$/,$$d' -i README.md
+ @echo '## API' >> README.md
+ @godoc2md github.com/studio-b12/gowebdav | sed '/^$$/N;/^\n$$/D' |\
+ sed '2d' |\
+ sed 's/\/src\/github.com\/studio-b12\/gowebdav\//https:\/\/github.com\/studio-b12\/gowebdav\/blob\/master\//g' |\
+ sed 's/\/src\/target\//https:\/\/github.com\/studio-b12\/gowebdav\/blob\/master\//g' |\
+ sed 's/^#/##/g' >> README.md
+
+check:
+ gofmt -w -s $(SRC)
+ @echo
+ gocyclo -over 15 .
+ @echo
+ golint ./...
+
+clean:
+ @rm -f ${BIN}
+
+.PHONY: all cmd clean test api check
diff --git a/vendor/github.com/studio-b12/gowebdav/README.md b/vendor/github.com/studio-b12/gowebdav/README.md
new file mode 100644
index 0000000..fc6cfe6
--- /dev/null
+++ b/vendor/github.com/studio-b12/gowebdav/README.md
@@ -0,0 +1,147 @@
+# GoWebDAV
+
+[![Build Status](https://travis-ci.org/studio-b12/gowebdav.svg?branch=master)](https://travis-ci.org/studio-b12/gowebdav)
+[![GoDoc](https://godoc.org/github.com/studio-b12/gowebdav?status.svg)](https://godoc.org/github.com/studio-b12/gowebdav)
+[![Go Report Card](https://goreportcard.com/badge/github.com/studio-b12/gowebdav)](https://goreportcard.com/report/github.com/studio-b12/gowebdav)
+
+A golang WebDAV client library.
+
+## Main features
+`gowebdav` library allows to perform following actions on the remote WebDAV server:
+* [create path](#create-path-on-a-webdav-server)
+* [get files list](#get-files-list)
+* [download file](#download-file-to-byte-array)
+* [upload file](#upload-file-from-byte-array)
+* [get information about specified file/folder](#get-information-about-specified-filefolder)
+* [move file to another location](#move-file-to-another-location)
+* [copy file to another location](#copy-file-to-another-location)
+* [delete file](#delete-file)
+
+## Usage
+
+First of all you should create `Client` instance using `NewClient()` function:
+
+```go
+root := "https://webdav.mydomain.me"
+user := "user"
+password := "password"
+
+c := gowebdav.NewClient(root, user, password)
+```
+
+After you can use this `Client` to perform actions, described below.
+
+**NOTICE:** we will not check errors in examples, to focus you on the `gowebdav` library's code, but you should do it in your code!
+
+### Create path on a WebDAV server
+```go
+err := c.Mkdir("folder", 0644)
+```
+In case you want to create several folders you can use `c.MkdirAll()`:
+```go
+err := c.MkdirAll("folder/subfolder/subfolder2", 0644)
+```
+
+### Get files list
+```go
+files, _ := c.ReadDir("folder/subfolder")
+for _, file := range files {
+ //notice that [file] has os.FileInfo type
+ fmt.Println(file.Name())
+}
+```
+
+### Download file to byte array
+```go
+webdavFilePath := "folder/subfolder/file.txt"
+localFilePath := "/tmp/webdav/file.txt"
+
+bytes, _ := c.Read(webdavFilePath)
+ioutil.WriteFile(localFilePath, bytes, 0644)
+```
+
+### Download file via reader
+Also you can use `c.ReadStream()` method:
+```go
+webdavFilePath := "folder/subfolder/file.txt"
+localFilePath := "/tmp/webdav/file.txt"
+
+reader, _ := c.ReadStream(webdavFilePath)
+
+file, _ := os.Create(localFilePath)
+defer file.Close()
+
+io.Copy(file, reader)
+```
+
+### Upload file from byte array
+```go
+webdavFilePath := "folder/subfolder/file.txt"
+localFilePath := "/tmp/webdav/file.txt"
+
+bytes, _ := ioutil.ReadFile(localFilePath)
+
+c.Write(webdavFilePath, bytes, 0644)
+```
+
+### Upload file via writer
+```go
+webdavFilePath := "folder/subfolder/file.txt"
+localFilePath := "/tmp/webdav/file.txt"
+
+file, _ := os.Open(localFilePath)
+defer file.Close()
+
+c.WriteStream(webdavFilePath, file, 0644)
+```
+
+### Get information about specified file/folder
+```go
+webdavFilePath := "folder/subfolder/file.txt"
+
+info := c.Stat(webdavFilePath)
+//notice that [info] has os.FileInfo type
+fmt.Println(info)
+```
+
+### Move file to another location
+```go
+oldPath := "folder/subfolder/file.txt"
+newPath := "folder/subfolder/moved.txt"
+isOverwrite := true
+
+c.Rename(oldPath, newPath, isOverwrite)
+```
+
+### Copy file to another location
+```go
+oldPath := "folder/subfolder/file.txt"
+newPath := "folder/subfolder/file-copy.txt"
+isOverwrite := true
+
+c.Copy(oldPath, newPath, isOverwrite)
+```
+
+### Delete file
+```go
+webdavFilePath := "folder/subfolder/file.txt"
+
+c.Remove(webdavFilePath)
+```
+
+## Links
+
+More details about WebDAV server you can read from following resources:
+
+* [RFC 4918 - HTTP Extensions for Web Distributed Authoring and Versioning (WebDAV)](https://tools.ietf.org/html/rfc4918)
+* [RFC 5689 - Extended MKCOL for Web Distributed Authoring and Versioning (WebDAV)](https://tools.ietf.org/html/rfc5689)
+* [RFC 2616 - HTTP/1.1 Status Code Definitions](http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html "HTTP/1.1 Status Code Definitions")
+* [WebDav: Next Generation Collaborative Web Authoring By Lisa Dusseaul](https://books.google.de/books?isbn=0130652083 "WebDav: Next Generation Collaborative Web Authoring By Lisa Dusseault")
+
+**NOTICE**: RFC 2518 is obsoleted by RFC 4918 in June 2007
+
+## Contributing
+All contributing are welcome. If you have any suggestions or find some bug - please create an Issue to let us make this project better. We appreciate your help!
+
+## License
+This library is distributed under the BSD 3-Clause license found in the [LICENSE](https://github.com/studio-b12/gowebdav/blob/master/LICENSE) file.
diff --git a/vendor/github.com/studio-b12/gowebdav/basicAuth.go b/vendor/github.com/studio-b12/gowebdav/basicAuth.go
new file mode 100644
index 0000000..5a69113
--- /dev/null
+++ b/vendor/github.com/studio-b12/gowebdav/basicAuth.go
@@ -0,0 +1,33 @@
+package gowebdav
+
+import (
+ "encoding/base64"
+)
+
+// BasicAuth structure holds our credentials
+type BasicAuth struct {
+ user string
+ pw string
+}
+
+// Type identifies the BasicAuthenticator
+func (b *BasicAuth) Type() string {
+ return "BasicAuth"
+}
+
+// User holds the BasicAuth username
+func (b *BasicAuth) User() string {
+ return b.user
+}
+
+// Pass holds the BasicAuth password
+func (b *BasicAuth) Pass() string {
+ return b.pw
+}
+
+// Authorize the current request
+func (b *BasicAuth) Authorize(c *Client, method string, path string) {
+ a := b.user + ":" + b.pw
+ auth := "Basic " + base64.StdEncoding.EncodeToString([]byte(a))
+ c.headers.Set("Authorization", auth)
+}
diff --git a/vendor/github.com/studio-b12/gowebdav/client.go b/vendor/github.com/studio-b12/gowebdav/client.go
new file mode 100644
index 0000000..17459b9
--- /dev/null
+++ b/vendor/github.com/studio-b12/gowebdav/client.go
@@ -0,0 +1,380 @@
+package gowebdav
+
+import (
+ "bytes"
+ "encoding/xml"
+ "io"
+ "net/http"
+ "net/url"
+ "os"
+ pathpkg "path"
+ "strings"
+ "time"
+)
+
+// Client defines our structure
+type Client struct {
+ root string
+ headers http.Header
+ c *http.Client
+ auth Authenticator
+}
+
+// Authenticator stub
+type Authenticator interface {
+ Type() string
+ User() string
+ Pass() string
+ Authorize(*Client, 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(c *Client, 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), &http.Client{}, &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)
+}
+
+// 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
+}
+
+// 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.QueryUnescape(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,
+ `
+
+
+
+
+
+
+
+
+ `,
+ &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,
+ `
+
+
+
+
+
+
+
+
+ `,
+ &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) error {
+ path = FixSlashes(path)
+ status := c.mkcol(path)
+ 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) error {
+ path = FixSlashes(path)
+ status := c.mkcol(path)
+ if status == 201 {
+ return nil
+ } else if status == 409 {
+ paths := strings.Split(path, "/")
+ sub := "/"
+ for _, e := range paths {
+ if e == "" {
+ continue
+ }
+ sub += e + "/"
+ status = c.mkcol(sub)
+ 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)
+}
+
+// Write writes data to a given path
+func (c *Client) Write(path string, data []byte, _ os.FileMode) error {
+ s := c.put(path, bytes.NewReader(data))
+ switch s {
+
+ case 200, 201, 204:
+ return nil
+
+ case 409:
+ err := c.createParentCollection(path)
+ if err != nil {
+ return err
+ }
+
+ s = c.put(path, bytes.NewReader(data))
+ if s == 200 || s == 201 || s == 204 {
+ return nil
+ }
+ }
+
+ return newPathError("Write", path, s)
+}
+
+// WriteStream writes a stream
+func (c *Client) WriteStream(path string, stream io.Reader, _ os.FileMode) error {
+
+ err := c.createParentCollection(path)
+ if err != nil {
+ return err
+ }
+
+ s := c.put(path, stream)
+
+ switch s {
+ case 200, 201, 204:
+ return nil
+
+ default:
+ return newPathError("WriteStream", path, s)
+ }
+}
diff --git a/vendor/github.com/studio-b12/gowebdav/digestAuth.go b/vendor/github.com/studio-b12/gowebdav/digestAuth.go
new file mode 100644
index 0000000..dd5c844
--- /dev/null
+++ b/vendor/github.com/studio-b12/gowebdav/digestAuth.go
@@ -0,0 +1,146 @@
+package gowebdav
+
+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
+}
+
+// Type identifies the DigestAuthenticator
+func (d *DigestAuth) Type() string {
+ return "DigestAuth"
+}
+
+// User holds the DigestAuth username
+func (d *DigestAuth) User() string {
+ return d.user
+}
+
+// Pass holds the DigestAuth password
+func (d *DigestAuth) Pass() string {
+ return d.pw
+}
+
+// Authorize the current request
+func (d *DigestAuth) Authorize(c *Client, method string, path string) {
+ d.digestParts["uri"] = path
+ d.digestParts["method"] = method
+ d.digestParts["username"] = d.user
+ d.digestParts["password"] = d.pw
+ c.headers.Set("Authorization", getDigestAuthorization(d.digestParts))
+}
+
+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
+}
diff --git a/vendor/github.com/studio-b12/gowebdav/doc.go b/vendor/github.com/studio-b12/gowebdav/doc.go
new file mode 100644
index 0000000..e47d5ee
--- /dev/null
+++ b/vendor/github.com/studio-b12/gowebdav/doc.go
@@ -0,0 +1,3 @@
+// Package gowebdav is a WebDAV client library with a command line tool
+// included.
+package gowebdav
diff --git a/vendor/github.com/studio-b12/gowebdav/file.go b/vendor/github.com/studio-b12/gowebdav/file.go
new file mode 100644
index 0000000..200485d
--- /dev/null
+++ b/vendor/github.com/studio-b12/gowebdav/file.go
@@ -0,0 +1,72 @@
+package gowebdav
+
+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
+}
+
+// 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)
+}
diff --git a/vendor/github.com/studio-b12/gowebdav/netrc.go b/vendor/github.com/studio-b12/gowebdav/netrc.go
new file mode 100644
index 0000000..df479b5
--- /dev/null
+++ b/vendor/github.com/studio-b12/gowebdav/netrc.go
@@ -0,0 +1,54 @@
+package gowebdav
+
+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 "", ""
+}
diff --git a/vendor/github.com/studio-b12/gowebdav/requests.go b/vendor/github.com/studio-b12/gowebdav/requests.go
new file mode 100644
index 0000000..511e890
--- /dev/null
+++ b/vendor/github.com/studio-b12/gowebdav/requests.go
@@ -0,0 +1,164 @@
+package gowebdav
+
+import (
+ "bytes"
+ "fmt"
+ "io"
+ "net/http"
+ "path"
+ "strings"
+)
+
+func (c *Client) req(method, path string, body io.Reader, intercept func(*http.Request)) (req *http.Response, err error) {
+ // Tee the body, because if authorization fails we will need to read from it again.
+ var r *http.Request
+ var ba bytes.Buffer
+ bb := io.TeeReader(body, &ba)
+
+ if body == nil {
+ r, err = http.NewRequest(method, PathEscape(Join(c.root, path)), nil)
+ } else {
+ r, err = http.NewRequest(method, PathEscape(Join(c.root, path)), bb)
+ }
+
+ if err != nil {
+ return nil, err
+ }
+
+ c.auth.Authorize(c, method, path)
+
+ for k, vals := range c.headers {
+ for _, v := range vals {
+ r.Header.Add(k, v)
+ }
+ }
+
+ if intercept != nil {
+ intercept(r)
+ }
+
+ rs, err := c.c.Do(r)
+ if err != nil {
+ return nil, err
+ }
+
+ if rs.StatusCode == 401 && c.auth.Type() == "NoAuth" {
+ if strings.Index(rs.Header.Get("Www-Authenticate"), "Digest") > -1 {
+ c.auth = &DigestAuth{c.auth.User(), c.auth.Pass(), digestParts(rs)}
+ } else if strings.Index(rs.Header.Get("Www-Authenticate"), "Basic") > -1 {
+ c.auth = &BasicAuth{c.auth.User(), c.auth.Pass()}
+ } else {
+ return rs, newPathError("Authorize", c.root, rs.StatusCode)
+ }
+
+ if body == nil {
+ return c.req(method, path, nil, intercept)
+ } else {
+ return c.req(method, path, &ba, intercept)
+ }
+
+ } else if rs.StatusCode == 401 {
+ return rs, newPathError("Authorize", c.root, rs.StatusCode)
+ }
+
+ return rs, err
+}
+
+func (c *Client) mkcol(path string) int {
+ rs, err := c.req("MKCOL", path, nil, nil)
+ if err != nil {
+ return 400
+ }
+ defer rs.Body.Close()
+
+ if rs.StatusCode == 201 || rs.StatusCode == 405 {
+ return 201
+ }
+
+ return rs.StatusCode
+}
+
+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 fmt.Errorf("%s - %s %s", rs.Status, "PROPFIND", path)
+ }
+
+ return parseXML(rs.Body, resp, parse)
+}
+
+func (c *Client) doCopyMove(method string, oldpath string, newpath string, overwrite bool) (int, io.ReadCloser) {
+ rs, err := c.req(method, oldpath, nil, func(rq *http.Request) {
+ rq.Header.Add("Destination", Join(c.root, newpath))
+ if overwrite {
+ rq.Header.Add("Overwrite", "T")
+ } else {
+ rq.Header.Add("Overwrite", "F")
+ }
+ })
+ if err != nil {
+ return 400, nil
+ }
+ return rs.StatusCode, rs.Body
+}
+
+func (c *Client) copymove(method string, oldpath string, newpath string, overwrite bool) error {
+ s, data := c.doCopyMove(method, oldpath, newpath, overwrite)
+ defer data.Close()
+
+ switch s {
+ case 201, 204:
+ return nil
+
+ case 207:
+ // TODO handle multistat errors, worst case ...
+ log(fmt.Sprintf(" TODO handle %s - %s multistatus result %s", 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) int {
+ rs, err := c.req("PUT", path, stream, nil)
+ if err != nil {
+ return 400
+ }
+ defer rs.Body.Close()
+
+ return rs.StatusCode
+}
+
+func (c *Client) createParentCollection(itemPath string) (err error) {
+ parentPath := path.Dir(itemPath)
+ return c.MkdirAll(parentPath, 0755)
+}
diff --git a/vendor/github.com/studio-b12/gowebdav/utils.go b/vendor/github.com/studio-b12/gowebdav/utils.go
new file mode 100644
index 0000000..e6caf50
--- /dev/null
+++ b/vendor/github.com/studio-b12/gowebdav/utils.go
@@ -0,0 +1,109 @@
+package gowebdav
+
+import (
+ "bytes"
+ "encoding/xml"
+ "fmt"
+ "io"
+ "net/url"
+ "os"
+ "strconv"
+ "strings"
+ "time"
+)
+
+func log(msg interface{}) {
+ fmt.Println(msg)
+}
+
+func newPathError(op string, path string, statusCode int) error {
+ return &os.PathError{
+ Op: op,
+ Path: path,
+ Err: fmt.Errorf("%d", statusCode),
+ }
+}
+
+func newPathErrorErr(op string, path string, err error) error {
+ return &os.PathError{
+ Op: op,
+ Path: path,
+ Err: err,
+ }
+}
+
+// PathEscape escapes all segemnts 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 s[0] != '/' {
+ 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
+}
diff --git a/vendor/modules.txt b/vendor/modules.txt
index 163183f..0e2571a 100644
--- a/vendor/modules.txt
+++ b/vendor/modules.txt
@@ -19,6 +19,8 @@ github.com/stretchr/objx
# github.com/stretchr/testify v1.2.2
github.com/stretchr/testify/mock
github.com/stretchr/testify/assert
+# github.com/studio-b12/gowebdav v0.0.0-20190103184047-38f79aeaf1ac
+github.com/studio-b12/gowebdav
# golang.org/x/net v0.0.0-20181129055619-fae4c4e3ad76
golang.org/x/net/publicsuffix
# golang.org/x/sys v0.0.0-20190422165155-953cdadca894