From 018fc569d3d96cb845c4f37fdd26a951ce94d0a9 Mon Sep 17 00:00:00 2001 From: adphi Date: Sun, 14 Jul 2019 01:49:50 +0200 Subject: [PATCH] fix #6: added gowebdav and webdav interface (TODO: webdav tests) --- auth.go | 5 + client.go | 17 +- go.mod | 1 + go.sum | 2 + types/interfaces.go | 1 + types/webdav.go | 34 ++ .../github.com/studio-b12/gowebdav/.gitignore | 19 + .../studio-b12/gowebdav/.travis.yml | 10 + vendor/github.com/studio-b12/gowebdav/LICENSE | 27 ++ .../github.com/studio-b12/gowebdav/Makefile | 33 ++ .../github.com/studio-b12/gowebdav/README.md | 147 +++++++ .../studio-b12/gowebdav/basicAuth.go | 33 ++ .../github.com/studio-b12/gowebdav/client.go | 380 ++++++++++++++++++ .../studio-b12/gowebdav/digestAuth.go | 146 +++++++ vendor/github.com/studio-b12/gowebdav/doc.go | 3 + vendor/github.com/studio-b12/gowebdav/file.go | 72 ++++ .../github.com/studio-b12/gowebdav/netrc.go | 54 +++ .../studio-b12/gowebdav/requests.go | 164 ++++++++ .../github.com/studio-b12/gowebdav/utils.go | 109 +++++ vendor/modules.txt | 2 + 20 files changed, 1257 insertions(+), 2 deletions(-) create mode 100644 types/webdav.go create mode 100644 vendor/github.com/studio-b12/gowebdav/.gitignore create mode 100644 vendor/github.com/studio-b12/gowebdav/.travis.yml create mode 100644 vendor/github.com/studio-b12/gowebdav/LICENSE create mode 100644 vendor/github.com/studio-b12/gowebdav/Makefile create mode 100644 vendor/github.com/studio-b12/gowebdav/README.md create mode 100644 vendor/github.com/studio-b12/gowebdav/basicAuth.go create mode 100644 vendor/github.com/studio-b12/gowebdav/client.go create mode 100644 vendor/github.com/studio-b12/gowebdav/digestAuth.go create mode 100644 vendor/github.com/studio-b12/gowebdav/doc.go create mode 100644 vendor/github.com/studio-b12/gowebdav/file.go create mode 100644 vendor/github.com/studio-b12/gowebdav/netrc.go create mode 100644 vendor/github.com/studio-b12/gowebdav/requests.go create mode 100644 vendor/github.com/studio-b12/gowebdav/utils.go 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