77 Commits
7 ... cmd_test

Author SHA1 Message Date
Christoph Polcin
0f74925694 wip 2022-11-02 17:37:27 +01:00
Christoph Polcin
200a600c02 Add TestWriteStreamFromPipe 2022-11-02 16:54:56 +01:00
Christoph Polcin
17255f2e74 Updates badges 2022-10-16 01:27:16 +02:00
Christoph Polcin
937a18c9a3 removes travis-ci 2022-10-16 01:22:06 +02:00
Christoph Polcin
d2a480ffa9 updates workflows 2022-10-16 01:13:50 +02:00
Christoph Polcin
8528c01163 updates API 2022-10-16 01:02:00 +02:00
Christoph Polcin
bf6102194f fix: Write creates parent collections on 404 2022-10-16 00:59:14 +02:00
Christoph Polcin
2c20e7e763 Revert "feat: handle 404 on propfind (#57)"
This reverts commit 8190232c06.
2022-10-16 00:59:14 +02:00
Christoph Polcin
fbeb69f25b Add client tests 2022-10-16 00:59:05 +02:00
Christoph Polcin
4adca27344 uses log instead of fmt 2022-10-15 14:44:49 +02:00
Felipe Martin Garcia
8190232c06 feat: handle 404 on propfind (#57)
Client.Stat was not returning a proper Go err for not found files, the
ideal way to check this is using `errors.Is(err, fs.ErrNotExist)` but
the client was returning a generic error.

I've updated the `propfind` to take 404 errors into account, retuning
the above error making easier to evaluate that kind of situations.
2022-10-13 23:11:52 +02:00
zhijian
e70a598e94 supports get range offset with unkown length (#58)
https://www.rfc-editor.org/rfc/rfc9110.html#name-byte-ranges
2022-10-12 18:09:28 +02:00
Ringo Hoffmann
c7b1ff8a5e Improve Error Handling (#54)
* bubble up request errors [#28]

* inhibit stream close on request

* add `StatusError`

* `PUT`: check if given target is a directory

* Revert "inhibit stream close on request"

Cherry-picked into branch dev-bodyclosing.

This reverts commit 2889239999.

Co-authored-by: Christoph Polcin <coco@miconoco.de>
2022-01-28 17:20:35 +01:00
Christoph Polcin
a047320e42 Updates README 2022-01-27 16:33:26 +01:00
Christoph Polcin
b5bd04e2b5 Escapes destination path on copy and move #42 2022-01-27 16:32:05 +01:00
Ringo Hoffmann
3f8721cd4b fix crash when req is called with no body 2021-11-09 09:32:28 +01:00
Ringo Hoffmann
adba8dc051 add .vscode directory 2021-11-09 09:32:28 +01:00
Ringo Hoffmann
2f2cda4122 use seeker when available on request 2021-11-08 09:54:01 +01:00
Ringo Hoffmann
73a7f0bf37 add artifacts workflow 2021-11-07 22:37:54 +01:00
Ringo Hoffmann
aff231de53 add github workflow for unit tests 2021-11-07 22:36:17 +01:00
Ringo Hoffmann
e5dd1e70b1 create go module 2021-11-07 22:35:47 +01:00
Christoph Polcin
29e74efa70 Merge pull request #46 from jkowalski/read-stream-range
added ReadStreamRange() method to efficiently read a range of data
2021-11-06 10:05:35 +01:00
Jarek Kowalski
741fdbda3d added ReadStreamRange() method to efficiently read a range of data
It passes "Range: bytes=X-Y" and if the server returns HTTP 206,
we know it complied with the request.

For servers that don't understand range and return HTTP 200 instead we
discard some bytes and limit the result to emulate this behavior.

This will greatly help https://github.com/kopia/kopia which relies on
partial reads from pack blobs.
2021-11-04 23:32:32 -07:00
Christoph Polcin
a3a86976a1 Merge pull request #45 from marcelblijleven/master
Fix index out of range runtime error when empty string is provided to FixSlashes
2021-09-17 15:32:50 +02:00
Marcel Blijleven
a2cbdfa976 Fix index out of range runtime error when provided string is empty 2021-09-17 13:24:28 +02:00
Marcel Blijleven
9a1ba21162 Fix typo in description 2021-09-17 13:23:09 +02:00
Mark Severson
7ff61aa87b Handle request errors in copymove 2021-06-30 12:06:26 +02:00
Christoph Polcin
86f8378cf1 Update API description 2021-04-27 23:21:33 +02:00
Felix Pojtinger
4145fa842c Add ability to define custom interceptors (fixes #35) 2021-04-27 23:12:51 +02:00
David Holdeman
8244b5a5f5 Merge pull request #41 from needsaholiday/bug/pluscharacter
fix unescape
2021-02-03 15:23:56 -06:00
Nick Kratzke
d02a1ebcd2 switched to PathUnescape 2021-02-03 21:16:30 +01:00
Nick
3ed042db71 fix unescape 2021-02-03 15:25:12 +01:00
Christoph Polcin
bdacfab947 update README due to API changes 2020-09-29 10:07:39 +02:00
Jarek Kowalski
617404b525 fixed panic due to concurrent map writes
Fixes #36
2020-09-28 11:17:19 +02:00
Christoph Polcin
9380631c29 update README 2020-03-03 16:07:24 +01:00
Lukáš Lalinský
a93005d73c Add File.Path to return the full path (#34)
Very useful when iterating over ReadDir results and wanting to do some operation on them.
2020-03-03 15:28:06 +01:00
vitalii
321978fa73 ref: make names more descriptive 2019-12-15 01:31:08 +02:00
vitalii
c4c707907d fix method of getting user's home path 2019-12-15 01:28:41 +02:00
Justus Flerlage
9f625b1b8e Added check for root path 2019-11-14 22:08:20 +02:00
Justus Flerlage
e53b818e1b Added check of parentPath in createParentCollection 2019-11-14 22:08:20 +02:00
vitalii
ff7f737904 fix(requests.go): allow www-authenticate to be case-insensitive. close #32 2019-10-05 22:13:01 +03:00
vitalii
38f79aeaf1 fix uploading file with wrong content. close #30 2019-01-03 20:40:47 +02:00
vitalii
6c32839dbd fix import for command line tool 2018-12-30 13:28:02 +02:00
vitalii
4d70d7ea28 use 'application/xml' instead of 'text/xml'. related with (1) in #15 2018-12-29 22:04:04 +02:00
yatsen1
8bcb1b383c Fix early defer panic. (#29) 2018-11-08 09:39:42 +01:00
vitalii
cba565a9dc links was updated in readme file. Related to studio-b12/gowebdav#27
RFC 2518 was removed because it is obsolete
RFC 4918 was added because it is actual
RFC 5689 was added because it updates RFC 4918
2018-10-24 14:05:51 +03:00
vitalii
425530b55e cmd: readme was updated. closes studio-b12/gowebdav#27
Wrapper script section was updated
2018-10-22 08:29:10 +03:00
vitalii
7493d8befb readme file for command line tool was added
Close #27
2018-10-22 08:10:23 +03:00
vitalii
e29bc0f031 main readme file was updated 2018-10-22 08:09:12 +03:00
vitalii
c8fc9ca590 Merge remote-tracking branch 'upstream/master' 2018-10-21 12:52:46 +03:00
vitalii
a68e21e92b requests: nil pointer dereference panic was fixed [#25]
http.Do() method will return non-nil [error] in following cases:
1. Request is nil
2. Response missing Location header
3. Client failed to parse Location header
4. Method "request.GetBody()" returns error
5. Http.Client.Send() returns error
5. Client timeout was exceeded

Signed-off-by: Christoph Polcin <labs@polcin.de>
2018-10-21 11:15:52 +02:00
vitalii
02aa9bdaeb unused imports was removed 2018-10-20 23:08:19 +03:00
vitalii
8de8ce169b Merge remote-tracking branch 'upstream/master' 2018-07-17 14:00:43 +03:00
Christoph Polcin
3cd755d6c4 make check api 2018-07-14 01:55:58 +02:00
Vitalii
83e3d1e31e Creating parent collection method was added (#22)
* method for creating parent collection was added to Client struct

"func (c *Client) createParentCollection(itemPath string) error" was added to request.go file

* using Client's method to create parent collection

in following methods:
Client.Write()
Client.WriteStream()
Client.copymove()

deadlock is impossible in method Client.copymove() because of paragraph #6 section 9.8.5 (https://tools.ietf.org/html/rfc4918#section-9.8.5) and paragraph #6 section 9.9.4 (https://tools.ietf.org/html/rfc4918#section-9.9.4) of RFC 4918 (https://tools.ietf.org/html/rfc4918)

* install dependencies script was added to Travis-CI file

* testing was added to Travis-CI file

* error wrapping was removed from Client.put() method

* using an early return on error in case of 409 in Client.Write() method
2018-07-14 01:48:30 +02:00
Christoph Polcin
28039fda22 fmt 2018-07-13 12:12:09 +02:00
Christoph Polcin
45a56c2115 cmd: remove Connect() due to #16 2018-07-13 12:11:56 +02:00
vitalii
f821ab73e9 Merge branch 'b12-master' 2018-07-11 12:41:49 +03:00
Vitalii
ec1263db2f all cases of Digest authorization was implemented (#19)
Digest authentication was improved
2018-07-10 18:51:11 +02:00
Vitalii
95706c0747 .gitignore was expanded (#18) 2018-07-08 14:27:10 +02:00
Vitalii
f43a0a4cf8 quick bugfix for issue #20 (#21)
issue title: "Can't upload file with content"
https://github.com/studio-b12/gowebdav/issues/20
2018-07-08 14:25:00 +02:00
vitalii
68824ef55e createParentCollection() function was added 2018-06-21 16:37:02 +03:00
vitalii
6ca20e2a70 copyMove() function returns error in case of 409 status code 2018-06-21 16:35:39 +03:00
vitalii
790397514e all cases of Digest authorization was implemented 2018-06-21 16:35:26 +03:00
vitalii
4ca2f77e2b Merge remote-tracking branch 'upstream/master' 2018-06-21 16:34:17 +03:00
Vitalii
008b27eb0f bug #14 fixed (#17)
"opaque" field should not be specified if server did not provide it
2018-06-21 10:57:32 +02:00
vitalii
d1ebcbebf2 Merge branch 'develop' 2018-06-20 04:02:49 +03:00
vitalii
4f450cfd02 Merge branch 'master' into develop 2018-06-20 04:01:37 +03:00
vitalii
876ef52924 not all webdav servers returns "Dav" header 2018-06-20 03:21:30 +03:00
vitalii
21d86ab356 use import of current fork (instead of original project) 2018-06-20 03:19:23 +03:00
vitalii
97a0b83aeb bug #14 fixed
"opaque" field should not be specified if server did not provide it
2018-06-20 03:17:52 +03:00
vitalii
1fe9163c92 Merge remote-tracking branch 'studio-b12/master' 2018-06-19 11:44:16 +03:00
vitalii
8bab650703 gitignore: .idea folder and *.exe files was added 2018-06-19 11:42:02 +03:00
Christoph Polcin
c4c24955e1 update README 2018-06-19 08:43:13 +02:00
Christoph Polcin
2593a81bf0 Update issue templates 2018-06-19 08:36:40 +02:00
David
b45378c08f Check status on every request to fix #14 2018-06-18 10:02:01 -05:00
vitalii
aebc3ef9d2 .idea/ folder and *.exe files was added to .gitignore 2018-06-14 09:24:10 +03:00
22 changed files with 1480 additions and 266 deletions

27
.github/ISSUE_TEMPLATE/bug_report.md vendored Normal file
View File

@@ -0,0 +1,27 @@
---
name: Bug report
about: Create a report to help us improve
---
Hello Collaborators,
**Describe the bug**
A short description of what you think the bug is.
**Software**
- OS:
- Golang:
- Version:
**To Reproduce**
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
4. See error
**Expected**
A short description of what you expected to happen.
**Additional context**
Add any other context about the problem here.

46
.github/workflows/artifacts.yml vendored Normal file
View File

@@ -0,0 +1,46 @@
name: Build Artifacts
on:
workflow_dispatch:
push:
branches:
- master
paths-ignore:
- "**.md"
jobs:
build_artifacts:
name: Build Artifcats
runs-on: ubuntu-latest
strategy:
matrix:
goos:
- linux
- windows
- darwin
goarch:
- amd64
- arm64
steps:
- name: Set up Go
uses: actions/setup-go@v1
with:
go-version: "^1.17"
- name: Check out code into the Go module directory
uses: actions/checkout@v2
- name: Get dependencies
run: go get ./...
- name: Build Client (${{ matrix.goos }}-${{ matrix.goarch }})
env:
GOOS: ${{ matrix.goos }}
GOARCH: ${{ matrix.goarch }}
run: go build -v -o ./bin/gowebdav-${{ matrix.goos }}-${{ matrix.goarch }} ./cmd/gowebdav/main.go
- name: Rename Windows Binary
if: ${{ matrix.goos == 'windows' }}
env:
FNAME: ./bin/gowebdav-${{ matrix.goos }}-${{ matrix.goarch }}
run: mv ${{ env.FNAME }} ${{ env.FNAME }}.exe
- name: Upload Artifcats
uses: actions/upload-artifact@v2
with:
name: ${{ matrix.goos }}-${{ matrix.goarch }}
path: ./bin/

31
.github/workflows/tests.yml vendored Normal file
View File

@@ -0,0 +1,31 @@
name: Unit Tests
on:
workflow_dispatch:
push:
branches:
- "*"
paths-ignore:
- "**.md"
pull_request:
jobs:
unit_tests:
name: Unit Tests
runs-on: ubuntu-latest
strategy:
matrix:
goversion:
- "1.19"
- "1.18"
- "1.17"
steps:
- name: Set up Go
uses: actions/setup-go@v1
with:
go-version: ${{ matrix.goversion }}
- name: Check out code into the Go module directory
uses: actions/checkout@v2
- name: Get dependencies
run: go get ./...
- name: Run Unit Tests
run: go test -modfile=go_test.mod -v -cover -race ./...

17
.gitignore vendored
View File

@@ -1,4 +1,21 @@
# 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
.vscode/

View File

@@ -1,4 +0,0 @@
language: go
go:
- "1.x"

View File

@@ -9,7 +9,7 @@ ${BIN}: ${SRC}
go build -o $@ ./cmd/gowebdav
test:
go test -v --short ./...
go test -modfile=go_test.mod -v -short -cover ./...
api:
@sed '/^## API$$/,$$d' -i README.md
@@ -25,7 +25,7 @@ check:
@echo
gocyclo -over 15 .
@echo
golint ./...
go vet -modfile=go_test.mod ./...
clean:
@rm -f ${BIN}

374
README.md
View File

@@ -1,87 +1,152 @@
# GoWebDAV
[![Build Status](https://travis-ci.org/studio-b12/gowebdav.svg?branch=master)](https://travis-ci.org/studio-b12/gowebdav)
[![Unit Tests Status](https://github.com/studio-b12/gowebdav/actions/workflows/tests.yml/badge.svg)](https://github.com/studio-b12/gowebdav/actions/workflows/tests.yml)
[![Build Artifacts Status](https://github.com/studio-b12/gowebdav/actions/workflows/artifacts.yml/badge.svg)](https://github.com/studio-b12/gowebdav/actions/workflows/artifacts.yml)
[![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 and command line tool.
A golang WebDAV client library.
## Install command line tool
## Main features
```sh
go get -u github.com/studio-b12/gowebdav/cmd/gowebdav
```
`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
```sh
$ gowebdav --help
Usage of gowebdav
-X string
Method:
LS <PATH>
STAT <PATH>
First of all you should create `Client` instance using `NewClient()` function:
MKDIR <PATH>
MKDIRALL <PATH>
```go
root := "https://webdav.mydomain.me"
user := "user"
password := "password"
GET <PATH> [<FILE>]
PUT <PATH> [<FILE>]
MV <OLD> <NEW>
CP <OLD> <NEW>
DEL <PATH>
-netrc-file string
read login from netrc file (default "~/.netrc")
-pw string
Password [ENV.PASSWORD]
-root string
WebDAV Endpoint [ENV.ROOT]
-user string
User [ENV.USER] (default "$USER")
c := gowebdav.NewClient(root, user, password)
```
*gowebdav wrapper script*
After you can use this `Client` to perform actions, described below.
Create a wrapper script for example `$EDITOR ./dav && chmod a+x ./dav` for your
server and use [pass](https://www.passwordstore.org/ "the standard unix password manager")
or similar tools to retrieve the password.
**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!
```sh
#!/bin/sh
ROOT="https://my.dav.server/" \
USER="foo" \
PASSWORD="$(pass dav/foo@my.dav.server)" \
gowebdav $@
### 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)
```
*Examples*
Using the `dav` wrapper:
```sh
$ ./dav -X LS /
$ echo hi dav! > hello && ./dav -X PUT /hello
$ ./dav -X STAT /hello
$ ./dav -X PUT /hello_dav hello
$ ./dav -X GET /hello_dav
$ ./dav -X GET /hello_dav hello.txt
### Get files list
```go
files, _ := c.ReadDir("folder/subfolder")
for _, file := range files {
//notice that [file] has os.FileInfo type
fmt.Println(file.Name())
}
```
## LINKS
### Download file to byte array
```go
webdavFilePath := "folder/subfolder/file.txt"
localFilePath := "/tmp/webdav/file.txt"
* [RFC 2518 - HTTP Extensions for Distributed Authoring -- WEBDAV](http://www.faqs.org/rfcs/rfc2518.html "RFC 2518 - HTTP Extensions for Distributed Authoring -- WEBDAV")
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.
## API
`import "github.com/studio-b12/gowebdav"`
@@ -98,13 +163,15 @@ included.
### <a name="pkg-index">Index</a>
* [func FixSlash(s string) string](#FixSlash)
* [func FixSlashes(s string) string](#FixSlashes)
* [func IsErrCode(err error, code int) bool](#IsErrCode)
* [func IsErrNotFound(err error) bool](#IsErrNotFound)
* [func Join(path0 string, path1 string) string](#Join)
* [func PathEscape(path string) string](#PathEscape)
* [func ReadConfig(uri, netrc string) (string, string)](#ReadConfig)
* [func String(r io.Reader) string](#String)
* [type Authenticator](#Authenticator)
* [type BasicAuth](#BasicAuth)
* [func (b *BasicAuth) Authorize(c *Client, method string, path string)](#BasicAuth.Authorize)
* [func (b *BasicAuth) Authorize(req *http.Request, method string, path string)](#BasicAuth.Authorize)
* [func (b *BasicAuth) Pass() string](#BasicAuth.Pass)
* [func (b *BasicAuth) Type() string](#BasicAuth.Type)
* [func (b *BasicAuth) User() string](#BasicAuth.User)
@@ -112,22 +179,24 @@ included.
* [func NewClient(uri, user, pw string) *Client](#NewClient)
* [func (c *Client) Connect() error](#Client.Connect)
* [func (c *Client) Copy(oldpath, newpath string, overwrite bool) error](#Client.Copy)
* [func (c *Client) Mkdir(path string, _ os.FileMode) error](#Client.Mkdir)
* [func (c *Client) MkdirAll(path string, _ os.FileMode) error](#Client.MkdirAll)
* [func (c *Client) Mkdir(path string, _ os.FileMode) (err error)](#Client.Mkdir)
* [func (c *Client) MkdirAll(path string, _ os.FileMode) (err error)](#Client.MkdirAll)
* [func (c *Client) Read(path string) ([]byte, error)](#Client.Read)
* [func (c *Client) ReadDir(path string) ([]os.FileInfo, error)](#Client.ReadDir)
* [func (c *Client) ReadStream(path string) (io.ReadCloser, error)](#Client.ReadStream)
* [func (c *Client) ReadStreamRange(path string, offset, length int64) (io.ReadCloser, error)](#Client.ReadStreamRange)
* [func (c *Client) Remove(path string) error](#Client.Remove)
* [func (c *Client) RemoveAll(path string) error](#Client.RemoveAll)
* [func (c *Client) Rename(oldpath, newpath string, overwrite bool) error](#Client.Rename)
* [func (c *Client) SetHeader(key, value string)](#Client.SetHeader)
* [func (c *Client) SetInterceptor(interceptor func(method string, rq *http.Request))](#Client.SetInterceptor)
* [func (c *Client) SetTimeout(timeout time.Duration)](#Client.SetTimeout)
* [func (c *Client) SetTransport(transport http.RoundTripper)](#Client.SetTransport)
* [func (c *Client) Stat(path string) (os.FileInfo, error)](#Client.Stat)
* [func (c *Client) Write(path string, data []byte, _ os.FileMode) error](#Client.Write)
* [func (c *Client) WriteStream(path string, stream io.Reader, _ os.FileMode) error](#Client.WriteStream)
* [func (c *Client) Write(path string, data []byte, _ os.FileMode) (err error)](#Client.Write)
* [func (c *Client) WriteStream(path string, stream io.Reader, _ os.FileMode) (err error)](#Client.WriteStream)
* [type DigestAuth](#DigestAuth)
* [func (d *DigestAuth) Authorize(c *Client, method string, path string)](#DigestAuth.Authorize)
* [func (d *DigestAuth) Authorize(req *http.Request, method string, path string)](#DigestAuth.Authorize)
* [func (d *DigestAuth) Pass() string](#DigestAuth.Pass)
* [func (d *DigestAuth) Type() string](#DigestAuth.Type)
* [func (d *DigestAuth) User() string](#DigestAuth.User)
@@ -138,44 +207,62 @@ included.
* [func (f File) ModTime() time.Time](#File.ModTime)
* [func (f File) Mode() os.FileMode](#File.Mode)
* [func (f File) Name() string](#File.Name)
* [func (f File) Path() string](#File.Path)
* [func (f File) Size() int64](#File.Size)
* [func (f File) String() string](#File.String)
* [func (f File) Sys() interface{}](#File.Sys)
* [type NoAuth](#NoAuth)
* [func (n *NoAuth) Authorize(c *Client, method string, path string)](#NoAuth.Authorize)
* [func (n *NoAuth) Authorize(req *http.Request, method string, path string)](#NoAuth.Authorize)
* [func (n *NoAuth) Pass() string](#NoAuth.Pass)
* [func (n *NoAuth) Type() string](#NoAuth.Type)
* [func (n *NoAuth) User() string](#NoAuth.User)
* [type StatusError](#StatusError)
* [func (se StatusError) Error() string](#StatusError.Error)
##### <a name="pkg-examples">Examples</a>
* [PathEscape](#example_PathEscape)
##### <a name="pkg-files">Package files</a>
[basicAuth.go](https://github.com/studio-b12/gowebdav/blob/master/basicAuth.go) [client.go](https://github.com/studio-b12/gowebdav/blob/master/client.go) [digestAuth.go](https://github.com/studio-b12/gowebdav/blob/master/digestAuth.go) [doc.go](https://github.com/studio-b12/gowebdav/blob/master/doc.go) [file.go](https://github.com/studio-b12/gowebdav/blob/master/file.go) [netrc.go](https://github.com/studio-b12/gowebdav/blob/master/netrc.go) [requests.go](https://github.com/studio-b12/gowebdav/blob/master/requests.go) [utils.go](https://github.com/studio-b12/gowebdav/blob/master/utils.go)
[basicAuth.go](https://github.com/studio-b12/gowebdav/blob/master/basicAuth.go) [client.go](https://github.com/studio-b12/gowebdav/blob/master/client.go) [digestAuth.go](https://github.com/studio-b12/gowebdav/blob/master/digestAuth.go) [doc.go](https://github.com/studio-b12/gowebdav/blob/master/doc.go) [errors.go](https://github.com/studio-b12/gowebdav/blob/master/errors.go) [file.go](https://github.com/studio-b12/gowebdav/blob/master/file.go) [netrc.go](https://github.com/studio-b12/gowebdav/blob/master/netrc.go) [requests.go](https://github.com/studio-b12/gowebdav/blob/master/requests.go) [utils.go](https://github.com/studio-b12/gowebdav/blob/master/utils.go)
### <a name="FixSlash">func</a> [FixSlash](https://github.com/studio-b12/gowebdav/blob/master/utils.go?s=707:737#L45)
### <a name="FixSlash">func</a> [FixSlash](https://github.com/studio-b12/gowebdav/blob/master/utils.go?s=354:384#L23)
``` go
func FixSlash(s string) string
```
FixSlash appends a trailing / to our string
### <a name="FixSlashes">func</a> [FixSlashes](https://github.com/studio-b12/gowebdav/blob/master/utils.go?s=859:891#L53)
### <a name="FixSlashes">func</a> [FixSlashes](https://github.com/studio-b12/gowebdav/blob/master/utils.go?s=506:538#L31)
``` go
func FixSlashes(s string) string
```
FixSlashes appends and prepends a / if they are missing
### <a name="Join">func</a> [Join](https://github.com/studio-b12/gowebdav/blob/master/utils.go?s=976:1020#L61)
### <a name="IsErrCode">func</a> [IsErrCode](https://github.com/studio-b12/gowebdav/blob/master/errors.go?s=355:395#L21)
``` go
func IsErrCode(err error, code int) bool
```
IsErrCode returns true if the given error
is an os.PathError wrapping a StatusError
with the given status code.
### <a name="IsErrNotFound">func</a> [IsErrNotFound](https://github.com/studio-b12/gowebdav/blob/master/errors.go?s=587:621#L31)
``` go
func IsErrNotFound(err error) bool
```
IsErrNotFound is shorthand for IsErrCode
for status 404.
### <a name="Join">func</a> [Join](https://github.com/studio-b12/gowebdav/blob/master/utils.go?s=639:683#L40)
``` go
func Join(path0 string, path1 string) string
```
Join joins two paths
### <a name="PathEscape">func</a> [PathEscape](https://github.com/studio-b12/gowebdav/blob/master/utils.go?s=506:541#L36)
### <a name="PathEscape">func</a> [PathEscape](https://github.com/studio-b12/gowebdav/blob/master/utils.go?s=153:188#L14)
``` go
func PathEscape(path string) string
```
PathEscape escapes all segemnts of a given path
PathEscape escapes all segments of a given path
### <a name="ReadConfig">func</a> [ReadConfig](https://github.com/studio-b12/gowebdav/blob/master/netrc.go?s=428:479#L27)
``` go
@@ -184,162 +271,183 @@ func ReadConfig(uri, netrc string) (string, string)
ReadConfig reads login and password configuration from ~/.netrc
machine foo.com login username password 123456
### <a name="String">func</a> [String](https://github.com/studio-b12/gowebdav/blob/master/utils.go?s=1150:1181#L66)
### <a name="String">func</a> [String](https://github.com/studio-b12/gowebdav/blob/master/utils.go?s=813:844#L45)
``` go
func String(r io.Reader) string
```
String pulls a string out of our io.Reader
### <a name="Authenticator">type</a> [Authenticator](https://github.com/studio-b12/gowebdav/blob/master/client.go?s=285:398#L24)
### <a name="Authenticator">type</a> [Authenticator](https://github.com/studio-b12/gowebdav/blob/master/client.go?s=388:507#L29)
``` go
type Authenticator interface {
Type() string
User() string
Pass() string
Authorize(*Client, string, string)
Authorize(*http.Request, string, string)
}
```
Authenticator stub
### <a name="BasicAuth">type</a> [BasicAuth](https://github.com/studio-b12/gowebdav/blob/master/basicAuth.go?s=94:145#L8)
### <a name="BasicAuth">type</a> [BasicAuth](https://github.com/studio-b12/gowebdav/blob/master/basicAuth.go?s=106:157#L9)
``` go
type BasicAuth struct {
// contains filtered or unexported fields
}
```
BasicAuth structure holds our credentials
#### <a name="BasicAuth.Authorize">func</a> (\*BasicAuth) [Authorize](https://github.com/studio-b12/gowebdav/blob/master/basicAuth.go?s=461:529#L29)
#### <a name="BasicAuth.Authorize">func</a> (\*BasicAuth) [Authorize](https://github.com/studio-b12/gowebdav/blob/master/basicAuth.go?s=473:549#L30)
``` go
func (b *BasicAuth) Authorize(c *Client, method string, path string)
func (b *BasicAuth) Authorize(req *http.Request, method string, path string)
```
Authorize the current request
#### <a name="BasicAuth.Pass">func</a> (\*BasicAuth) [Pass](https://github.com/studio-b12/gowebdav/blob/master/basicAuth.go?s=376:409#L24)
#### <a name="BasicAuth.Pass">func</a> (\*BasicAuth) [Pass](https://github.com/studio-b12/gowebdav/blob/master/basicAuth.go?s=388:421#L25)
``` go
func (b *BasicAuth) Pass() string
```
Pass holds the BasicAuth password
#### <a name="BasicAuth.Type">func</a> (\*BasicAuth) [Type](https://github.com/studio-b12/gowebdav/blob/master/basicAuth.go?s=189:222#L14)
#### <a name="BasicAuth.Type">func</a> (\*BasicAuth) [Type](https://github.com/studio-b12/gowebdav/blob/master/basicAuth.go?s=201:234#L15)
``` go
func (b *BasicAuth) Type() string
```
Type identifies the BasicAuthenticator
#### <a name="BasicAuth.User">func</a> (\*BasicAuth) [User](https://github.com/studio-b12/gowebdav/blob/master/basicAuth.go?s=285:318#L19)
#### <a name="BasicAuth.User">func</a> (\*BasicAuth) [User](https://github.com/studio-b12/gowebdav/blob/master/basicAuth.go?s=297:330#L20)
``` go
func (b *BasicAuth) User() string
```
User holds the BasicAuth username
### <a name="Client">type</a> [Client](https://github.com/studio-b12/gowebdav/blob/master/client.go?s=157:261#L16)
### <a name="Client">type</a> [Client](https://github.com/studio-b12/gowebdav/blob/master/client.go?s=172:364#L18)
``` go
type Client struct {
// contains filtered or unexported fields
}
```
Client defines our structure
#### <a name="NewClient">func</a> [NewClient](https://github.com/studio-b12/gowebdav/blob/master/client.go?s=902:946#L57)
#### <a name="NewClient">func</a> [NewClient](https://github.com/studio-b12/gowebdav/blob/master/client.go?s=1019:1063#L62)
``` go
func NewClient(uri, user, pw string) *Client
```
NewClient creates a new instance of client
#### <a name="Client.Connect">func</a> (\*Client) [Connect](https://github.com/studio-b12/gowebdav/blob/master/client.go?s=1516:1548#L77)
#### <a name="Client.Connect">func</a> (\*Client) [Connect](https://github.com/studio-b12/gowebdav/blob/master/client.go?s=1843:1875#L87)
``` go
func (c *Client) Connect() error
```
Connect connects to our dav server
#### <a name="Client.Copy">func</a> (\*Client) [Copy](https://github.com/studio-b12/gowebdav/blob/master/client.go?s=6960:7028#L314)
#### <a name="Client.Copy">func</a> (\*Client) [Copy](https://github.com/studio-b12/gowebdav/blob/master/client.go?s=6818:6886#L323)
``` go
func (c *Client) Copy(oldpath, newpath string, overwrite bool) error
```
Copy copies a file from A to B
#### <a name="Client.Mkdir">func</a> (\*Client) [Mkdir](https://github.com/studio-b12/gowebdav/blob/master/client.go?s=6051:6107#L273)
#### <a name="Client.Mkdir">func</a> (\*Client) [Mkdir](https://github.com/studio-b12/gowebdav/blob/master/client.go?s=5793:5855#L272)
``` go
func (c *Client) Mkdir(path string, _ os.FileMode) error
func (c *Client) Mkdir(path string, _ os.FileMode) (err error)
```
Mkdir makes a directory
#### <a name="Client.MkdirAll">func</a> (\*Client) [MkdirAll](https://github.com/studio-b12/gowebdav/blob/master/client.go?s=6286:6345#L284)
#### <a name="Client.MkdirAll">func</a> (\*Client) [MkdirAll](https://github.com/studio-b12/gowebdav/blob/master/client.go?s=6068:6133#L286)
``` go
func (c *Client) MkdirAll(path string, _ os.FileMode) error
func (c *Client) MkdirAll(path string, _ os.FileMode) (err error)
```
MkdirAll like mkdir -p, but for webdav
#### <a name="Client.Read">func</a> (\*Client) [Read](https://github.com/studio-b12/gowebdav/blob/master/client.go?s=7134:7184#L319)
#### <a name="Client.Read">func</a> (\*Client) [Read](https://github.com/studio-b12/gowebdav/blob/master/client.go?s=6992:7042#L328)
``` go
func (c *Client) Read(path string) ([]byte, error)
```
Read reads the contents of a remote file
#### <a name="Client.ReadDir">func</a> (\*Client) [ReadDir](https://github.com/studio-b12/gowebdav/blob/master/client.go?s=3126:3186#L131)
#### <a name="Client.ReadDir">func</a> (\*Client) [ReadDir](https://github.com/studio-b12/gowebdav/blob/master/client.go?s=2869:2929#L130)
``` go
func (c *Client) ReadDir(path string) ([]os.FileInfo, error)
```
ReadDir reads the contents of a remote directory
#### <a name="Client.ReadStream">func</a> (\*Client) [ReadStream](https://github.com/studio-b12/gowebdav/blob/master/client.go?s=7495:7558#L337)
#### <a name="Client.ReadStream">func</a> (\*Client) [ReadStream](https://github.com/studio-b12/gowebdav/blob/master/client.go?s=7353:7416#L346)
``` go
func (c *Client) ReadStream(path string) (io.ReadCloser, error)
```
ReadStream reads the stream for a given path
#### <a name="Client.Remove">func</a> (\*Client) [Remove](https://github.com/studio-b12/gowebdav/blob/master/client.go?s=5557:5599#L250)
#### <a name="Client.ReadStreamRange">func</a> (\*Client) [ReadStreamRange](https://github.com/studio-b12/gowebdav/blob/master/client.go?s=8165:8255#L368)
``` go
func (c *Client) ReadStreamRange(path string, offset, length int64) (io.ReadCloser, error)
```
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`.
#### <a name="Client.Remove">func</a> (\*Client) [Remove](https://github.com/studio-b12/gowebdav/blob/master/client.go?s=5299:5341#L249)
``` go
func (c *Client) Remove(path string) error
```
Remove removes a remote file
#### <a name="Client.RemoveAll">func</a> (\*Client) [RemoveAll](https://github.com/studio-b12/gowebdav/blob/master/client.go?s=5665:5710#L255)
#### <a name="Client.RemoveAll">func</a> (\*Client) [RemoveAll](https://github.com/studio-b12/gowebdav/blob/master/client.go?s=5407:5452#L254)
``` go
func (c *Client) RemoveAll(path string) error
```
RemoveAll removes remote files
#### <a name="Client.Rename">func</a> (\*Client) [Rename](https://github.com/studio-b12/gowebdav/blob/master/client.go?s=6794:6864#L309)
#### <a name="Client.Rename">func</a> (\*Client) [Rename](https://github.com/studio-b12/gowebdav/blob/master/client.go?s=6652:6722#L318)
``` go
func (c *Client) Rename(oldpath, newpath string, overwrite bool) error
```
Rename moves a file from A to B
#### <a name="Client.SetHeader">func</a> (\*Client) [SetHeader](https://github.com/studio-b12/gowebdav/blob/master/client.go?s=1099:1144#L62)
#### <a name="Client.SetHeader">func</a> (\*Client) [SetHeader](https://github.com/studio-b12/gowebdav/blob/master/client.go?s=1235:1280#L67)
``` go
func (c *Client) SetHeader(key, value string)
```
SetHeader lets us set arbitrary headers for a given client
#### <a name="Client.SetTimeout">func</a> (\*Client) [SetTimeout](https://github.com/studio-b12/gowebdav/blob/master/client.go?s=1244:1294#L67)
#### <a name="Client.SetInterceptor">func</a> (\*Client) [SetInterceptor](https://github.com/studio-b12/gowebdav/blob/master/client.go?s=1387:1469#L72)
``` go
func (c *Client) SetInterceptor(interceptor func(method string, rq *http.Request))
```
SetInterceptor lets us set an arbitrary interceptor for a given client
#### <a name="Client.SetTimeout">func</a> (\*Client) [SetTimeout](https://github.com/studio-b12/gowebdav/blob/master/client.go?s=1571:1621#L77)
``` go
func (c *Client) SetTimeout(timeout time.Duration)
```
SetTimeout exposes the ability to set a time limit for requests
#### <a name="Client.SetTransport">func</a> (\*Client) [SetTransport](https://github.com/studio-b12/gowebdav/blob/master/client.go?s=1387:1445#L72)
#### <a name="Client.SetTransport">func</a> (\*Client) [SetTransport](https://github.com/studio-b12/gowebdav/blob/master/client.go?s=1714:1772#L82)
``` go
func (c *Client) SetTransport(transport http.RoundTripper)
```
SetTransport exposes the ability to define custom transports
#### <a name="Client.Stat">func</a> (\*Client) [Stat](https://github.com/studio-b12/gowebdav/blob/master/client.go?s=4513:4568#L198)
#### <a name="Client.Stat">func</a> (\*Client) [Stat](https://github.com/studio-b12/gowebdav/blob/master/client.go?s=4255:4310#L197)
``` go
func (c *Client) Stat(path string) (os.FileInfo, error)
```
Stat returns the file stats for a specified path
#### <a name="Client.Write">func</a> (\*Client) [Write](https://github.com/studio-b12/gowebdav/blob/master/client.go?s=7849:7918#L352)
#### <a name="Client.Write">func</a> (\*Client) [Write](https://github.com/studio-b12/gowebdav/blob/master/client.go?s=9260:9335#L402)
``` go
func (c *Client) Write(path string, data []byte, _ os.FileMode) error
func (c *Client) Write(path string, data []byte, _ os.FileMode) (err error)
```
Write writes data to a given path
#### <a name="Client.WriteStream">func</a> (\*Client) [WriteStream](https://github.com/studio-b12/gowebdav/blob/master/client.go?s=8320:8400#L374)
#### <a name="Client.WriteStream">func</a> (\*Client) [WriteStream](https://github.com/studio-b12/gowebdav/blob/master/client.go?s=9759:9845#L432)
``` go
func (c *Client) WriteStream(path string, stream io.Reader, _ os.FileMode) error
func (c *Client) WriteStream(path string, stream io.Reader, _ os.FileMode) (err error)
```
WriteStream writes a stream
@@ -348,12 +456,13 @@ WriteStream writes a stream
type DigestAuth struct {
// contains filtered or unexported fields
}
```
DigestAuth structure holds our credentials
#### <a name="DigestAuth.Authorize">func</a> (\*DigestAuth) [Authorize](https://github.com/studio-b12/gowebdav/blob/master/digestAuth.go?s=577:646#L36)
#### <a name="DigestAuth.Authorize">func</a> (\*DigestAuth) [Authorize](https://github.com/studio-b12/gowebdav/blob/master/digestAuth.go?s=577:654#L36)
``` go
func (d *DigestAuth) Authorize(c *Client, method string, path string)
func (d *DigestAuth) Authorize(req *http.Request, method string, path string)
```
Authorize the current request
@@ -380,94 +489,117 @@ User holds the DigestAuth username
type File struct {
// contains filtered or unexported fields
}
```
File is our structure for a given file
#### <a name="File.ContentType">func</a> (File) [ContentType](https://github.com/studio-b12/gowebdav/blob/master/file.go?s=388:422#L26)
#### <a name="File.ContentType">func</a> (File) [ContentType](https://github.com/studio-b12/gowebdav/blob/master/file.go?s=476:510#L31)
``` go
func (f File) ContentType() string
```
ContentType returns the content type of a file
#### <a name="File.ETag">func</a> (File) [ETag](https://github.com/studio-b12/gowebdav/blob/master/file.go?s=841:868#L51)
#### <a name="File.ETag">func</a> (File) [ETag](https://github.com/studio-b12/gowebdav/blob/master/file.go?s=929:956#L56)
``` go
func (f File) ETag() string
```
ETag returns the ETag of a file
#### <a name="File.IsDir">func</a> (File) [IsDir](https://github.com/studio-b12/gowebdav/blob/master/file.go?s=947:973#L56)
#### <a name="File.IsDir">func</a> (File) [IsDir](https://github.com/studio-b12/gowebdav/blob/master/file.go?s=1035:1061#L61)
``` go
func (f File) IsDir() bool
```
IsDir let us see if a given file is a directory or not
#### <a name="File.ModTime">func</a> (File) [ModTime](https://github.com/studio-b12/gowebdav/blob/master/file.go?s=748:781#L46)
#### <a name="File.ModTime">func</a> (File) [ModTime](https://github.com/studio-b12/gowebdav/blob/master/file.go?s=836:869#L51)
``` go
func (f File) ModTime() time.Time
```
ModTime returns the modified time of a file
#### <a name="File.Mode">func</a> (File) [Mode](https://github.com/studio-b12/gowebdav/blob/master/file.go?s=577:609#L36)
#### <a name="File.Mode">func</a> (File) [Mode](https://github.com/studio-b12/gowebdav/blob/master/file.go?s=665:697#L41)
``` go
func (f File) Mode() os.FileMode
```
Mode will return the mode of a given file
#### <a name="File.Name">func</a> (File) [Name](https://github.com/studio-b12/gowebdav/blob/master/file.go?s=290:317#L21)
#### <a name="File.Name">func</a> (File) [Name](https://github.com/studio-b12/gowebdav/blob/master/file.go?s=378:405#L26)
``` go
func (f File) Name() string
```
Name returns the name of a file
#### <a name="File.Size">func</a> (File) [Size](https://github.com/studio-b12/gowebdav/blob/master/file.go?s=485:511#L31)
#### <a name="File.Path">func</a> (File) [Path](https://github.com/studio-b12/gowebdav/blob/master/file.go?s=295:322#L21)
``` go
func (f File) Path() string
```
Path returns the full path of a file
#### <a name="File.Size">func</a> (File) [Size](https://github.com/studio-b12/gowebdav/blob/master/file.go?s=573:599#L36)
``` go
func (f File) Size() int64
```
Size returns the size of a file
#### <a name="File.String">func</a> (File) [String](https://github.com/studio-b12/gowebdav/blob/master/file.go?s=1095:1124#L66)
#### <a name="File.String">func</a> (File) [String](https://github.com/studio-b12/gowebdav/blob/master/file.go?s=1183:1212#L71)
``` go
func (f File) String() string
```
String lets us see file information
#### <a name="File.Sys">func</a> (File) [Sys](https://github.com/studio-b12/gowebdav/blob/master/file.go?s=1007:1038#L61)
#### <a name="File.Sys">func</a> (File) [Sys](https://github.com/studio-b12/gowebdav/blob/master/file.go?s=1095:1126#L66)
``` go
func (f File) Sys() interface{}
```
Sys ????
### <a name="NoAuth">type</a> [NoAuth](https://github.com/studio-b12/gowebdav/blob/master/client.go?s=442:490#L32)
### <a name="NoAuth">type</a> [NoAuth](https://github.com/studio-b12/gowebdav/blob/master/client.go?s=551:599#L37)
``` go
type NoAuth struct {
// contains filtered or unexported fields
}
```
NoAuth structure holds our credentials
#### <a name="NoAuth.Authorize">func</a> (\*NoAuth) [Authorize](https://github.com/studio-b12/gowebdav/blob/master/client.go?s=785:850#L53)
#### <a name="NoAuth.Authorize">func</a> (\*NoAuth) [Authorize](https://github.com/studio-b12/gowebdav/blob/master/client.go?s=894:967#L58)
``` go
func (n *NoAuth) Authorize(c *Client, method string, path string)
func (n *NoAuth) Authorize(req *http.Request, method string, path string)
```
Authorize the current request
#### <a name="NoAuth.Pass">func</a> (\*NoAuth) [Pass](https://github.com/studio-b12/gowebdav/blob/master/client.go?s=703:733#L48)
#### <a name="NoAuth.Pass">func</a> (\*NoAuth) [Pass](https://github.com/studio-b12/gowebdav/blob/master/client.go?s=812:842#L53)
``` go
func (n *NoAuth) Pass() string
```
Pass returns the current password
#### <a name="NoAuth.Type">func</a> (\*NoAuth) [Type](https://github.com/studio-b12/gowebdav/blob/master/client.go?s=529:559#L38)
#### <a name="NoAuth.Type">func</a> (\*NoAuth) [Type](https://github.com/studio-b12/gowebdav/blob/master/client.go?s=638:668#L43)
``` go
func (n *NoAuth) Type() string
```
Type identifies the authenticator
#### <a name="NoAuth.User">func</a> (\*NoAuth) [User](https://github.com/studio-b12/gowebdav/blob/master/client.go?s=615:645#L43)
#### <a name="NoAuth.User">func</a> (\*NoAuth) [User](https://github.com/studio-b12/gowebdav/blob/master/client.go?s=724:754#L48)
``` go
func (n *NoAuth) User() string
```
User returns the current user
### <a name="StatusError">type</a> [StatusError](https://github.com/studio-b12/gowebdav/blob/master/errors.go?s=114:153#L10)
``` go
type StatusError struct {
Status int
}
```
StatusError implements error and wraps
an erroneous status code.
#### <a name="StatusError.Error">func</a> (StatusError) [Error](https://github.com/studio-b12/gowebdav/blob/master/errors.go?s=155:191#L14)
``` go
func (se StatusError) Error() string
```
- - -
Generated by [godoc2md](http://godoc.org/github.com/davecheney/godoc2md)

View File

@@ -2,6 +2,7 @@ package gowebdav
import (
"encoding/base64"
"net/http"
)
// BasicAuth structure holds our credentials
@@ -26,8 +27,8 @@ func (b *BasicAuth) Pass() string {
}
// Authorize the current request
func (b *BasicAuth) Authorize(c *Client, method string, path string) {
func (b *BasicAuth) Authorize(req *http.Request, method string, path string) {
a := b.user + ":" + b.pw
auth := "Basic " + base64.StdEncoding.EncodeToString([]byte(a))
c.headers.Set("Authorization", auth)
req.Header.Set("Authorization", auth)
}

135
client.go
View File

@@ -3,12 +3,14 @@ package gowebdav
import (
"bytes"
"encoding/xml"
"fmt"
"io"
"net/http"
"net/url"
"os"
pathpkg "path"
"strings"
"sync"
"time"
)
@@ -16,7 +18,10 @@ import (
type Client struct {
root string
headers http.Header
interceptor func(method string, rq *http.Request)
c *http.Client
authMutex sync.Mutex
auth Authenticator
}
@@ -25,7 +30,7 @@ type Authenticator interface {
Type() string
User() string
Pass() string
Authorize(*Client, string, string)
Authorize(*http.Request, string, string)
}
// NoAuth structure holds our credentials
@@ -50,12 +55,12 @@ func (n *NoAuth) Pass() string {
}
// Authorize the current request
func (n *NoAuth) Authorize(c *Client, method string, path string) {
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), &http.Client{}, &NoAuth{user, pw}}
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
@@ -63,6 +68,11 @@ 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
@@ -85,18 +95,7 @@ func (c *Client) Connect() error {
return 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 newPathError("Authorize", c.root, rs.StatusCode)
}
return c.Connect()
} else if rs.StatusCode == 401 {
return newPathError("Authorize", c.root, rs.StatusCode)
} else if rs.StatusCode != 200 || (rs.Header.Get("Dav") == "" && rs.Header.Get("DAV") == "") {
if rs.StatusCode != 200 {
return newPathError("Connect", c.root, rs.StatusCode)
}
@@ -146,7 +145,7 @@ func (c *Client) ReadDir(path string) ([]os.FileInfo, error) {
if p := getProps(r, "200"); p != nil {
f := new(File)
if ps, err := url.QueryUnescape(r.Href); err == nil {
if ps, err := url.PathUnescape(r.Href); err == nil {
f.name = pathpkg.Base(ps)
} else {
f.name = p.Name
@@ -270,9 +269,12 @@ func (c *Client) RemoveAll(path string) error {
}
// Mkdir makes a directory
func (c *Client) Mkdir(path string, _ os.FileMode) error {
func (c *Client) Mkdir(path string, _ os.FileMode) (err error) {
path = FixSlashes(path)
status := c.mkcol(path)
status, err := c.mkcol(path)
if err != nil {
return
}
if status == 201 {
return nil
}
@@ -281,12 +283,16 @@ func (c *Client) Mkdir(path string, _ os.FileMode) error {
}
// MkdirAll like mkdir -p, but for webdav
func (c *Client) MkdirAll(path string, _ os.FileMode) error {
func (c *Client) MkdirAll(path string, _ os.FileMode) (err error) {
path = FixSlashes(path)
status := c.mkcol(path)
status, err := c.mkcol(path)
if err != nil {
return
}
if status == 201 {
return nil
} else if status == 409 {
}
if status == 409 {
paths := strings.Split(path, "/")
sub := "/"
for _, e := range paths {
@@ -294,7 +300,10 @@ func (c *Client) MkdirAll(path string, _ os.FileMode) error {
continue
}
sub += e + "/"
status = c.mkcol(sub)
status, err = c.mkcol(sub)
if err != nil {
return
}
if status != 201 {
return newPathError("MkdirAll", sub, status)
}
@@ -348,22 +357,71 @@ func (c *Client) ReadStream(path string) (io.ReadCloser, error) {
return nil, newPathError("ReadStream", path, rs.StatusCode)
}
// ReadStreamRange reads the stream representing a subset of bytes for a given path,
// utilizing HTTP Range Requests if the server supports it.
// The range is expressed as offset from the start of the file and length, for example
// offset=10, length=10 will return bytes 10 through 19.
//
// If the server does not support partial content requests and returns full content instead,
// this function will emulate the behavior by skipping `offset` bytes and limiting the result
// to `length`.
func (c *Client) ReadStreamRange(path string, offset, length int64) (io.ReadCloser, error) {
rs, err := c.req("GET", path, nil, func(r *http.Request) {
if length > 0 {
r.Header.Add("Range", fmt.Sprintf("bytes=%d-%d", offset, offset+length-1))
} else {
r.Header.Add("Range", fmt.Sprintf("bytes=%d-", offset))
}
})
if err != nil {
return nil, newPathErrorErr("ReadStreamRange", path, err)
}
if rs.StatusCode == http.StatusPartialContent {
// server supported partial content, return as-is.
return rs.Body, nil
}
// server returned success, but did not support partial content, so we have the whole
// stream in rs.Body
if rs.StatusCode == 200 {
// discard first 'offset' bytes.
if _, err := io.Copy(io.Discard, io.LimitReader(rs.Body, offset)); err != nil {
return nil, newPathErrorErr("ReadStreamRange", path, err)
}
// return a io.ReadCloser that is limited to `length` bytes.
return &limitedReadCloser{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) error {
s := c.put(path, bytes.NewReader(data))
func (c *Client) Write(path string, data []byte, _ os.FileMode) (err error) {
s, err := c.put(path, bytes.NewReader(data))
if err != nil {
return
}
switch s {
case 200, 201, 204:
return nil
case 409:
if i := strings.LastIndex(path, "/"); i > -1 {
if err := c.MkdirAll(path[0:i+1], 0755); err == nil {
s = c.put(path, bytes.NewReader(data))
case 404, 409:
err = c.createParentCollection(path)
if err != nil {
return
}
s, err = c.put(path, bytes.NewReader(data))
if err != nil {
return
}
if s == 200 || s == 201 || s == 204 {
return nil
}
}
return
}
}
@@ -371,9 +429,18 @@ func (c *Client) Write(path string, data []byte, _ os.FileMode) error {
}
// WriteStream writes a stream
func (c *Client) WriteStream(path string, stream io.Reader, _ os.FileMode) error {
// TODO check if parent collection exists
s := c.put(path, stream)
func (c *Client) WriteStream(path string, stream io.Reader, _ os.FileMode) (err error) {
err = c.createParentCollection(path)
if err != nil {
return err
}
s, err := c.put(path, stream)
if err != nil {
return err
}
switch s {
case 200, 201, 204:
return nil

445
client_test.go Normal file
View File

@@ -0,0 +1,445 @@
package gowebdav
import (
"bytes"
"context"
"errors"
"fmt"
"io"
"net/http"
"net/http/httptest"
"os"
"strings"
"sync"
"testing"
"time"
"golang.org/x/net/webdav"
)
func basicAuth(h http.Handler) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
if user, passwd, ok := r.BasicAuth(); ok {
if user == "user" && passwd == "password" {
h.ServeHTTP(w, r)
return
}
http.Error(w, "not authorized", 403)
} else {
w.Header().Set("WWW-Authenticate", `Basic realm="x"`)
w.WriteHeader(401)
}
}
}
func fillFs(t *testing.T, fs webdav.FileSystem) context.Context {
ctx := context.Background()
f, err := fs.OpenFile(ctx, "hello.txt", os.O_CREATE, 0644)
if err != nil {
t.Errorf("fail to crate file: %v", err)
}
f.Write([]byte("hello gowebdav\n"))
f.Close()
err = fs.Mkdir(ctx, "/test", 0755)
if err != nil {
t.Errorf("fail to crate directory: %v", err)
}
f, err = fs.OpenFile(ctx, "/test/test.txt", os.O_CREATE, 0644)
if err != nil {
t.Errorf("fail to crate file: %v", err)
}
f.Write([]byte("test test gowebdav\n"))
f.Close()
return ctx
}
func newServer(t *testing.T) (*Client, *httptest.Server, webdav.FileSystem, context.Context) {
mux := http.NewServeMux()
fs := webdav.NewMemFS()
ctx := fillFs(t, fs)
mux.HandleFunc("/", basicAuth(&webdav.Handler{
FileSystem: fs,
LockSystem: webdav.NewMemLS(),
}))
srv := httptest.NewServer(mux)
cli := NewClient(srv.URL, "user", "password")
return cli, srv, fs, ctx
}
func TestConnect(t *testing.T) {
cli, srv, _, _ := newServer(t)
defer srv.Close()
if err := cli.Connect(); err != nil {
t.Fatalf("got error: %v, want nil", err)
}
cli = NewClient(srv.URL, "no", "no")
if err := cli.Connect(); err == nil {
t.Fatalf("got nil, want error: %v", err)
}
}
func TestReadDirConcurrent(t *testing.T) {
cli, srv, _, _ := newServer(t)
defer srv.Close()
var wg sync.WaitGroup
errs := make(chan error, 2)
for i := 0; i < 2; i++ {
wg.Add(1)
go func() {
defer wg.Done()
f, err := cli.ReadDir("/")
if err != nil {
errs <- errors.New(fmt.Sprintf("got error: %v, want file listing: %v", err, f))
}
if len(f) != 2 {
errs <- errors.New(fmt.Sprintf("f: %v err: %v", f, err))
}
if f[0].Name() != "hello.txt" && f[1].Name() != "hello.txt" {
errs <- errors.New(fmt.Sprintf("got: %v, want file: %s", f, "hello.txt"))
}
if f[0].Name() != "test" && f[1].Name() != "test" {
errs <- errors.New(fmt.Sprintf("got: %v, want directory: %s", f, "test"))
}
}()
}
wg.Wait()
close(errs)
for err := range errs {
if err != nil {
t.Fatal(err)
}
}
}
func TestRead(t *testing.T) {
cli, srv, _, _ := newServer(t)
defer srv.Close()
data, err := cli.Read("/hello.txt")
if err != nil || bytes.Compare(data, []byte("hello gowebdav\n")) != 0 {
t.Fatalf("got: %v, want data: %s", err, []byte("hello gowebdav\n"))
}
data, err = cli.Read("/404.txt")
if err == nil {
t.Fatalf("got: %v, want error: %v", data, err)
}
if !IsErrNotFound(err) {
t.Fatalf("got: %v, want 404 error", err)
}
}
func TestReadStream(t *testing.T) {
cli, srv, _, _ := newServer(t)
defer srv.Close()
stream, err := cli.ReadStream("/hello.txt")
if err != nil {
t.Fatalf("got: %v, want data: %v", err, stream)
}
buf := new(bytes.Buffer)
buf.ReadFrom(stream)
if buf.String() != "hello gowebdav\n" {
t.Fatalf("got: %v, want stream: hello gowebdav", buf.String())
}
stream, err = cli.ReadStream("/404/hello.txt")
if err == nil {
t.Fatalf("got: %v, want error: %v", stream, err)
}
}
func TestReadStreamRange(t *testing.T) {
cli, srv, _, _ := newServer(t)
defer srv.Close()
stream, err := cli.ReadStreamRange("/hello.txt", 4, 4)
if err != nil {
t.Fatalf("got: %v, want data: %v", err, stream)
}
buf := new(bytes.Buffer)
buf.ReadFrom(stream)
if buf.String() != "o go" {
t.Fatalf("got: %v, want stream: o go", buf.String())
}
stream, err = cli.ReadStream("/404/hello.txt")
if err == nil {
t.Fatalf("got: %v, want error: %v", stream, err)
}
}
func TestReadStreamRangeUnkownLength(t *testing.T) {
cli, srv, _, _ := newServer(t)
defer srv.Close()
stream, err := cli.ReadStreamRange("/hello.txt", 6, 0)
if err != nil {
t.Fatalf("got: %v, want data: %v", err, stream)
}
buf := new(bytes.Buffer)
buf.ReadFrom(stream)
if buf.String() != "gowebdav\n" {
t.Fatalf("got: %v, want stream: gowebdav\n", buf.String())
}
stream, err = cli.ReadStream("/404/hello.txt")
if err == nil {
t.Fatalf("got: %v, want error: %v", stream, err)
}
}
func TestStat(t *testing.T) {
cli, srv, _, _ := newServer(t)
defer srv.Close()
info, err := cli.Stat("/hello.txt")
if err != nil {
t.Fatalf("got: %v, want os.Info: %v", err, info)
}
if info.Name() != "hello.txt" {
t.Fatalf("got: %v, want file hello.txt", info)
}
info, err = cli.Stat("/404.txt")
if err == nil {
t.Fatalf("got: %v, want error: %v", info, err)
}
if !IsErrNotFound(err) {
t.Fatalf("got: %v, want 404 error", err)
}
}
func TestMkdir(t *testing.T) {
cli, srv, fs, ctx := newServer(t)
defer srv.Close()
info, err := cli.Stat("/newdir")
if err == nil {
t.Fatalf("got: %v, want error: %v", info, err)
}
if err := cli.Mkdir("/newdir", 0755); err != nil {
t.Fatalf("got: %v, want mkdir /newdir", err)
}
if err := cli.Mkdir("/newdir", 0755); err != nil {
t.Fatalf("got: %v, want mkdir /newdir", err)
}
info, err = fs.Stat(ctx, "/newdir")
if err != nil {
t.Fatalf("got: %v, want dir info: %v", err, info)
}
if err := cli.Mkdir("/404/newdir", 0755); err == nil {
t.Fatalf("expected Mkdir error due to missing parent directory")
}
}
func TestMkdirAll(t *testing.T) {
cli, srv, fs, ctx := newServer(t)
defer srv.Close()
if err := cli.MkdirAll("/dir/dir/dir", 0755); err != nil {
t.Fatalf("got: %v, want mkdirAll /dir/dir/dir", err)
}
info, err := fs.Stat(ctx, "/dir/dir/dir")
if err != nil {
t.Fatalf("got: %v, want dir info: %v", err, info)
}
}
func TestCopy(t *testing.T) {
cli, srv, fs, ctx := newServer(t)
defer srv.Close()
info, err := fs.Stat(ctx, "/copy.txt")
if err == nil {
t.Fatalf("got: %v, want error: %v", info, err)
}
if err := cli.Copy("/hello.txt", "/copy.txt", false); err != nil {
t.Fatalf("got: %v, want copy /hello.txt to /copy.txt", err)
}
info, err = fs.Stat(ctx, "/copy.txt")
if err != nil {
t.Fatalf("got: %v, want file info: %v", err, info)
}
if info.Size() != 15 {
t.Fatalf("got: %v, want file size: %d bytes", info.Size(), 15)
}
info, err = fs.Stat(ctx, "/hello.txt")
if err != nil {
t.Fatalf("got: %v, want file info: %v", err, info)
}
if info.Size() != 15 {
t.Fatalf("got: %v, want file size: %d bytes", info.Size(), 15)
}
if err := cli.Copy("/hello.txt", "/copy.txt", false); err == nil {
t.Fatalf("expected copy error due to overwrite false")
}
if err := cli.Copy("/hello.txt", "/copy.txt", true); err != nil {
t.Fatalf("got: %v, want overwrite /copy.txt with /hello.txt", err)
}
}
func TestRename(t *testing.T) {
cli, srv, fs, ctx := newServer(t)
defer srv.Close()
info, err := fs.Stat(ctx, "/copy.txt")
if err == nil {
t.Fatalf("got: %v, want error: %v", info, err)
}
if err := cli.Rename("/hello.txt", "/copy.txt", false); err != nil {
t.Fatalf("got: %v, want mv /hello.txt to /copy.txt", err)
}
info, err = fs.Stat(ctx, "/copy.txt")
if err != nil {
t.Fatalf("got: %v, want file info: %v", err, info)
}
if info.Size() != 15 {
t.Fatalf("got: %v, want file size: %d bytes", info.Size(), 15)
}
if info, err = fs.Stat(ctx, "/hello.txt"); err == nil {
t.Fatalf("got: %v, want error: %v", info, err)
}
if err := cli.Rename("/test/test.txt", "/copy.txt", true); err != nil {
t.Fatalf("got: %v, want overwrite /copy.txt with /hello.txt", err)
}
info, err = fs.Stat(ctx, "/copy.txt")
if err != nil {
t.Fatalf("got: %v, want file info: %v", err, info)
}
if info.Size() != 19 {
t.Fatalf("got: %v, want file size: %d bytes", info.Size(), 19)
}
}
func TestRemove(t *testing.T) {
cli, srv, fs, ctx := newServer(t)
defer srv.Close()
if err := cli.Remove("/hello.txt"); err != nil {
t.Fatalf("got: %v, want nil", err)
}
if info, err := fs.Stat(ctx, "/hello.txt"); err == nil {
t.Fatalf("got: %v, want error: %v", info, err)
}
if err := cli.Remove("/404.txt"); err != nil {
t.Fatalf("got: %v, want nil", err)
}
}
func TestRemoveAll(t *testing.T) {
cli, srv, fs, ctx := newServer(t)
defer srv.Close()
if err := cli.RemoveAll("/test/test.txt"); err != nil {
t.Fatalf("got: %v, want nil", err)
}
if info, err := fs.Stat(ctx, "/test/test.txt"); err == nil {
t.Fatalf("got: %v, want error: %v", info, err)
}
if err := cli.RemoveAll("/404.txt"); err != nil {
t.Fatalf("got: %v, want nil", err)
}
if err := cli.RemoveAll("/404/404/404.txt"); err != nil {
t.Fatalf("got: %v, want nil", err)
}
}
func TestWrite(t *testing.T) {
cli, srv, fs, ctx := newServer(t)
defer srv.Close()
if err := cli.Write("/newfile.txt", []byte("foo bar\n"), 0660); err != nil {
t.Fatalf("got: %v, want nil", err)
}
info, err := fs.Stat(ctx, "/newfile.txt")
if err != nil {
t.Fatalf("got: %v, want file info: %v", err, info)
}
if info.Size() != 8 {
t.Fatalf("got: %v, want file size: %d bytes", info.Size(), 8)
}
if err := cli.Write("/404/newfile.txt", []byte("foo bar\n"), 0660); err != nil {
t.Fatalf("got: %v, want nil", err)
}
}
func TestWriteStream(t *testing.T) {
cli, srv, fs, ctx := newServer(t)
defer srv.Close()
if err := cli.WriteStream("/newfile.txt", strings.NewReader("foo bar\n"), 0660); err != nil {
t.Fatalf("got: %v, want nil", err)
}
info, err := fs.Stat(ctx, "/newfile.txt")
if err != nil {
t.Fatalf("got: %v, want file info: %v", err, info)
}
if info.Size() != 8 {
t.Fatalf("got: %v, want file size: %d bytes", info.Size(), 8)
}
if err := cli.WriteStream("/404/works.txt", strings.NewReader("foo bar\n"), 0660); err != nil {
t.Fatalf("got: %v, want nil", err)
}
if info, err := fs.Stat(ctx, "/404/works.txt"); err != nil {
t.Fatalf("got: %v, want file info: %v", err, info)
}
}
func TestWriteStreamFromPipe(t *testing.T) {
cli, srv, fs, ctx := newServer(t)
defer srv.Close()
r, w := io.Pipe()
go func() {
defer w.Close()
fmt.Fprint(w, "foo")
time.Sleep(1 * time.Second)
fmt.Fprint(w, " ")
time.Sleep(1 * time.Second)
fmt.Fprint(w, "bar\n")
}()
if err := cli.WriteStream("/newfile.txt", r, 0660); err != nil {
t.Fatalf("got: %v, want nil", err)
}
info, err := fs.Stat(ctx, "/newfile.txt")
if err != nil {
t.Fatalf("got: %v, want file info: %v", err, info)
}
if info.Size() != 8 {
t.Fatalf("got: %v, want file size: %d bytes", info.Size(), 8)
}
}

103
cmd/gowebdav/README.md Normal file
View File

@@ -0,0 +1,103 @@
# Description
Command line tool for [gowebdav](https://github.com/studio-b12/gowebdav) library.
# Prerequisites
## Software
* **OS**: all, which are supported by `Golang`
* **Golang**: version 1.x
* **Git**: version 2.14.2 at higher (required to install via `go get`)
# Install
```sh
go get -u github.com/studio-b12/gowebdav/cmd/gowebdav
```
# Usage
It is recommended to set following environment variables to improve your experience with this tool:
* `ROOT` is an URL of target WebDAV server (e.g. `https://webdav.mydomain.me/user_root_folder`)
* `USER` is a login to connect to specified server (e.g. `user`)
* `PASSWORD` is a password to connect to specified server (e.g. `p@s$w0rD`)
In following examples we suppose that:
* environment variable `ROOT` is set to `https://webdav.mydomain.me/ufolder`
* environment variable `USER` is set to `user`
* environment variable `PASSWORD` is set `p@s$w0rD`
* folder `/ufolder/temp` exists on the server
* file `/ufolder/temp/file.txt` exists on the server
* file `/ufolder/temp/document.rtf` exists on the server
* file `/tmp/webdav/to_upload.txt` exists on the local machine
* folder `/tmp/webdav/` is used to download files from the server
## Examples
#### Get content of specified folder
```sh
gowebdav -X LS temp
```
#### Get info about file/folder
```sh
gowebdav -X STAT temp
gowebdav -X STAT temp/file.txt
```
#### Create folder on the remote server
```sh
gowebdav -X MKDIR temp2
gowebdav -X MKDIRALL all/folders/which-you-want/to_create
```
#### Download file
```sh
gowebdav -X GET temp/document.rtf /tmp/webdav/document.rtf
```
You may do not specify target local path, in this case file will be downloaded to the current folder with the
#### Upload file
```sh
gowebdav -X PUT temp/uploaded.txt /tmp/webdav/to_upload.txt
```
#### Move file on the remote server
```sh
gowebdav -X MV temp/file.txt temp/moved_file.txt
```
#### Copy file to another location
```sh
gowebdav -X MV temp/file.txt temp/file-copy.txt
```
#### Delete file from the remote server
```sh
gowebdav -X DEL temp/file.txt
```
# Wrapper script
You can create wrapper script for your server (via `$EDITOR ./dav && chmod a+x ./dav`) and add following content to it:
```sh
#!/bin/sh
ROOT="https://my.dav.server/" \
USER="foo" \
PASSWORD="$(pass dav/foo@my.dav.server)" \
gowebdav $@
```
It allows you to use [pass](https://www.passwordstore.org/ "the standard unix password manager") or similar tools to retrieve the password.
## Examples
Using the `dav` wrapper:
```sh
$ ./dav -X LS /
$ echo hi dav! > hello && ./dav -X PUT /hello
$ ./dav -X STAT /hello
$ ./dav -X PUT /hello_dav hello
$ ./dav -X GET /hello_dav
$ ./dav -X GET /hello_dav hello.txt
```

View File

@@ -4,18 +4,22 @@ import (
"errors"
"flag"
"fmt"
d "github.com/studio-b12/gowebdav"
"io"
"io/fs"
"os"
"os/user"
"path"
"path/filepath"
"runtime"
"strings"
d "github.com/studio-b12/gowebdav"
)
func main() {
root := flag.String("root", os.Getenv("ROOT"), "WebDAV Endpoint [ENV.ROOT]")
usr := flag.String("user", os.Getenv("USER"), "User [ENV.USER]")
pw := flag.String("pw", os.Getenv("PASSWORD"), "Password [ENV.PASSWORD]")
user := flag.String("user", os.Getenv("USER"), "User [ENV.USER]")
password := flag.String("pw", os.Getenv("PASSWORD"), "Password [ENV.PASSWORD]")
netrc := flag.String("netrc-file", filepath.Join(getHome(), ".netrc"), "read login from netrc file")
method := flag.String("X", "", `Method:
LS <PATH>
@@ -38,21 +42,18 @@ func main() {
fail("Set WebDAV ROOT")
}
if l := len(flag.Args()); l == 0 || l > 2 {
if argsLength := len(flag.Args()); argsLength == 0 || argsLength > 2 {
fail("Unsupported arguments")
}
if *pw == "" {
if *password == "" {
if u, p := d.ReadConfig(*root, *netrc); u != "" && p != "" {
usr = &u
pw = &p
user = &u
password = &p
}
}
c := d.NewClient(*root, *usr, *pw)
if err := c.Connect(); err != nil {
fail(fmt.Sprintf("Failed to connect due to: %s", err.Error()))
}
c := d.NewClient(*root, *user, *password)
cmd := getCmd(*method)
@@ -69,10 +70,21 @@ func fail(err interface{}) {
}
func getHome() string {
if u, e := user.Current(); e != nil {
u, e := user.Current()
if e != nil {
return os.Getenv("HOME")
}
if u != nil {
return u.HomeDir
}
return os.Getenv("HOME")
switch runtime.GOOS {
case "windows":
return ""
default:
return "~/"
}
}
func getCmd(method string) func(c *d.Client, p0, p1 string) error {
@@ -181,8 +193,18 @@ func cmdCp(c *d.Client, p0, p1 string) (err error) {
func cmdPut(c *d.Client, p0, p1 string) (err error) {
if p1 == "" {
p1 = filepath.Join(".", p0)
p1 = path.Join(".", p0)
} else {
var fi fs.FileInfo
fi, err = c.Stat(p0)
if err != nil && !d.IsErrNotFound(err) {
return
}
if !d.IsErrNotFound(err) && fi.IsDir() {
p0 = path.Join(p0, p1)
}
}
stream, err := getStream(p1)
if err != nil {
return
@@ -214,8 +236,12 @@ func writeFile(path string, bytes []byte, mode os.FileMode) error {
}
func getStream(pathOrString string) (io.ReadCloser, error) {
fi, err := os.Stat(pathOrString)
if err == nil {
if err != nil {
return nil, err
}
if fi.IsDir() {
return nil, &os.PathError{
Op: "Open",
@@ -223,23 +249,15 @@ func getStream(pathOrString string) (io.ReadCloser, error) {
Err: errors.New("Path: '" + pathOrString + "' is a directory"),
}
}
f, err := os.Open(pathOrString)
if err == nil {
return f, nil
}
return nil, &os.PathError{
Op: "Open",
Path: pathOrString,
Err: err,
}
}
return nopCloser{strings.NewReader(pathOrString)}, nil
}
type nopCloser struct {
io.Reader
}
func (nopCloser) Close() error {
return nil
}

78
cmd/gowebdav/main_test.go Normal file
View File

@@ -0,0 +1,78 @@
package main
import (
"context"
"flag"
"net/http"
"net/http/httptest"
"os"
"testing"
"golang.org/x/net/webdav"
)
func basicAuth(h http.Handler) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
if user, passwd, ok := r.BasicAuth(); ok {
if user == "user" && passwd == "password" {
h.ServeHTTP(w, r)
return
}
http.Error(w, "not authorized", 403)
} else {
w.Header().Set("WWW-Authenticate", `Basic realm="x"`)
w.WriteHeader(401)
}
}
}
func newServer(t *testing.T) (*httptest.Server, webdav.FileSystem, context.Context) {
mux := http.NewServeMux()
fs := webdav.NewMemFS()
ctx := fillFs(t, fs)
mux.HandleFunc("/", basicAuth(&webdav.Handler{
FileSystem: fs,
LockSystem: webdav.NewMemLS(),
}))
srv := httptest.NewServer(mux)
os.Setenv("ROOT", srv.URL)
os.Setenv("USER", "user")
os.Setenv("PASSWORD", "password")
return srv, fs, ctx
}
func fillFs(t *testing.T, fs webdav.FileSystem) context.Context {
ctx := context.Background()
f, err := fs.OpenFile(ctx, "hello.txt", os.O_CREATE, 0644)
if err != nil {
t.Errorf("fail to crate file: %v", err)
}
f.Write([]byte("hello gowebdav\n"))
f.Close()
err = fs.Mkdir(ctx, "/test", 0755)
if err != nil {
t.Errorf("fail to crate directory: %v", err)
}
f, err = fs.OpenFile(ctx, "/test/test.txt", os.O_CREATE, 0644)
if err != nil {
t.Errorf("fail to crate file: %v", err)
}
f.Write([]byte("test test gowebdav\n"))
f.Close()
return ctx
}
func TestLs(t *testing.T) {
oldArgs := os.Args
defer func() { os.Args = oldArgs }()
srv, _, _ := newServer(t)
defer srv.Close()
flag.CommandLine = flag.NewFlagSet("ls", flag.ExitOnError)
os.Args = []string{"ls", "-X", "ls", "/"}
main()
}

View File

@@ -33,23 +33,26 @@ func (d *DigestAuth) Pass() string {
}
// Authorize the current request
func (d *DigestAuth) Authorize(c *Client, method string, path string) {
func (d *DigestAuth) Authorize(req *http.Request, 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))
req.Header.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"}
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.Split(r, `"`)[1]
result[w] = strings.Trim(
strings.SplitN(r, `=`, 2)[1],
`"`,
)
}
}
}
@@ -72,12 +75,72 @@ func getCnonce() string {
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.
ha1 := getMD5(d["username"] + ":" + d["realm"] + ":" + d["password"])
ha2 := getMD5(d["method"] + ":" + d["uri"])
nonceCount := 00000001
cnonce := getCnonce()
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", cnonce="%s", nc="%v", qop="%s", response="%s", opaque="%s"`,
d["username"], d["realm"], d["nonce"], d["uri"], cnonce, nonceCount, d["qop"], response, d["opaque"])
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
}

49
errors.go Normal file
View File

@@ -0,0 +1,49 @@
package gowebdav
import (
"fmt"
"os"
)
// StatusError implements error and wraps
// an erroneous status code.
type StatusError struct {
Status int
}
func (se StatusError) Error() string {
return fmt.Sprintf("%d", se.Status)
}
// IsErrCode returns true if the given error
// is an os.PathError wrapping a StatusError
// with the given status code.
func IsErrCode(err error, code int) bool {
if pe, ok := err.(*os.PathError); ok {
se, ok := pe.Err.(StatusError)
return ok && se.Status == code
}
return false
}
// IsErrNotFound is shorthand for IsErrCode
// for status 404.
func IsErrNotFound(err error) bool {
return IsErrCode(err, 404)
}
func newPathError(op string, path string, statusCode int) error {
return &os.PathError{
Op: op,
Path: path,
Err: StatusError{statusCode},
}
}
func newPathErrorErr(op string, path string, err error) error {
return &os.PathError{
Op: op,
Path: path,
Err: err,
}
}

View File

@@ -17,6 +17,11 @@ type File struct {
isdir bool
}
// Path returns the full path of a file
func (f File) Path() string {
return f.path
}
// Name returns the name of a file
func (f File) Name() string {
return f.name

3
go.mod Normal file
View File

@@ -0,0 +1,3 @@
module github.com/studio-b12/gowebdav
go 1.17

5
go_test.mod Normal file
View File

@@ -0,0 +1,5 @@
module github.com/studio-b12/gowebdav
go 1.17
require golang.org/x/net v0.0.0-20221014081412-f15817d10f9b

7
go_test.sum Normal file
View File

@@ -0,0 +1,7 @@
golang.org/x/net v0.0.0-20221014081412-f15817d10f9b h1:tvrvnPFcdzp294diPnrdZZZ8XUt2Tyj7svb7X52iDuU=
golang.org/x/net v0.0.0-20221014081412-f15817d10f9b/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=

View File

@@ -1,45 +1,109 @@
package gowebdav
import (
"fmt"
"bytes"
"io"
"log"
"net/http"
"path"
"strings"
)
func (c *Client) req(method, path string, body io.Reader, intercept func(*http.Request)) (req *http.Response, err error) {
r, err := http.NewRequest(method, PathEscape(Join(c.root, path)), body)
var r *http.Request
var retryBuf io.Reader
if body != nil {
// If the authorization fails, we will need to restart reading
// from the passed body stream.
// When body is seekable, use seek to reset the streams
// cursor to the start.
// Otherwise, copy the stream into a buffer while uploading
// and use the buffers content on retry.
if sk, ok := body.(io.Seeker); ok {
if _, err = sk.Seek(0, io.SeekStart); err != nil {
return
}
retryBuf = body
} else {
buff := &bytes.Buffer{}
retryBuf = buff
body = io.TeeReader(body, buff)
}
r, err = http.NewRequest(method, PathEscape(Join(c.root, path)), body)
} else {
r, err = http.NewRequest(method, PathEscape(Join(c.root, path)), nil)
}
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)
}
}
// make sure we read 'c.auth' only once since it will be substituted below
// and that is unsafe to do when multiple goroutines are running at the same time.
c.authMutex.Lock()
auth := c.auth
c.authMutex.Unlock()
auth.Authorize(r, method, path)
if intercept != nil {
intercept(r)
}
return c.c.Do(r)
if c.interceptor != nil {
c.interceptor(method, r)
}
func (c *Client) mkcol(path string) int {
rs, err := c.req("MKCOL", path, nil, nil)
defer rs.Body.Close()
rs, err := c.c.Do(r)
if err != nil {
return 400
return nil, err
}
if rs.StatusCode == 201 || rs.StatusCode == 405 {
return 201
if rs.StatusCode == 401 && auth.Type() == "NoAuth" {
wwwAuthenticateHeader := strings.ToLower(rs.Header.Get("Www-Authenticate"))
if strings.Index(wwwAuthenticateHeader, "digest") > -1 {
c.authMutex.Lock()
c.auth = &DigestAuth{auth.User(), auth.Pass(), digestParts(rs)}
c.authMutex.Unlock()
} else if strings.Index(wwwAuthenticateHeader, "basic") > -1 {
c.authMutex.Lock()
c.auth = &BasicAuth{auth.User(), auth.Pass()}
c.authMutex.Unlock()
} else {
return rs, newPathError("Authorize", c.root, rs.StatusCode)
}
return rs.StatusCode
// retryBuf will be nil if body was nil initially so no check
// for body == nil is required here.
return c.req(method, path, retryBuf, intercept)
} else if rs.StatusCode == 401 {
return rs, newPathError("Authorize", c.root, rs.StatusCode)
}
return rs, err
}
func (c *Client) mkcol(path string) (status int, err error) {
rs, err := c.req("MKCOL", path, nil, nil)
if err != nil {
return
}
defer rs.Body.Close()
status = rs.StatusCode
if status == 405 {
status = 201
}
return
}
func (c *Client) options(path string) (*http.Response, error) {
@@ -55,27 +119,36 @@ func (c *Client) propfind(path string, self bool, body string, resp interface{},
} else {
rq.Header.Add("Depth", "1")
}
rq.Header.Add("Content-Type", "text/xml;charset=UTF-8")
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", "")
})
defer rs.Body.Close()
if err != nil {
return err
}
defer rs.Body.Close()
if rs.StatusCode != 207 {
return fmt.Errorf("%s - %s %s", rs.Status, "PROPFIND", path)
return newPathError("PROPFIND", path, rs.StatusCode)
}
return parseXML(rs.Body, resp, parse)
}
func (c *Client) doCopyMove(method string, oldpath string, newpath string, overwrite bool) (int, io.ReadCloser) {
func (c *Client) doCopyMove(
method string,
oldpath string,
newpath string,
overwrite bool,
) (
status int,
r io.ReadCloser,
err error,
) {
rs, err := c.req(method, oldpath, nil, func(rq *http.Request) {
rq.Header.Add("Destination", Join(c.root, newpath))
rq.Header.Add("Destination", PathEscape(Join(c.root, newpath)))
if overwrite {
rq.Header.Add("Overwrite", "T")
} else {
@@ -83,14 +156,21 @@ func (c *Client) doCopyMove(method string, oldpath string, newpath string, overw
}
})
if err != nil {
return 400, nil
return
}
return rs.StatusCode, rs.Body
status = rs.StatusCode
r = rs.Body
return
}
func (c *Client) copymove(method string, oldpath string, newpath string, overwrite bool) error {
s, data := c.doCopyMove(method, oldpath, newpath, overwrite)
func (c *Client) copymove(method string, oldpath string, newpath string, overwrite bool) (err error) {
s, data, err := c.doCopyMove(method, oldpath, newpath, overwrite)
if err != nil {
return
}
if data != nil {
defer data.Close()
}
switch s {
case 201, 204:
@@ -98,21 +178,36 @@ func (c *Client) copymove(method string, oldpath string, newpath string, overwri
case 207:
// TODO handle multistat errors, worst case ...
log(fmt.Sprintf(" TODO handle %s - %s multistatus result %s", method, oldpath, String(data)))
log.Printf("TODO handle %s - %s multistatus result %s\n", method, oldpath, String(data))
case 409:
// TODO create dst path
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 {
func (c *Client) put(path string, stream io.Reader) (status int, err error) {
rs, err := c.req("PUT", path, stream, nil)
defer rs.Body.Close()
if err != nil {
return 400
return
}
defer rs.Body.Close()
status = rs.StatusCode
return
}
return rs.StatusCode
func (c *Client) createParentCollection(itemPath string) (err error) {
parentPath := path.Dir(itemPath)
if parentPath == "." || parentPath == "/" {
return nil
}
return c.MkdirAll(parentPath, 0755)
}

View File

@@ -3,36 +3,14 @@ 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
// PathEscape escapes all segments of a given path
func PathEscape(path string) string {
s := strings.Split(path, "/")
for i, e := range s {
@@ -51,9 +29,10 @@ func FixSlash(s string) string {
// FixSlashes appends and prepends a / if they are missing
func FixSlashes(s string) string {
if s[0] != '/' {
if !strings.HasPrefix(s, "/") {
s = "/" + s
}
return FixSlash(s)
}
@@ -107,3 +86,28 @@ func parseXML(data io.Reader, resp interface{}, parse func(resp interface{}) err
}
return nil
}
// limitedReadCloser wraps a io.ReadCloser and limits the number of bytes that can be read from it.
type limitedReadCloser struct {
rc io.ReadCloser
remaining int
}
func (l *limitedReadCloser) Read(buf []byte) (int, error) {
if l.remaining <= 0 {
return 0, io.EOF
}
if len(buf) > l.remaining {
buf = buf[0:l.remaining]
}
n, err := l.rc.Read(buf)
l.remaining -= n
return n, err
}
func (l *limitedReadCloser) Close() error {
return l.rc.Close()
}

View File

@@ -43,3 +43,25 @@ func TestEscapeURL(t *testing.T) {
t.Error("expected: " + ex + " got: " + u.String())
}
}
func TestFixSlashes(t *testing.T) {
expected := "/"
if got := FixSlashes(""); got != expected {
t.Errorf("expected: %q, got: %q", expected, got)
}
expected = "/path/"
if got := FixSlashes("path"); got != expected {
t.Errorf("expected: %q, got: %q", expected, got)
}
if got := FixSlashes("/path"); got != expected {
t.Errorf("expected: %q, got: %q", expected, got)
}
if got := FixSlashes("path/"); got != expected {
t.Errorf("expected: %q, got: %q", expected, got)
}
}