Full Code of studio-b12/gowebdav for AI

master f7c407706250 cached
29 files
115.8 KB
35.2k tokens
197 symbols
1 requests
Download .txt
Repository: studio-b12/gowebdav
Branch: master
Commit: f7c407706250
Files: 29
Total size: 115.8 KB

Directory structure:
gitextract_nlfpais6/

├── .github/
│   ├── ISSUE_TEMPLATE/
│   │   └── bug_report.md
│   └── workflows/
│       ├── artifacts.yml
│       └── tests.yml
├── .gitignore
├── LICENSE
├── Makefile
├── README.md
├── auth.go
├── auth_test.go
├── basicAuth.go
├── basicAuth_test.go
├── client.go
├── client_test.go
├── cmd/
│   └── gowebdav/
│       ├── README.md
│       └── main.go
├── digestAuth.go
├── digestAuth_test.go
├── doc.go
├── errors.go
├── file.go
├── go.mod
├── go_test.mod
├── go_test.sum
├── netrc.go
├── passportAuth.go
├── passportAuth_test.go
├── requests.go
├── utils.go
└── utils_test.go

================================================
FILE CONTENTS
================================================

================================================
FILE: .github/ISSUE_TEMPLATE/bug_report.md
================================================
---
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.


================================================
FILE: .github/workflows/artifacts.yml
================================================
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@v6
        with:
          check-latest: true
          go-version: "1.25"
      - name: Check out code into the Go module directory
        uses: actions/checkout@v6
      - 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@v6
        with:
          name: ${{ matrix.goos }}-${{ matrix.goarch }}
          path: ./bin/


================================================
FILE: .github/workflows/tests.yml
================================================
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.25"
          - "1.24"
    steps:
      - name: Set up Go
        uses: actions/setup-go@v6
        with:
          check-latest: true
          go-version: ${{ matrix.goversion }}
      - name: Check out code into the Go module directory
        uses: actions/checkout@v6
      - name: Get dependencies
        run: go get ./...
      - name: Run Unit Tests
        run: go test -modfile=go_test.mod -v -cover -race ./...


================================================
FILE: .gitignore
================================================
# 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/

================================================
FILE: LICENSE
================================================
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.


================================================
FILE: Makefile
================================================
BIN := gowebdav
SRC := $(wildcard *.go) cmd/gowebdav/main.go

all: test cmd

cmd: ${BIN}

${BIN}: ${SRC}
	go build -o $@ ./cmd/gowebdav

test:
	go test -modfile=go_test.mod -v -short -cover ./...

api: .go/bin/godoc2md
	@sed '/^## API$$/,$$d' -i README.md
	@echo '## API' >> README.md
	@$< 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: .go/bin/gocyclo
	gofmt -w -s $(SRC)
	@echo
	.go/bin/gocyclo -over 15 .
	@echo
	go vet -modfile=go_test.mod ./...


.go/bin/godoc2md:
	@mkdir -p $(@D)
	@GOPATH="$(CURDIR)/.go" go install github.com/davecheney/godoc2md@latest

.go/bin/gocyclo:
	@mkdir -p $(@D)
	@GOPATH="$(CURDIR)/.go" go install github.com/fzipp/gocyclo/cmd/gocyclo@latest

clean:
	@rm -f ${BIN}

.PHONY: all cmd clean test api check


================================================
FILE: README.md
================================================
# 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 pure Golang WebDAV client library that comes with a [reference implementation](https://github.com/studio-b12/gowebdav/tree/master/cmd/gowebdav).

## Features at a glance

Our `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)

It also provides an [authentication API](#type-authenticator) that makes it easy to encapsulate and control complex authentication challenges.
The default implementation negotiates the algorithm based on the user's preferences and the methods offered by the remote server.

Out-of-box authentication support for:

* [BasicAuth](https://en.wikipedia.org/wiki/Basic_access_authentication)
* [DigestAuth](https://en.wikipedia.org/wiki/Digest_access_authentication)
* [MS-PASS](https://github.com/studio-b12/gowebdav/pull/70#issuecomment-1421713726)
* [WIP Kerberos](https://github.com/studio-b12/gowebdav/pull/71#issuecomment-1416465334)
* [WIP Bearer Token](https://github.com/studio-b12/gowebdav/issues/61)

## 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)
c.Connect()
// kick of your work!
```

After you can use this `Client` to perform actions, described below.

**NOTICE:** We will not check for errors in the 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)
os.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, _ := os.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)
```

For non-seekable stream, this will read data into memory first
to discover content length.

### Upload file via writer with known length
```go
webdavFilePath := "folder/subfolder/file.txt"

bytes := []byte{0x20, 0x20}

c.WriteStreamWithLength(webdavFilePath, bytes.NewBuffer(bytes), int64(len(data)), 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.

## Stargazers over time
[![Stargazers over time](https://starchart.cc/studio-b12/gowebdav.svg?variant=adaptive)](https://starchart.cc/studio-b12/gowebdav)

## API

`import "github.com/studio-b12/gowebdav"`

* [Overview](#pkg-overview)
* [Index](#pkg-index)
* [Examples](#pkg-examples)
* [Subdirectories](#pkg-subdirectories)

### <a name="pkg-overview">Overview</a>
Package gowebdav is a WebDAV client library with a command line tool
included.

### <a name="pkg-index">Index</a>
* [Constants](#pkg-constants)
* [Variables](#pkg-variables)
* [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 NewPathError(op string, path string, statusCode int) error](#NewPathError)
* [func NewPathErrorErr(op string, path string, err error) error](#NewPathErrorErr)
* [func PathEscape(path string) string](#PathEscape)
* [func ReadConfig(uri, netrc string) (string, string)](#ReadConfig)
* [func String(r io.Reader) string](#String)
* [type AuthFactory](#AuthFactory)
* [type Authenticator](#Authenticator)
  * [func NewDigestAuth(login, secret string, rs *http.Response) (Authenticator, error)](#NewDigestAuth)
  * [func NewPassportAuth(c *http.Client, user, pw, partnerURL string, header *http.Header) (Authenticator, error)](#NewPassportAuth)
* [type Authorizer](#Authorizer)
  * [func NewAutoAuth(login string, secret string) Authorizer](#NewAutoAuth)
  * [func NewEmptyAuth() Authorizer](#NewEmptyAuth)
  * [func NewPreemptiveAuth(auth Authenticator) Authorizer](#NewPreemptiveAuth)
* [type BasicAuth](#BasicAuth)
  * [func (b *BasicAuth) Authorize(c *http.Client, rq *http.Request, path string) error](#BasicAuth.Authorize)
  * [func (b *BasicAuth) Clone() Authenticator](#BasicAuth.Clone)
  * [func (b *BasicAuth) Close() error](#BasicAuth.Close)
  * [func (b *BasicAuth) String() string](#BasicAuth.String)
  * [func (b *BasicAuth) Verify(c *http.Client, rs *http.Response, path string) (redo bool, err error)](#BasicAuth.Verify)
* [type Client](#Client)
  * [func NewAuthClient(uri string, auth Authorizer) *Client](#NewAuthClient)
  * [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) (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) SetJar(jar http.CookieJar)](#Client.SetJar)
  * [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) (err error)](#Client.Write)
  * [func (c *Client) WriteStream(path string, stream io.Reader, _ os.FileMode) (err error)](#Client.WriteStream)
  * [func (c *Client) WriteStreamWithLength(path string, stream io.Reader, contentLength int64, _ os.FileMode) (err error)](#Client.WriteStreamWithLength)
* [type DigestAuth](#DigestAuth)
  * [func (d *DigestAuth) Authorize(c *http.Client, rq *http.Request, path string) error](#DigestAuth.Authorize)
  * [func (d *DigestAuth) Clone() Authenticator](#DigestAuth.Clone)
  * [func (d *DigestAuth) Close() error](#DigestAuth.Close)
  * [func (d *DigestAuth) String() string](#DigestAuth.String)
  * [func (d *DigestAuth) Verify(c *http.Client, rs *http.Response, path string) (redo bool, err error)](#DigestAuth.Verify)
* [type File](#File)
  * [func (f File) ContentType() string](#File.ContentType)
  * [func (f File) ETag() string](#File.ETag)
  * [func (f File) IsDir() bool](#File.IsDir)
  * [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 PassportAuth](#PassportAuth)
  * [func (p *PassportAuth) Authorize(c *http.Client, rq *http.Request, path string) error](#PassportAuth.Authorize)
  * [func (p *PassportAuth) Clone() Authenticator](#PassportAuth.Clone)
  * [func (p *PassportAuth) Close() error](#PassportAuth.Close)
  * [func (p *PassportAuth) String() string](#PassportAuth.String)
  * [func (p *PassportAuth) Verify(c *http.Client, rs *http.Response, path string) (redo bool, err error)](#PassportAuth.Verify)
* [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>
[auth.go](https://github.com/studio-b12/gowebdav/blob/master/auth.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) [passportAuth.go](https://github.com/studio-b12/gowebdav/blob/master/passportAuth.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="pkg-constants">Constants</a>
``` go
const XInhibitRedirect = "X-Gowebdav-Inhibit-Redirect"
```

### <a name="pkg-variables">Variables</a>
``` go
var ErrAuthChanged = errors.New("authentication failed, change algorithm")
```
ErrAuthChanged must be returned from the Verify method as an error
to trigger a re-authentication / negotiation with a new authenticator.

``` go
var ErrTooManyRedirects = errors.New("stopped after 10 redirects")
```
ErrTooManyRedirects will be used as return error if a request exceeds 10 redirects.

### <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=506:538#L31)
``` go
func FixSlashes(s string) string
```
FixSlashes appends and prepends a / if they are missing

### <a name="IsErrCode">func</a> [IsErrCode](https://github.com/studio-b12/gowebdav/blob/master/errors.go?s=740:780#L29)
``` 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=972:1006#L39)
``` 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="NewPathError">func</a> [NewPathError](https://github.com/studio-b12/gowebdav/blob/master/errors.go?s=1040:1103#L43)
``` go
func NewPathError(op string, path string, statusCode int) error
```

### <a name="NewPathErrorErr">func</a> [NewPathErrorErr](https://github.com/studio-b12/gowebdav/blob/master/errors.go?s=1194:1255#L51)
``` go
func NewPathErrorErr(op string, path string, err error) error
```

### <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 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
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=813:844#L45)
``` go
func String(r io.Reader) string
```
String pulls a string out of our io.Reader

### <a name="AuthFactory">type</a> [AuthFactory](https://github.com/studio-b12/gowebdav/blob/master/auth.go?s=150:251#L13)
``` go
type AuthFactory func(c *http.Client, rs *http.Response, path string) (auth Authenticator, err error)
```
AuthFactory prototype function to create a new Authenticator

### <a name="Authenticator">type</a> [Authenticator](https://github.com/studio-b12/gowebdav/blob/master/auth.go?s=2156:2696#L56)
``` go
type Authenticator interface {
    // Authorizes a request. Usually by adding some authorization headers.
    Authorize(c *http.Client, rq *http.Request, path string) error
    // Verifies the response if the authorization was successful.
    // May trigger some round trips to pass the authentication.
    // May also trigger a new Authenticator negotiation by returning `ErrAuthChenged`
    Verify(c *http.Client, rs *http.Response, path string) (redo bool, err error)
    // Creates a copy of the underlying Authenticator.
    Clone() Authenticator
    io.Closer
}
```
An Authenticator implements a specific way to authorize requests.
Each request is bound to a separate Authenticator instance.

The authentication flow itself is broken down into `Authorize`
and `Verify` steps. The former method runs before, and the latter
runs after the `Request` is submitted.
This makes it easy to encapsulate and control complex
authentication challenges.

Some authentication flows causing authentication round trips,
which can be archived by returning the `redo` of the Verify
method. `True` restarts the authentication process for the
current action: A new `Request` is spawned, which must be
authorized, sent, and re-verified again, until the action
is successfully submitted.
The preferred way is to handle the authentication ping-pong
within `Verify`, and then `redo` with fresh credentials.

The result of the `Verify` method can also trigger an
`Authenticator` change by returning the `ErrAuthChanged`
as an error. Depending on the `Authorizer` this may trigger
an `Authenticator` negotiation.

Set the `XInhibitRedirect` header to '1' in the `Authorize`
method to get control over request redirection.
Attention! You must handle the incoming request yourself.

To store a shared session state the `Clone` method **must**
return a new instance, initialized with the shared state.

#### <a name="NewDigestAuth">func</a> [NewDigestAuth](https://github.com/studio-b12/gowebdav/blob/master/digestAuth.go?s=324:406#L21)
``` go
func NewDigestAuth(login, secret string, rs *http.Response) (Authenticator, error)
```
NewDigestAuth creates a new instance of our Digest Authenticator

#### <a name="NewPassportAuth">func</a> [NewPassportAuth](https://github.com/studio-b12/gowebdav/blob/master/passportAuth.go?s=386:495#L21)
``` go
func NewPassportAuth(c *http.Client, user, pw, partnerURL string, header *http.Header) (Authenticator, error)
```
constructor for PassportAuth creates a new PassportAuth object and
automatically authenticates against the given partnerURL

### <a name="Authorizer">type</a> [Authorizer](https://github.com/studio-b12/gowebdav/blob/master/auth.go?s=349:764#L17)
``` go
type Authorizer interface {
    // Creates a new Authenticator Shim per request.
    // It may track request related states and perform payload buffering
    // for authentication round trips.
    // The underlying Authenticator will perform the real authentication.
    NewAuthenticator(body io.Reader) (Authenticator, io.Reader)
    // Registers a new Authenticator factory to a key.
    AddAuthenticator(key string, fn AuthFactory)
}
```
Authorizer our Authenticator factory which creates an
`Authenticator` per action/request.

#### <a name="NewAutoAuth">func</a> [NewAutoAuth](https://github.com/studio-b12/gowebdav/blob/master/auth.go?s=3790:3846#L109)
``` go
func NewAutoAuth(login string, secret string) Authorizer
```
NewAutoAuth creates an auto Authenticator factory.
It negotiates the default authentication method
based on the order of the registered Authenticators
and the remotely offered authentication methods.
First In, First Out.

#### <a name="NewEmptyAuth">func</a> [NewEmptyAuth](https://github.com/studio-b12/gowebdav/blob/master/auth.go?s=4695:4725#L132)
``` go
func NewEmptyAuth() Authorizer
```
NewEmptyAuth creates an empty Authenticator factory
The order of adding the Authenticator matters.
First In, First Out.
It offers the `NewAutoAuth` features.

#### <a name="NewPreemptiveAuth">func</a> [NewPreemptiveAuth](https://github.com/studio-b12/gowebdav/blob/master/auth.go?s=5301:5354#L148)
``` go
func NewPreemptiveAuth(auth Authenticator) Authorizer
```
NewPreemptiveAuth creates a preemptive Authenticator
The preemptive authorizer uses the provided Authenticator
for every request regardless of any `Www-Authenticate` header.

It may only have one authentication method,
so calling `AddAuthenticator` **will panic**!

Look out!! This offers the skinniest and slickest implementation
without any synchronisation!!
Still applicable with `BasicAuth` within go routines.

### <a name="BasicAuth">type</a> [BasicAuth](https://github.com/studio-b12/gowebdav/blob/master/basicAuth.go?s=94:145#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=180:262#L15)
``` go
func (b *BasicAuth) Authorize(c *http.Client, rq *http.Request, path string) error
```
Authorize the current request

#### <a name="BasicAuth.Clone">func</a> (\*BasicAuth) [Clone](https://github.com/studio-b12/gowebdav/blob/master/basicAuth.go?s=666:707#L34)
``` go
func (b *BasicAuth) Clone() Authenticator
```
Clone creates a Copy of itself

#### <a name="BasicAuth.Close">func</a> (\*BasicAuth) [Close](https://github.com/studio-b12/gowebdav/blob/master/basicAuth.go?s=581:614#L29)
``` go
func (b *BasicAuth) Close() error
```
Close cleans up all resources

#### <a name="BasicAuth.String">func</a> (\*BasicAuth) [String](https://github.com/studio-b12/gowebdav/blob/master/basicAuth.go?s=778:813#L40)
``` go
func (b *BasicAuth) String() string
```
String toString

#### <a name="BasicAuth.Verify">func</a> (\*BasicAuth) [Verify](https://github.com/studio-b12/gowebdav/blob/master/basicAuth.go?s=352:449#L21)
``` go
func (b *BasicAuth) Verify(c *http.Client, rs *http.Response, path string) (redo bool, err error)
```
Verify verifies if the authentication

### <a name="Client">type</a> [Client](https://github.com/studio-b12/gowebdav/blob/master/client.go?s=220:388#L19)
``` go
type Client struct {
    // contains filtered or unexported fields
}

```
Client defines our structure

#### <a name="NewAuthClient">func</a> [NewAuthClient](https://github.com/studio-b12/gowebdav/blob/master/client.go?s=608:663#L33)
``` go
func NewAuthClient(uri string, auth Authorizer) *Client
```
NewAuthClient creates a new client instance with a custom Authorizer

#### <a name="NewClient">func</a> [NewClient](https://github.com/studio-b12/gowebdav/blob/master/client.go?s=436:480#L28)
``` 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=1829:1861#L74)
``` 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=6815:6883#L310)
``` 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=5790:5852#L259)
``` go
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=6065:6130#L273)
``` go
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=6989:7039#L315)
``` 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=2855:2915#L117)
``` 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=7350:7413#L333)
``` go
func (c *Client) ReadStream(path string) (io.ReadCloser, error)
```
ReadStream reads the stream for a given path

#### <a name="Client.ReadStreamRange">func</a> (\*Client) [ReadStreamRange](https://github.com/studio-b12/gowebdav/blob/master/client.go?s=8162:8252#L355)
``` 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=5296:5338#L236)
``` 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=5404:5449#L241)
``` 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=6649:6719#L305)
``` 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=1092:1137#L49)
``` go
func (c *Client) SetHeader(key, value string)
```
SetHeader lets us set arbitrary headers for a given client

#### <a name="Client.SetInterceptor">func</a> (\*Client) [SetInterceptor](https://github.com/studio-b12/gowebdav/blob/master/client.go?s=1244:1326#L54)
``` 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.SetJar">func</a> (\*Client) [SetJar](https://github.com/studio-b12/gowebdav/blob/master/client.go?s=1727:1770#L69)
``` go
func (c *Client) SetJar(jar http.CookieJar)
```
SetJar exposes the ability to set a cookie jar to the client.

#### <a name="Client.SetTimeout">func</a> (\*Client) [SetTimeout](https://github.com/studio-b12/gowebdav/blob/master/client.go?s=1428:1478#L59)
``` 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=1571:1629#L64)
``` 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=4241:4296#L184)
``` 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=9272:9347#L389)
``` go
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=9857:9943#L419)
``` go
func (c *Client) WriteStream(path string, stream io.Reader, _ os.FileMode) (err error)
```
WriteStream writes a stream - it will copy to memory for non-seekable streams

#### <a name="Client.WriteStreamWithLength">func</a> (\*Client) [WriteStreamWithLength](https://github.com/studio-b12/gowebdav/blob/master/client.go?s=10683:10800#L463)
``` go
func (c *Client) WriteStreamWithLength(path string, stream io.Reader, contentLength int64, _ os.FileMode) (err error)
```
WriteStream writes a stream with a known content length

### <a name="DigestAuth">type</a> [DigestAuth](https://github.com/studio-b12/gowebdav/blob/master/digestAuth.go?s=157:254#L14)
``` go
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=525:608#L26)
``` go
func (d *DigestAuth) Authorize(c *http.Client, rq *http.Request, path string) error
```
Authorize the current request

#### <a name="DigestAuth.Clone">func</a> (\*DigestAuth) [Clone](https://github.com/studio-b12/gowebdav/blob/master/digestAuth.go?s=1303:1345#L54)
``` go
func (d *DigestAuth) Clone() Authenticator
```
Clone creates a copy of itself

#### <a name="DigestAuth.Close">func</a> (\*DigestAuth) [Close](https://github.com/studio-b12/gowebdav/blob/master/digestAuth.go?s=1217:1251#L49)
``` go
func (d *DigestAuth) Close() error
```
Close cleans up all resources

#### <a name="DigestAuth.String">func</a> (\*DigestAuth) [String](https://github.com/studio-b12/gowebdav/blob/master/digestAuth.go?s=1541:1577#L63)
``` go
func (d *DigestAuth) String() string
```
String toString

#### <a name="DigestAuth.Verify">func</a> (\*DigestAuth) [Verify](https://github.com/studio-b12/gowebdav/blob/master/digestAuth.go?s=912:1010#L36)
``` go
func (d *DigestAuth) Verify(c *http.Client, rs *http.Response, path string) (redo bool, err error)
```
Verify checks for authentication issues and may trigger a re-authentication

### <a name="File">type</a> [File](https://github.com/studio-b12/gowebdav/blob/master/file.go?s=93:253#L10)
``` go
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=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=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=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=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=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=378:405#L26)
``` go
func (f File) Name() string
```
Name returns the name of a file

#### <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=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=1095:1126#L66)
``` go
func (f File) Sys() interface{}
```
Sys ????

### <a name="PassportAuth">type</a> [PassportAuth](https://github.com/studio-b12/gowebdav/blob/master/passportAuth.go?s=125:254#L12)
``` go
type PassportAuth struct {
    // contains filtered or unexported fields
}

```
PassportAuth structure holds our credentials

#### <a name="PassportAuth.Authorize">func</a> (\*PassportAuth) [Authorize](https://github.com/studio-b12/gowebdav/blob/master/passportAuth.go?s=690:775#L32)
``` go
func (p *PassportAuth) Authorize(c *http.Client, rq *http.Request, path string) error
```
Authorize the current request

#### <a name="PassportAuth.Clone">func</a> (\*PassportAuth) [Clone](https://github.com/studio-b12/gowebdav/blob/master/passportAuth.go?s=1701:1745#L69)
``` go
func (p *PassportAuth) Clone() Authenticator
```
Clone creates a Copy of itself

#### <a name="PassportAuth.Close">func</a> (\*PassportAuth) [Close](https://github.com/studio-b12/gowebdav/blob/master/passportAuth.go?s=1613:1649#L64)
``` go
func (p *PassportAuth) Close() error
```
Close cleans up all resources

#### <a name="PassportAuth.String">func</a> (\*PassportAuth) [String](https://github.com/studio-b12/gowebdav/blob/master/passportAuth.go?s=2048:2086#L83)
``` go
func (p *PassportAuth) String() string
```
String toString

#### <a name="PassportAuth.Verify">func</a> (\*PassportAuth) [Verify](https://github.com/studio-b12/gowebdav/blob/master/passportAuth.go?s=1075:1175#L46)
``` go
func (p *PassportAuth) Verify(c *http.Client, rs *http.Response, path string) (redo bool, err error)
```
Verify verifies if the authentication is good

### <a name="StatusError">type</a> [StatusError](https://github.com/studio-b12/gowebdav/blob/master/errors.go?s=499:538#L18)
``` 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=540:576#L22)
``` go
func (se StatusError) Error() string
```

- - -
Generated by [godoc2md](http://godoc.org/github.com/davecheney/godoc2md)


================================================
FILE: auth.go
================================================
package gowebdav

import (
	"bytes"
	"errors"
	"io"
	"net/http"
	"strings"
	"sync"
)

// AuthFactory prototype function to create a new Authenticator
type AuthFactory func(c *http.Client, rs *http.Response, path string) (auth Authenticator, err error)

// Authorizer our Authenticator factory which creates an
// `Authenticator` per action/request.
type Authorizer interface {
	// Creates a new Authenticator Shim per request.
	// It may track request related states and perform payload buffering
	// for authentication round trips.
	// The underlying Authenticator will perform the real authentication.
	NewAuthenticator(body io.Reader) (Authenticator, io.Reader)
	// Registers a new Authenticator factory to a key.
	AddAuthenticator(key string, fn AuthFactory)
}

// An Authenticator implements a specific way to authorize requests.
// Each request is bound to a separate Authenticator instance.
//
// The authentication flow itself is broken down into `Authorize`
// and `Verify` steps. The former method runs before, and the latter
// runs after the `Request` is submitted.
// This makes it easy to encapsulate and control complex
// authentication challenges.
//
// Some authentication flows causing authentication round trips,
// which can be archived by returning the `redo` of the Verify
// method. `True` restarts the authentication process for the
// current action: A new `Request` is spawned, which must be
// authorized, sent, and re-verified again, until the action
// is successfully submitted.
// The preferred way is to handle the authentication ping-pong
// within `Verify`, and then `redo` with fresh credentials.
//
// The result of the `Verify` method can also trigger an
// `Authenticator` change by returning the `ErrAuthChanged`
// as an error. Depending on the `Authorizer` this may trigger
// an `Authenticator` negotiation.
//
// Set the `XInhibitRedirect` header to '1' in the `Authorize`
// method to get control over request redirection.
// Attention! You must handle the incoming request yourself.
//
// To store a shared session state the `Clone` method **must**
// return a new instance, initialized with the shared state.
type Authenticator interface {
	// Authorizes a request. Usually by adding some authorization headers.
	Authorize(c *http.Client, rq *http.Request, path string) error
	// Verifies the response if the authorization was successful.
	// May trigger some round trips to pass the authentication.
	// May also trigger a new Authenticator negotiation by returning `ErrAuthChenged`
	Verify(c *http.Client, rs *http.Response, path string) (redo bool, err error)
	// Creates a copy of the underlying Authenticator.
	Clone() Authenticator
	io.Closer
}

type authfactory struct {
	key    string
	create AuthFactory
}

// authorizer structure holds our Authenticator create functions
type authorizer struct {
	factories  []authfactory
	defAuthMux sync.Mutex
	defAuth    Authenticator
}

// preemptiveAuthorizer structure holds the preemptive Authenticator
type preemptiveAuthorizer struct {
	auth Authenticator
}

// authShim structure that wraps the real Authenticator
type authShim struct {
	factory AuthFactory
	body    io.Reader
	auth    Authenticator
}

// negoAuth structure holds the authenticators that are going to be negotiated
type negoAuth struct {
	auths                   []Authenticator
	setDefaultAuthenticator func(auth Authenticator)
}

// nullAuth initializes the whole authentication flow
type nullAuth struct{}

// noAuth structure to perform no authentication at all
type noAuth struct{}

// NewAutoAuth creates an auto Authenticator factory.
// It negotiates the default authentication method
// based on the order of the registered Authenticators
// and the remotely offered authentication methods.
// First In, First Out.
func NewAutoAuth(login string, secret string) Authorizer {
	fmap := make([]authfactory, 0)
	az := &authorizer{factories: fmap, defAuthMux: sync.Mutex{}, defAuth: &nullAuth{}}

	az.AddAuthenticator("basic", func(c *http.Client, rs *http.Response, path string) (auth Authenticator, err error) {
		return &BasicAuth{user: login, pw: secret}, nil
	})

	az.AddAuthenticator("digest", func(c *http.Client, rs *http.Response, path string) (auth Authenticator, err error) {
		return NewDigestAuth(login, secret, rs)
	})

	az.AddAuthenticator("passport1.4", func(c *http.Client, rs *http.Response, path string) (auth Authenticator, err error) {
		return NewPassportAuth(c, login, secret, rs.Request.URL.String(), &rs.Header)
	})

	return az
}

// NewEmptyAuth creates an empty Authenticator factory
// The order of adding the Authenticator matters.
// First In, First Out.
// It offers the `NewAutoAuth` features.
func NewEmptyAuth() Authorizer {
	fmap := make([]authfactory, 0)
	az := &authorizer{factories: fmap, defAuthMux: sync.Mutex{}, defAuth: &nullAuth{}}
	return az
}

// NewPreemptiveAuth creates a preemptive Authenticator
// The preemptive authorizer uses the provided Authenticator
// for every request regardless of any `Www-Authenticate` header.
//
// It may only have one authentication method,
// so calling `AddAuthenticator` **will panic**!
//
// Look out!! This offers the skinniest and slickest implementation
// without any synchronisation!!
// Still applicable with `BasicAuth` within go routines.
func NewPreemptiveAuth(auth Authenticator) Authorizer {
	return &preemptiveAuthorizer{auth: auth}
}

// NewAuthenticator creates an Authenticator (Shim) per request
func (a *authorizer) NewAuthenticator(body io.Reader) (Authenticator, io.Reader) {
	var retryBuf io.Reader = body
	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 _, ok := retryBuf.(io.Seeker); ok {
			body = io.NopCloser(body)
		} else {
			buff := &bytes.Buffer{}
			retryBuf = buff
			body = io.TeeReader(body, buff)
		}
	}
	a.defAuthMux.Lock()
	defAuth := a.defAuth.Clone()
	a.defAuthMux.Unlock()

	return &authShim{factory: a.factory, body: retryBuf, auth: defAuth}, body
}

// AddAuthenticator appends the AuthFactory to our factories.
// It converts the key to lower case and preserves the order.
func (a *authorizer) AddAuthenticator(key string, fn AuthFactory) {
	key = strings.ToLower(key)
	for _, f := range a.factories {
		if f.key == key {
			panic("Authenticator exists: " + key)
		}
	}
	a.factories = append(a.factories, authfactory{key, fn})
}

// factory picks all valid Authenticators based on Www-Authenticate headers
func (a *authorizer) factory(c *http.Client, rs *http.Response, path string) (auth Authenticator, err error) {
	headers := rs.Header.Values("Www-Authenticate")
	if len(headers) > 0 {
		auths := make([]Authenticator, 0)
		for _, f := range a.factories {
			for _, header := range headers {
				headerLower := strings.ToLower(header)
				if strings.Contains(headerLower, f.key) {
					rs.Header.Set("Www-Authenticate", header)
					if auth, err = f.create(c, rs, path); err == nil {
						auths = append(auths, auth)
						break
					}
				}
			}
		}

		switch len(auths) {
		case 0:
			return nil, NewPathError("NoAuthenticator", path, rs.StatusCode)
		case 1:
			auth = auths[0]
		default:
			auth = &negoAuth{auths: auths, setDefaultAuthenticator: a.setDefaultAuthenticator}
		}
	} else {
		auth = &noAuth{}
	}

	a.setDefaultAuthenticator(auth)

	return auth, nil
}

// setDefaultAuthenticator sets the default Authenticator
func (a *authorizer) setDefaultAuthenticator(auth Authenticator) {
	a.defAuthMux.Lock()
	a.defAuth.Close()
	a.defAuth = auth
	a.defAuthMux.Unlock()
}

// Authorize the current request
func (s *authShim) Authorize(c *http.Client, rq *http.Request, path string) error {
	if err := s.auth.Authorize(c, rq, path); err != nil {
		return err
	}
	body := s.body
	rq.GetBody = func() (io.ReadCloser, error) {
		if body != nil {
			if sk, ok := body.(io.Seeker); ok {
				if _, err := sk.Seek(0, io.SeekStart); err != nil {
					return nil, err
				}
			}
			return io.NopCloser(body), nil
		}
		return nil, nil
	}
	return nil
}

// Verify checks for authentication issues and may trigger a re-authentication.
// Catches AlgoChangedErr to update the current Authenticator
func (s *authShim) Verify(c *http.Client, rs *http.Response, path string) (redo bool, err error) {
	redo, err = s.auth.Verify(c, rs, path)
	if err != nil && errors.Is(err, ErrAuthChanged) {
		if auth, aerr := s.factory(c, rs, path); aerr == nil {
			s.auth.Close()
			s.auth = auth
			return true, nil
		} else {
			return false, aerr
		}
	}
	return
}

// Close closes all resources
func (s *authShim) Close() error {
	s.auth.Close()
	s.auth, s.factory = nil, nil
	if s.body != nil {
		if closer, ok := s.body.(io.Closer); ok {
			return closer.Close()
		}
	}
	return nil
}

// It's not intend to Clone the shim
// therefore it returns a noAuth instance
func (s *authShim) Clone() Authenticator {
	return &noAuth{}
}

// String toString
func (s *authShim) String() string {
	return "AuthShim"
}

// Authorize authorizes the current request with the top most Authorizer
func (n *negoAuth) Authorize(c *http.Client, rq *http.Request, path string) error {
	if len(n.auths) == 0 {
		return NewPathError("NoAuthenticator", path, 400)
	}
	return n.auths[0].Authorize(c, rq, path)
}

// Verify verifies the authentication and selects the next one based on the result
func (n *negoAuth) Verify(c *http.Client, rs *http.Response, path string) (redo bool, err error) {
	if len(n.auths) == 0 {
		return false, NewPathError("NoAuthenticator", path, 400)
	}
	redo, err = n.auths[0].Verify(c, rs, path)
	if err != nil {
		if len(n.auths) > 1 {
			n.auths[0].Close()
			n.auths = n.auths[1:]
			return true, nil
		}
	} else if redo {
		return
	} else {
		auth := n.auths[0]
		n.auths = n.auths[1:]
		n.setDefaultAuthenticator(auth)
		return
	}

	return false, NewPathError("NoAuthenticator", path, rs.StatusCode)
}

// Close will close the underlying authenticators.
func (n *negoAuth) Close() error {
	for _, a := range n.auths {
		a.Close()
	}
	n.setDefaultAuthenticator = nil
	return nil
}

// Clone clones the underlying authenticators.
func (n *negoAuth) Clone() Authenticator {
	auths := make([]Authenticator, len(n.auths))
	for i, e := range n.auths {
		auths[i] = e.Clone()
	}
	return &negoAuth{auths: auths, setDefaultAuthenticator: n.setDefaultAuthenticator}
}

func (n *negoAuth) String() string {
	return "NegoAuth"
}

// Authorize the current request
func (n *noAuth) Authorize(c *http.Client, rq *http.Request, path string) error {
	return nil
}

// Verify checks for authentication issues and may trigger a re-authentication
func (n *noAuth) Verify(c *http.Client, rs *http.Response, path string) (redo bool, err error) {
	if "" != rs.Header.Get("Www-Authenticate") {
		err = ErrAuthChanged
	}
	return
}

// Close closes all resources
func (n *noAuth) Close() error {
	return nil
}

// Clone creates a copy of itself
func (n *noAuth) Clone() Authenticator {
	// no copy due to read only access
	return n
}

// String toString
func (n *noAuth) String() string {
	return "NoAuth"
}

// Authorize the current request
func (n *nullAuth) Authorize(c *http.Client, rq *http.Request, path string) error {
	rq.Header.Set(XInhibitRedirect, "1")
	return nil
}

// Verify checks for authentication issues and may trigger a re-authentication
func (n *nullAuth) Verify(c *http.Client, rs *http.Response, path string) (redo bool, err error) {
	return true, ErrAuthChanged
}

// Close closes all resources
func (n *nullAuth) Close() error {
	return nil
}

// Clone creates a copy of itself
func (n *nullAuth) Clone() Authenticator {
	// no copy due to read only access
	return n
}

// String toString
func (n *nullAuth) String() string {
	return "NullAuth"
}

// NewAuthenticator creates an Authenticator (Shim) per request
func (b *preemptiveAuthorizer) NewAuthenticator(body io.Reader) (Authenticator, io.Reader) {
	return b.auth.Clone(), body
}

// AddAuthenticator Will PANIC because it may only have a single authentication method
func (b *preemptiveAuthorizer) AddAuthenticator(key string, fn AuthFactory) {
	panic("You're funny! A preemptive authorizer may only have a single authentication method")
}


================================================
FILE: auth_test.go
================================================
package gowebdav

import (
	"bytes"
	"net/http"
	"strings"
	"testing"
)

func TestEmptyAuth(t *testing.T) {
	auth := NewEmptyAuth()
	srv, _, _ := newAuthSrv(t, basicAuth)
	defer srv.Close()
	cli := NewAuthClient(srv.URL, auth)
	if err := cli.Connect(); err == nil {
		t.Fatalf("got nil want error")
	}
}

func TestRedirectAuthWIP(t *testing.T) {
	hasPassedAuthServer := false
	authHandler := func(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" {
					hasPassedAuthServer = true
					w.WriteHeader(200)
					return
				}
			}
			w.Header().Set("Www-Authenticate", `Basic realm="x"`)
			w.WriteHeader(401)
		}
	}

	psrv, _, _ := newAuthSrv(t, authHandler)
	defer psrv.Close()

	dataHandler := func(h http.Handler) http.HandlerFunc {
		return func(w http.ResponseWriter, r *http.Request) {
			hasAuth := strings.Contains(r.Header.Get("Authorization"), "Basic dXNlcjpwYXNzd29yZA==")

			if hasPassedAuthServer && hasAuth {
				h.ServeHTTP(w, r)
				return
			}
			w.Header().Set("Www-Authenticate", `Basic realm="x"`)
			http.Redirect(w, r, psrv.URL+"/", 302)
		}
	}

	srv, _, _ := newAuthSrv(t, dataHandler)
	defer srv.Close()
	cli := NewClient(srv.URL, "user", "password")
	data, err := cli.Read("/hello.txt")
	if err != nil {
		t.Logf("WIP got error=%v; want nil", err)
	}
	if bytes.Compare(data, []byte("hello gowebdav\n")) != 0 {
		t.Logf("WIP got data=%v; want=hello gowebdav", data)
	}
}


================================================
FILE: basicAuth.go
================================================
package gowebdav

import (
	"fmt"
	"net/http"
)

// BasicAuth structure holds our credentials
type BasicAuth struct {
	user string
	pw   string
}

// Authorize the current request
func (b *BasicAuth) Authorize(c *http.Client, rq *http.Request, path string) error {
	rq.SetBasicAuth(b.user, b.pw)
	return nil
}

// Verify verifies if the authentication
func (b *BasicAuth) Verify(c *http.Client, rs *http.Response, path string) (redo bool, err error) {
	if rs.StatusCode == 401 {
		err = NewPathError("Authorize", path, rs.StatusCode)
	}
	return
}

// Close cleans up all resources
func (b *BasicAuth) Close() error {
	return nil
}

// Clone creates a Copy of itself
func (b *BasicAuth) Clone() Authenticator {
	// no copy due to read only access
	return b
}

// String toString
func (b *BasicAuth) String() string {
	return fmt.Sprintf("BasicAuth login: %s", b.user)
}


================================================
FILE: basicAuth_test.go
================================================
package gowebdav

import (
	"net/http"
	"testing"
)

func TestNewBasicAuth(t *testing.T) {
	a := &BasicAuth{user: "user", pw: "password"}

	ex := "BasicAuth login: user"
	if a.String() != ex {
		t.Error("expected: " + ex + " got: " + a.String())
	}

	if a.Clone() != a {
		t.Error("expected the same instance")
	}

	if a.Close() != nil {
		t.Error("expected close without errors")
	}
}

func TestBasicAuthAuthorize(t *testing.T) {
	a := &BasicAuth{user: "user", pw: "password"}
	rq, _ := http.NewRequest("GET", "http://localhost/", nil)
	a.Authorize(nil, rq, "/")
	if rq.Header.Get("Authorization") != "Basic dXNlcjpwYXNzd29yZA==" {
		t.Error("got wrong Authorization header: " + rq.Header.Get("Authorization"))
	}
}

func TestPreemtiveBasicAuth(t *testing.T) {
	a := &BasicAuth{user: "user", pw: "password"}
	auth := NewPreemptiveAuth(a)
	n, b := auth.NewAuthenticator(nil)
	if b != nil {
		t.Error("expected body to be nil")
	}
	if n != a {
		t.Error("expected the same instance")
	}

	srv, _, _ := newAuthSrv(t, basicAuth)
	defer srv.Close()
	cli := NewAuthClient(srv.URL, auth)
	if err := cli.Connect(); err != nil {
		t.Fatalf("got error: %v, want nil", err)
	}
}


================================================
FILE: client.go
================================================
package gowebdav

import (
	"bytes"
	"encoding/xml"
	"fmt"
	"io"
	"net/http"
	"net/url"
	"os"
	pathpkg "path"
	"strings"
	"time"
)

const XInhibitRedirect = "X-Gowebdav-Inhibit-Redirect"

// Client defines our structure
type Client struct {
	root        string
	headers     http.Header
	interceptor func(method string, rq *http.Request)
	c           *http.Client
	auth        Authorizer
}

// NewClient creates a new instance of client
func NewClient(uri, user, pw string) *Client {
	return NewAuthClient(uri, NewAutoAuth(user, pw))
}

// NewAuthClient creates a new client instance with a custom Authorizer
func NewAuthClient(uri string, auth Authorizer) *Client {
	c := &http.Client{
		CheckRedirect: func(rq *http.Request, via []*http.Request) error {
			if len(via) >= 10 {
				return ErrTooManyRedirects
			}
			if via[0].Header.Get(XInhibitRedirect) != "" {
				return http.ErrUseLastResponse
			}
			return nil
		},
	}
	return &Client{root: FixSlash(uri), headers: make(http.Header), interceptor: nil, c: c, auth: auth}
}

// SetHeader lets us set arbitrary headers for a given client
func (c *Client) SetHeader(key, value string) {
	c.headers.Add(key, value)
}

// SetInterceptor lets us set an arbitrary interceptor for a given client
func (c *Client) SetInterceptor(interceptor func(method string, rq *http.Request)) {
	c.interceptor = interceptor
}

// SetTimeout exposes the ability to set a time limit for requests
func (c *Client) SetTimeout(timeout time.Duration) {
	c.c.Timeout = timeout
}

// SetTransport exposes the ability to define custom transports
func (c *Client) SetTransport(transport http.RoundTripper) {
	c.c.Transport = transport
}

// SetJar exposes the ability to set a cookie jar to the client.
func (c *Client) SetJar(jar http.CookieJar) {
	c.c.Jar = jar
}

// Connect connects to our dav server
func (c *Client) Connect() error {
	rs, err := c.options("/")
	if err != nil {
		return err
	}

	err = rs.Body.Close()
	if err != nil {
		return err
	}

	if rs.StatusCode < 200 || rs.StatusCode >= 300 {
		return NewPathError("Connect", c.root, rs.StatusCode)
	}

	return nil
}

type props struct {
	Status      string   `xml:"DAV: status"`
	Name        string   `xml:"DAV: prop>displayname,omitempty"`
	Type        xml.Name `xml:"DAV: prop>resourcetype>collection,omitempty"`
	Size        string   `xml:"DAV: prop>getcontentlength,omitempty"`
	ContentType string   `xml:"DAV: prop>getcontenttype,omitempty"`
	ETag        string   `xml:"DAV: prop>getetag,omitempty"`
	Modified    string   `xml:"DAV: prop>getlastmodified,omitempty"`
}

type response struct {
	Href  string  `xml:"DAV: href"`
	Props []props `xml:"DAV: propstat"`
}

func getProps(r *response, status string) *props {
	for _, prop := range r.Props {
		if strings.Contains(prop.Status, status) {
			return &prop
		}
	}
	return nil
}

// ReadDir reads the contents of a remote directory
func (c *Client) ReadDir(path string) ([]os.FileInfo, error) {
	path = FixSlashes(path)
	files := make([]os.FileInfo, 0)
	skipSelf := true
	parse := func(resp interface{}) error {
		r := resp.(*response)

		if skipSelf {
			skipSelf = false
			if p := getProps(r, "200"); p != nil && p.Type.Local == "collection" {
				r.Props = nil
				return nil
			}
			return NewPathError("ReadDir", path, 405)
		}

		if p := getProps(r, "200"); p != nil {
			f := new(File)
			if ps, err := url.PathUnescape(r.Href); err == nil {
				f.name = pathpkg.Base(ps)
			} else {
				f.name = p.Name
			}
			f.path = path + f.name
			f.modified = parseModified(&p.Modified)
			f.etag = p.ETag
			f.contentType = p.ContentType

			if p.Type.Local == "collection" {
				f.path += "/"
				f.size = 0
				f.isdir = true
			} else {
				f.size = parseInt64(&p.Size)
				f.isdir = false
			}

			files = append(files, *f)
		}

		r.Props = nil
		return nil
	}

	err := c.propfind(path, false,
		`<d:propfind xmlns:d='DAV:'>
			<d:prop>
				<d:displayname/>
				<d:resourcetype/>
				<d:getcontentlength/>
				<d:getcontenttype/>
				<d:getetag/>
				<d:getlastmodified/>
			</d:prop>
		</d:propfind>`,
		&response{},
		parse)

	if err != nil {
		if _, ok := err.(*os.PathError); !ok {
			err = NewPathErrorErr("ReadDir", path, err)
		}
	}
	return files, err
}

// Stat returns the file stats for a specified path
func (c *Client) Stat(path string) (os.FileInfo, error) {
	var f *File
	parse := func(resp interface{}) error {
		r := resp.(*response)
		if p := getProps(r, "200"); p != nil && f == nil {
			f = new(File)
			f.name = p.Name
			f.path = path
			f.etag = p.ETag
			f.contentType = p.ContentType

			if p.Type.Local == "collection" {
				if !strings.HasSuffix(f.path, "/") {
					f.path += "/"
				}
				f.size = 0
				f.modified = parseModified(&p.Modified)
				f.isdir = true
			} else {
				f.size = parseInt64(&p.Size)
				f.modified = parseModified(&p.Modified)
				f.isdir = false
			}
		}

		r.Props = nil
		return nil
	}

	err := c.propfind(path, true,
		`<d:propfind xmlns:d='DAV:'>
			<d:prop>
				<d:displayname/>
				<d:resourcetype/>
				<d:getcontentlength/>
				<d:getcontenttype/>
				<d:getetag/>
				<d:getlastmodified/>
			</d:prop>
		</d:propfind>`,
		&response{},
		parse)

	if err != nil {
		if _, ok := err.(*os.PathError); !ok {
			err = NewPathErrorErr("ReadDir", path, err)
		}
	}
	return f, err
}

// Remove removes a remote file
func (c *Client) Remove(path string) error {
	return c.RemoveAll(path)
}

// RemoveAll removes remote files
func (c *Client) RemoveAll(path string) error {
	rs, err := c.req("DELETE", path, nil, nil)
	if err != nil {
		return NewPathError("Remove", path, 400)
	}
	err = rs.Body.Close()
	if err != nil {
		return err
	}

	if rs.StatusCode == 200 || rs.StatusCode == 204 || rs.StatusCode == 404 {
		return nil
	}

	return NewPathError("Remove", path, rs.StatusCode)
}

// Mkdir makes a directory
func (c *Client) Mkdir(path string, _ os.FileMode) (err error) {
	path = FixSlashes(path)
	status, err := c.mkcol(path)
	if err != nil {
		return
	}
	if status == 201 {
		return nil
	}

	return NewPathError("Mkdir", path, status)
}

// MkdirAll like mkdir -p, but for webdav
func (c *Client) MkdirAll(path string, _ os.FileMode) (err error) {
	path = FixSlashes(path)
	status, err := c.mkcol(path)
	if err != nil {
		return
	}
	if status == 201 {
		return nil
	}
	if status == 409 {
		paths := strings.Split(path, "/")
		sub := "/"
		for _, e := range paths {
			if e == "" {
				continue
			}
			sub += e + "/"
			status, err = c.mkcol(sub)
			if err != nil {
				return
			}
			if status != 201 {
				return NewPathError("MkdirAll", sub, status)
			}
		}
		return nil
	}

	return NewPathError("MkdirAll", path, status)
}

// Rename moves a file from A to B
func (c *Client) Rename(oldpath, newpath string, overwrite bool) error {
	return c.copymove("MOVE", oldpath, newpath, overwrite)
}

// Copy copies a file from A to B
func (c *Client) Copy(oldpath, newpath string, overwrite bool) error {
	return c.copymove("COPY", oldpath, newpath, overwrite)
}

// Read reads the contents of a remote file
func (c *Client) Read(path string) ([]byte, error) {
	var stream io.ReadCloser
	var err error

	if stream, err = c.ReadStream(path); 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)
}

// 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{rc: rs.Body, remaining: int(length)}, nil
	}

	rs.Body.Close()
	return nil, NewPathError("ReadStream", path, rs.StatusCode)
}

// Write writes data to a given path
func (c *Client) Write(path string, data []byte, _ os.FileMode) (err error) {
	s, err := c.put(path, bytes.NewReader(data), int64(len(data)))
	if err != nil {
		return
	}

	switch s {

	case 200, 201, 204:
		return nil

	case 404, 409:
		err = c.createParentCollection(path)
		if err != nil {
			return
		}

		s, err = c.put(path, bytes.NewReader(data), int64(len(data)))
		if err != nil {
			return
		}
		if s == 200 || s == 201 || s == 204 {
			return
		}
	}

	return NewPathError("Write", path, s)
}

// WriteStream writes a stream - it will copy to memory for non-seekable streams
func (c *Client) WriteStream(path string, stream io.Reader, _ os.FileMode) (err error) {

	err = c.createParentCollection(path)
	if err != nil {
		return err
	}

	contentLength := int64(0)
	if seeker, ok := stream.(io.Seeker); ok {
		contentLength, err = seeker.Seek(0, io.SeekEnd)
		if err != nil {
			return err
		}

		_, err = seeker.Seek(0, io.SeekStart)
		if err != nil {
			return err
		}
	} else {
		buffer := bytes.NewBuffer(make([]byte, 0, 1024*1024 /* 1MB */))

		contentLength, err = io.Copy(buffer, stream)
		if err != nil {
			return err
		}

		stream = buffer
	}

	s, err := c.put(path, stream, contentLength)
	if err != nil {
		return err
	}

	switch s {
	case 200, 201, 204:
		return nil

	default:
		return NewPathError("WriteStream", path, s)
	}
}

// WriteStream writes a stream with a known content length
func (c *Client) WriteStreamWithLength(path string, stream io.Reader, contentLength int64, _ os.FileMode) (err error) {
	err = c.createParentCollection(path)
	if err != nil {
		return err
	}

	s, err := c.put(path, stream, contentLength)
	if err != nil {
		return err
	}

	switch s {
	case 200, 201, 204:
		return nil

	default:
		return NewPathError("WriteStreamWithLength", path, s)
	}
}


================================================
FILE: client_test.go
================================================
package gowebdav

import (
	"bytes"
	"context"
	"crypto/rand"
	"errors"
	"fmt"
	"io"
	"net/http"
	"net/http/httptest"
	"os"
	"strings"
	"sync"
	"testing"
	"time"

	"golang.org/x/net/webdav"
)

func noAuthHndl(h http.Handler) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		h.ServeHTTP(w, r)
	}
}

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 basicAuthNoContent(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" {
				w.WriteHeader(http.StatusNoContent)
				h.ServeHTTP(w, r)
				return
			}

			http.Error(w, "not authorized", 403)
		} else {
			w.Header().Set("WWW-Authenticate", `Basic realm="x"`)
			w.WriteHeader(401)
		}
	}
}

func basicAuthWithPostHandlerFunc(h http.Handler, postHandlerFunc http.HandlerFunc) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		user, passwd, ok := r.BasicAuth()
		if !ok {
			w.Header().Set("WWW-Authenticate", `Basic realm="x"`)
			w.WriteHeader(401)
			return
		}

		if user != "user" || passwd != "password" {
			http.Error(w, "not authorized", 403)
			return
		}

		h.ServeHTTP(w, r)
		postHandlerFunc(w, r)
	}
}

func multipleAuth(h http.Handler) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		notAuthed := false
		if r.Header.Get("Authorization") == "" {
			notAuthed = true
		} else if user, passwd, ok := r.BasicAuth(); ok {
			if user == "user" && passwd == "password" {
				h.ServeHTTP(w, r)
				return
			}
			notAuthed = true
		} else if strings.HasPrefix(r.Header.Get("Authorization"), "Digest ") {
			pairs := strings.TrimPrefix(r.Header.Get("Authorization"), "Digest ")
			digestParts := make(map[string]string)
			for _, pair := range strings.Split(pairs, ",") {
				kv := strings.SplitN(strings.TrimSpace(pair), "=", 2)
				key, value := kv[0], kv[1]
				value = strings.Trim(value, `"`)
				digestParts[key] = value
			}
			if digestParts["qop"] == "" {
				digestParts["qop"] = "auth"
			}

			ha1 := getMD5(fmt.Sprint(digestParts["username"], ":", digestParts["realm"], ":", "digestPW"))
			ha2 := getMD5(fmt.Sprint(r.Method, ":", digestParts["uri"]))
			expected := getMD5(fmt.Sprint(ha1,
				":", digestParts["nonce"],
				":", digestParts["nc"],
				":", digestParts["cnonce"],
				":", digestParts["qop"],
				":", ha2))

			if expected == digestParts["response"] {
				h.ServeHTTP(w, r)
				return
			}
			notAuthed = true
		}

		if notAuthed {
			w.Header().Add("WWW-Authenticate", `Digest realm="testrealm@host.com", qop="auth,auth-int",nonce="dcd98b7102dd2f0e8b11d0f600bfb0c093",opaque="5ccc069c403ebaf9f0171e9517f40e41"`)
			w.Header().Add("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) {
	return newAuthServer(t, basicAuth)
}

func newServerAuthNoContent(t *testing.T) (*Client, *httptest.Server, webdav.FileSystem, context.Context) {
	return newAuthServer(t, basicAuthNoContent)
}

func newAuthServer(t *testing.T, auth func(h http.Handler) http.HandlerFunc) (*Client, *httptest.Server, webdav.FileSystem, context.Context) {
	srv, fs, ctx := newAuthSrv(t, auth)
	cli := NewClient(srv.URL, "user", "password")
	return cli, srv, fs, ctx
}

func newAuthSrv(t *testing.T, auth func(h http.Handler) http.HandlerFunc) (*httptest.Server, webdav.FileSystem, context.Context) {
	mux := http.NewServeMux()
	fs := webdav.NewMemFS()
	ctx := fillFs(t, fs)
	mux.HandleFunc("/", auth(&webdav.Handler{
		FileSystem: fs,
		LockSystem: webdav.NewMemLS(),
	}))
	srv := httptest.NewServer(mux)
	return srv, fs, ctx
}

func newAuthServerAcquireContentLength(t *testing.T) (*Client, *httptest.Server, webdav.FileSystem, context.Context) {
	srv, fs, ctx := newAuthSrvAcquireContentLength(t, basicAuthWithPostHandlerFunc)
	cli := NewClient(srv.URL, "user", "password")
	return cli, srv, fs, ctx
}

func newAuthSrvAcquireContentLength(t *testing.T, authWithPostHandlerFunc func(h http.Handler, postHandlerFunc http.HandlerFunc) http.HandlerFunc) (*httptest.Server, webdav.FileSystem, context.Context) {
	mux := http.NewServeMux()
	fs := webdav.NewMemFS()
	ctx := fillFs(t, fs)
	mux.HandleFunc("/", authWithPostHandlerFunc(&webdav.Handler{
		FileSystem: fs,
		LockSystem: webdav.NewMemLS(),
	}, func(w http.ResponseWriter, r *http.Request) {
		if r.Method != http.MethodPut {
			return
		}

		fileName := strings.TrimPrefix(r.URL.Path, "/")
		stat, err := fs.Stat(ctx, fileName)
		if err != nil {
			t.Fatalf("got: %v, want nil", err)
		}

		if r.ContentLength != stat.Size() {
			t.Fatalf("acquire content length got: %v, want %v", r.ContentLength, stat.Size())
		}
	}))
	srv := httptest.NewServer(mux)
	return 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 TestConnectAuthNoContent(t *testing.T) {
	cli, srv, _, _ := newServerAuthNoContent(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 TestConnectMultipleAuth(t *testing.T) {
	cli, srv, _, _ := newAuthServer(t, multipleAuth)
	defer srv.Close()
	if err := cli.Connect(); err != nil {
		t.Fatalf("got error: %v, want nil", err)
	}

	cli = NewClient(srv.URL, "digestUser", "digestPW")
	if err := cli.Connect(); err != nil {
		t.Fatalf("got nil, want error: %v", err)
	}

	cli = NewClient(srv.URL, "no", "no")
	if err := cli.Connect(); err == nil {
		t.Fatalf("got nil, want error: %v", err)
	}
}

func TestConnectMultiAuthII(t *testing.T) {
	cli, srv, _, _ := newAuthServer(t, func(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().Add("WWW-Authenticate", `FooAuth`)
				w.Header().Add("WWW-Authenticate", `BazAuth`)
				w.Header().Add("WWW-Authenticate", `BarAuth`)
				w.Header().Add("WWW-Authenticate", `Basic realm="x"`)
				w.WriteHeader(401)
			}
		}
	})
	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 TestReadNoAuth(t *testing.T) {
	cli, srv, _, _ := newAuthServer(t, noAuthHndl)
	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)
	}
}

func TestWriteToServerAcquireContentLength(t *testing.T) {
	cli, srv, _, _ := newAuthServerAcquireContentLength(t)
	defer srv.Close()

	if err := cli.Write("/newfile.txt", []byte("foo bar\n"), 0660); err != nil {
		t.Fatalf("got: %v, want nil", err)
	}
}

func TestWriteStreamToServerAcquireContentLength(t *testing.T) {
	cli, srv, _, _ := newAuthServerAcquireContentLength(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)
	}

	lf := make([]byte, 10*1024*1024)
	rand.Read(lf)
	if err := cli.WriteStream("/largefile.bin", bytes.NewBuffer(lf), 0660); err != nil {
		t.Fatalf("got: %v, want nil", err)
	}

	lf2, err := cli.Read("/largefile.bin")
	if err != nil {
		t.Fatalf("got: %v, want nil", err)
	}

	if !bytes.Equal(lf, lf2) {
		t.Fatalf("%s largefile.bin doesn't match", t.Name())
	}
}

func TestWriteStreamWithLength(t *testing.T) {
	cli, srv, fs, ctx := newServer(t)
	defer srv.Close()

	content := "foo bar\n"
	if err := cli.WriteStreamWithLength("/newfile.txt", strings.NewReader(content), int64(len(content)), 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.WriteStreamWithLength("/404/works.txt", strings.NewReader(content), int64(len(content)), 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 TestWriteStreamWithLengthFromPipe(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.WriteStreamWithLength("/newfile.txt", r, 8, 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)
	}
}

func TestWriteStreamWithLengthIncorrectContentLength(t *testing.T) {
	cli, srv, _, _ := newServer(t)
	defer srv.Close()

	content := "foo bar\n"
	if err := cli.WriteStreamWithLength("/newfile1.txt", strings.NewReader(content), int64(len(content)+10), 0660); err == nil {
		t.Fatalf("got: expected error on invalid content length")
	}

	longContent := "this is a very long content that exceeds the declared length"
	if err := cli.WriteStreamWithLength("/newfile2.txt", strings.NewReader(longContent), 10, 0660); err == nil {
		t.Fatalf("got: expected error on invalid content length")
	}
}


================================================
FILE: cmd/gowebdav/README.md
================================================
# 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
```

================================================
FILE: cmd/gowebdav/main.go
================================================
package main

import (
	"errors"
	"flag"
	"fmt"
	"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]")
	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>
	STAT <PATH>

	MKDIR <PATH>
	MKDIRALL <PATH>

	GET <PATH> [<FILE>]
	PUT <PATH> [<FILE>]

	MV <OLD> <NEW>
	CP <OLD> <NEW>

	DEL <PATH>
	`)
	flag.Parse()

	if *root == "" {
		fail("Set WebDAV ROOT")
	}

	if argsLength := len(flag.Args()); argsLength == 0 || argsLength > 2 {
		fail("Unsupported arguments")
	}

	if *password == "" {
		if u, p := d.ReadConfig(*root, *netrc); u != "" && p != "" {
			user = &u
			password = &p
		}
	}

	c := d.NewClient(*root, *user, *password)

	if e := c.Connect(); e != nil {
		panic(e)
	}

	cmd := getCmd(*method)

	if e := cmd(c, flag.Arg(0), flag.Arg(1)); e != nil {
		fail(e)
	}
}

func fail(err interface{}) {
	if err != nil {
		fmt.Println(err)
	}
	os.Exit(-1)
}

func getHome() string {
	u, e := user.Current()
	if e != nil {
		return os.Getenv("HOME")
	}

	if u != nil {
		return u.HomeDir
	}

	switch runtime.GOOS {
	case "windows":
		return ""
	default:
		return "~/"
	}
}

func getCmd(method string) func(c *d.Client, p0, p1 string) error {
	switch strings.ToUpper(method) {
	case "LS", "LIST", "PROPFIND":
		return cmdLs

	case "STAT":
		return cmdStat

	case "GET", "PULL", "READ":
		return cmdGet

	case "DELETE", "RM", "DEL":
		return cmdRm

	case "MKCOL", "MKDIR":
		return cmdMkdir

	case "MKCOLALL", "MKDIRALL", "MKDIRP":
		return cmdMkdirAll

	case "RENAME", "MV", "MOVE":
		return cmdMv

	case "COPY", "CP":
		return cmdCp

	case "PUT", "PUSH", "WRITE":
		return cmdPut

	default:
		return func(c *d.Client, p0, p1 string) (err error) {
			return errors.New("Unsupported method: " + method)
		}
	}
}

func cmdLs(c *d.Client, p0, _ string) (err error) {
	files, err := c.ReadDir(p0)
	if err == nil {
		fmt.Println(fmt.Sprintf("ReadDir: '%s' entries: %d ", p0, len(files)))
		for _, f := range files {
			fmt.Println(f)
		}
	}
	return
}

func cmdStat(c *d.Client, p0, _ string) (err error) {
	file, err := c.Stat(p0)
	if err == nil {
		fmt.Println(file)
	}
	return
}

func cmdGet(c *d.Client, p0, p1 string) (err error) {
	bytes, err := c.Read(p0)
	if err == nil {
		if p1 == "" {
			p1 = filepath.Join(".", p0)
		}
		err = writeFile(p1, bytes, 0644)
		if err == nil {
			fmt.Println(fmt.Sprintf("Written %d bytes to: %s", len(bytes), p1))
		}
	}
	return
}

func cmdRm(c *d.Client, p0, _ string) (err error) {
	if err = c.Remove(p0); err == nil {
		fmt.Println("Remove: " + p0)
	}
	return
}

func cmdMkdir(c *d.Client, p0, _ string) (err error) {
	if err = c.Mkdir(p0, 0755); err == nil {
		fmt.Println("Mkdir: " + p0)
	}
	return
}

func cmdMkdirAll(c *d.Client, p0, _ string) (err error) {
	if err = c.MkdirAll(p0, 0755); err == nil {
		fmt.Println("MkdirAll: " + p0)
	}
	return
}

func cmdMv(c *d.Client, p0, p1 string) (err error) {
	if err = c.Rename(p0, p1, true); err == nil {
		fmt.Println("Rename: " + p0 + " -> " + p1)
	}
	return
}

func cmdCp(c *d.Client, p0, p1 string) (err error) {
	if err = c.Copy(p0, p1, true); err == nil {
		fmt.Println("Copy: " + p0 + " -> " + p1)
	}
	return
}

func cmdPut(c *d.Client, p0, p1 string) (err error) {
	if p1 == "" {
		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
	}
	defer stream.Close()

	if err = c.WriteStream(p0, stream, 0644); err == nil {
		fmt.Println("Put: " + p1 + " -> " + p0)
	}
	return
}

func writeFile(path string, bytes []byte, mode os.FileMode) error {
	parent := filepath.Dir(path)
	if _, e := os.Stat(parent); os.IsNotExist(e) {
		if e := os.MkdirAll(parent, os.ModePerm); e != nil {
			return e
		}
	}

	f, err := os.Create(path)
	if err != nil {
		return err
	}
	defer f.Close()

	_, err = f.Write(bytes)
	return err
}

func getStream(pathOrString string) (io.ReadCloser, error) {

	fi, err := os.Stat(pathOrString)
	if err != nil {
		return nil, err
	}

	if fi.IsDir() {
		return nil, &os.PathError{
			Op:   "Open",
			Path: pathOrString,
			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,
	}
}


================================================
FILE: digestAuth.go
================================================
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
}

// NewDigestAuth creates a new instance of our Digest Authenticator
func NewDigestAuth(login, secret string, rs *http.Response) (Authenticator, error) {
	return &DigestAuth{user: login, pw: secret, digestParts: digestParts(rs)}, nil
}

// Authorize the current request
func (d *DigestAuth) Authorize(c *http.Client, rq *http.Request, path string) error {
	d.digestParts["uri"] = path
	d.digestParts["method"] = rq.Method
	d.digestParts["username"] = d.user
	d.digestParts["password"] = d.pw
	rq.Header.Set("Authorization", getDigestAuthorization(d.digestParts))
	return nil
}

// Verify checks for authentication issues and may trigger a re-authentication
func (d *DigestAuth) Verify(c *http.Client, rs *http.Response, path string) (redo bool, err error) {
	if rs.StatusCode == 401 {
		if isStaled(rs) {
			redo = true
			err = ErrAuthChanged
		} else {
			err = NewPathError("Authorize", path, rs.StatusCode)
		}
	}
	return
}

// Close cleans up all resources
func (d *DigestAuth) Close() error {
	return nil
}

// Clone creates a copy of itself
func (d *DigestAuth) Clone() Authenticator {
	parts := make(map[string]string, len(d.digestParts))
	for k, v := range d.digestParts {
		parts[k] = v
	}
	return &DigestAuth{user: d.user, pw: d.pw, digestParts: parts}
}

// String toString
func (d *DigestAuth) String() string {
	return fmt.Sprintf("DigestAuth login: %s", d.user)
}

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
}

func isStaled(rs *http.Response) bool {
	header := rs.Header.Get("Www-Authenticate")
	if len(header) > 0 {
		directives := strings.Split(header, ",")
		for i := range directives {
			name, value, _ := strings.Cut(strings.Trim(directives[i], " "), "=")
			if strings.EqualFold(name, "stale") {
				return strings.EqualFold(value, "true")
			}
		}
	}
	return false
}


================================================
FILE: digestAuth_test.go
================================================
package gowebdav

import (
	"errors"
	"net/http"
	"strings"
	"testing"
)

func TestNewDigestAuth(t *testing.T) {
	a := &DigestAuth{user: "user", pw: "password", digestParts: make(map[string]string, 0)}

	ex := "DigestAuth login: user"
	if a.String() != ex {
		t.Error("expected: " + ex + " got: " + a.String())
	}

	if a.Clone() == a {
		t.Error("expected a different instance")
	}

	if a.Close() != nil {
		t.Error("expected close without errors")
	}
}

func TestDigestAuthAuthorize(t *testing.T) {
	a := &DigestAuth{user: "user", pw: "password", digestParts: make(map[string]string, 0)}
	rq, _ := http.NewRequest("GET", "http://localhost/", nil)
	a.Authorize(nil, rq, "/")
	// TODO this is a very lazy test it cuts of cnonce
	ex := `Digest username="user", realm="", nonce="", uri="/", nc=1, cnonce="`
	if strings.Index(rq.Header.Get("Authorization"), ex) != 0 {
		t.Error("got wrong Authorization header: " + rq.Header.Get("Authorization"))
	}
}

func TestDigestAuthVerify(t *testing.T) {
	a := &DigestAuth{user: "user", pw: "password", digestParts: make(map[string]string, 0)}

	// Nominal test: 200 OK response
	rs := &http.Response{
		Status:     "200 OK",
		StatusCode: http.StatusOK,
	}

	redo, err := a.Verify(nil, rs, "/")

	if err != nil {
		t.Errorf("got error: %v, want nil", err)
	}

	if redo {
		t.Errorf("got redo: %t, want false", redo)
	}

	// Digest expiration test: 401 Unauthorized response with stale directive in WWW-Authenticate header
	rs = &http.Response{
		Status:     "401 Unauthorized",
		StatusCode: http.StatusUnauthorized,
		Header: http.Header{
			"Www-Authenticate": []string{"Digest realm=\"webdav\", nonce=\"YVvALpkdBgA=931bbf2b6fa9dda227361dba38a735f005fd9f97\", algorithm=MD5, qop=\"auth\", stale=true"},
		},
	}

	redo, err = a.Verify(nil, rs, "/")

	if !errors.Is(err, ErrAuthChanged) {
		t.Errorf("got error: %v, want ErrAuthChanged", err)
	}

	if !redo {
		t.Errorf("got redo: %t, want true", redo)
	}
}


================================================
FILE: doc.go
================================================
// Package gowebdav is a WebDAV client library with a command line tool
// included.
package gowebdav


================================================
FILE: errors.go
================================================
package gowebdav

import (
	"errors"
	"fmt"
	"os"
)

// ErrAuthChanged must be returned from the Verify method as an error
// to trigger a re-authentication / negotiation with a new authenticator.
var ErrAuthChanged = errors.New("authentication failed, change algorithm")

// ErrTooManyRedirects will be used as return error if a request exceeds 10 redirects.
var ErrTooManyRedirects = errors.New("stopped after 10 redirects")

// 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,
	}
}


================================================
FILE: file.go
================================================
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
}

// 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
}

// 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)
}


================================================
FILE: go.mod
================================================
module github.com/studio-b12/gowebdav

go 1.17


================================================
FILE: go_test.mod
================================================
module github.com/studio-b12/gowebdav

go 1.17

require golang.org/x/net v0.0.0-20221014081412-f15817d10f9b


================================================
FILE: go_test.sum
================================================
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=


================================================
FILE: netrc.go
================================================
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 "", ""
}


================================================
FILE: passportAuth.go
================================================
package gowebdav

import (
	"fmt"
	"io"
	"net/http"
	"net/url"
	"strings"
)

// PassportAuth structure holds our credentials
type PassportAuth struct {
	user            string
	pw              string
	cookies         []http.Cookie
	inhibitRedirect bool
}

// constructor for PassportAuth creates a new PassportAuth object and
// automatically authenticates against the given partnerURL
func NewPassportAuth(c *http.Client, user, pw, partnerURL string, header *http.Header) (Authenticator, error) {
	p := &PassportAuth{
		user:            user,
		pw:              pw,
		inhibitRedirect: true,
	}
	err := p.genCookies(c, partnerURL, header)
	return p, err
}

// Authorize the current request
func (p *PassportAuth) Authorize(c *http.Client, rq *http.Request, path string) error {
	// prevent redirects to detect subsequent authentication requests
	if p.inhibitRedirect {
		rq.Header.Set(XInhibitRedirect, "1")
	} else {
		p.inhibitRedirect = true
	}
	for _, cookie := range p.cookies {
		rq.AddCookie(&cookie)
	}
	return nil
}

// Verify verifies if the authentication is good
func (p *PassportAuth) Verify(c *http.Client, rs *http.Response, path string) (redo bool, err error) {
	switch rs.StatusCode {
	case 301, 302, 307, 308:
		redo = true
		if rs.Header.Get("Www-Authenticate") != "" {
			// re-authentication required as we are redirected to the login page
			err = p.genCookies(c, rs.Request.URL.String(), &rs.Header)
		} else {
			// just a redirect, follow it
			p.inhibitRedirect = false
		}
	case 401:
		err = NewPathError("Authorize", path, rs.StatusCode)
	}
	return
}

// Close cleans up all resources
func (p *PassportAuth) Close() error {
	return nil
}

// Clone creates a Copy of itself
func (p *PassportAuth) Clone() Authenticator {
	// create a copy to allow independent cookie updates
	clonedCookies := make([]http.Cookie, len(p.cookies))
	copy(clonedCookies, p.cookies)

	return &PassportAuth{
		user:            p.user,
		pw:              p.pw,
		cookies:         clonedCookies,
		inhibitRedirect: true,
	}
}

// String toString
func (p *PassportAuth) String() string {
	return fmt.Sprintf("PassportAuth login: %s", p.user)
}

func (p *PassportAuth) genCookies(c *http.Client, partnerUrl string, header *http.Header) error {
	// For more details refer to:
	// https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-pass/2c80637d-438c-4d4b-adc5-903170a779f3
	// Skipping step 1 and 2 as we already have the partner server challenge

	baseAuthenticationServer := header.Get("Location")
	baseAuthenticationServerURL, err := url.Parse(baseAuthenticationServer)
	if err != nil {
		return err
	}

	// Skipping step 3 and 4 as we already know that we need and have the user's credentials
	// Step 5 (Sign-in request)
	authenticationServerUrl := url.URL{
		Scheme: baseAuthenticationServerURL.Scheme,
		Host:   baseAuthenticationServerURL.Host,
		Path:   "/login2.srf",
	}

	partnerServerChallenge := strings.Split(header.Get("Www-Authenticate"), " ")[1]

	req := http.Request{
		Method: "GET",
		URL:    &authenticationServerUrl,
		Header: http.Header{
			"Authorization": []string{"Passport1.4 sign-in=" + url.QueryEscape(p.user) + ",pwd=" + url.QueryEscape(p.pw) + ",OrgVerb=GET,OrgUrl=" + partnerUrl + "," + partnerServerChallenge},
		},
	}

	rs, err := c.Do(&req)
	if err != nil {
		return err
	}
	io.Copy(io.Discard, rs.Body)
	rs.Body.Close()
	if rs.StatusCode != 200 {
		return NewPathError("Authorize", "/", rs.StatusCode)
	}

	// Step 6 (Token Response from Authentication Server)
	tokenResponseHeader := rs.Header.Get("Authentication-Info")
	if tokenResponseHeader == "" {
		return NewPathError("Authorize", "/", 401)
	}
	tokenResponseHeaderList := strings.Split(tokenResponseHeader, ",")
	token := ""
	for _, tokenResponseHeader := range tokenResponseHeaderList {
		if strings.HasPrefix(tokenResponseHeader, "from-PP='") {
			token = tokenResponseHeader
			break
		}
	}
	if token == "" {
		return NewPathError("Authorize", "/", 401)
	}

	// Step 7 (First Authentication Request to Partner Server)
	origUrl, err := url.Parse(partnerUrl)
	if err != nil {
		return err
	}
	req = http.Request{
		Method: "GET",
		URL:    origUrl,
		Header: http.Header{
			"Authorization": []string{"Passport1.4 " + token},
		},
	}

	rs, err = c.Do(&req)
	if err != nil {
		return err
	}
	io.Copy(io.Discard, rs.Body)
	rs.Body.Close()
	if rs.StatusCode != 200 && rs.StatusCode != 302 {
		return NewPathError("Authorize", "/", rs.StatusCode)
	}

	// Step 8 (Set Token Message from Partner Server)
	cookies := rs.Header.Values("Set-Cookie")
	p.cookies = make([]http.Cookie, len(cookies))
	for i, cookie := range cookies {
		cookieParts := strings.Split(cookie, ";")
		cookieName := strings.Split(cookieParts[0], "=")[0]
		cookieValue := strings.Split(cookieParts[0], "=")[1]

		p.cookies[i] = http.Cookie{
			Name:  cookieName,
			Value: cookieValue,
		}
	}

	return nil
}


================================================
FILE: passportAuth_test.go
================================================
package gowebdav

import (
	"bytes"
	"net/http"
	"net/url"
	"regexp"
	"testing"
)

// testing the creation is enough as it handles the authorization during init
func TestNewPassportAuth(t *testing.T) {
	user := "user"
	pass := "password"
	p1 := "some,comma,separated,values"
	token := "from-PP='token'"

	authHandler := func(h http.Handler) http.HandlerFunc {
		return func(w http.ResponseWriter, r *http.Request) {
			reg, err := regexp.Compile("Passport1\\.4 sign-in=" + url.QueryEscape(user) + ",pwd=" + url.QueryEscape(pass) + ",OrgVerb=GET,OrgUrl=.*," + p1)
			if err != nil {
				t.Error(err)
			}
			if reg.MatchString(r.Header.Get("Authorization")) {
				w.Header().Set("Authentication-Info", token)
				w.WriteHeader(200)
				return
			}
		}
	}
	authsrv, _, _ := newAuthSrv(t, authHandler)
	defer authsrv.Close()

	dataHandler := func(h http.Handler) http.HandlerFunc {
		return func(w http.ResponseWriter, r *http.Request) {
			reg, err := regexp.Compile("Passport1\\.4 " + token)
			if err != nil {
				t.Error(err)
			}
			if reg.MatchString(r.Header.Get("Authorization")) {
				w.Header().Set("Set-Cookie", "Pass=port")
				h.ServeHTTP(w, r)
				return
			}
			for _, c := range r.Cookies() {
				if c.Name == "Pass" && c.Value == "port" {
					h.ServeHTTP(w, r)
					return
				}
			}
			w.Header().Set("Www-Authenticate", "Passport1.4 "+p1)
			http.Redirect(w, r, authsrv.URL+"/", 302)
		}
	}
	srv, _, _ := newAuthSrv(t, dataHandler)
	defer srv.Close()

	cli := NewClient(srv.URL, user, pass)
	data, err := cli.Read("/hello.txt")
	if err != nil {
		t.Errorf("got error=%v; want nil", err)
	}
	if !bytes.Equal(data, []byte("hello gowebdav\n")) {
		t.Logf("got data=%v; want=hello gowebdav", data)
	}
}


================================================
FILE: requests.go
================================================
package gowebdav

import (
	"io"
	"log"
	"net/http"
	"path"
	"strings"
)

func (c *Client) req(method, path string, body io.Reader, intercept func(*http.Request)) (rs *http.Response, err error) {
	var redo bool
	var r *http.Request
	var uri = PathEscape(Join(c.root, path))
	auth, body := c.auth.NewAuthenticator(body)
	defer auth.Close()

	for { // TODO auth.continue() strategy(true|n times|until)?
		if r, err = http.NewRequest(method, uri, body); err != nil {
			return
		}

		for k, vals := range c.headers {
			for _, v := range vals {
				r.Header.Add(k, v)
			}
		}

		if err = auth.Authorize(c.c, r, path); err != nil {
			return
		}

		if intercept != nil {
			intercept(r)
		}

		if c.interceptor != nil {
			c.interceptor(method, r)
		}

		if rs, err = c.c.Do(r); err != nil {
			return
		}

		if redo, err = auth.Verify(c.c, rs, path); err != nil {
			rs.Body.Close()
			return nil, err
		}
		if redo {
			rs.Body.Close()
			if body, err = r.GetBody(); err != nil {
				return nil, err
			}
			continue
		}
		break
	}

	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) {
	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 NewPathError("PROPFIND", path, rs.StatusCode)
	}

	return parseXML(rs.Body, resp, parse)
}

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", PathEscape(Join(c.root, newpath)))
		if overwrite {
			rq.Header.Add("Overwrite", "T")
		} else {
			rq.Header.Add("Overwrite", "F")
		}
	})
	if err != nil {
		return
	}
	status = rs.StatusCode
	r = rs.Body
	return
}

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:
		return nil

	case 207:
		// TODO handle multistat errors, worst case ...
		log.Printf("TODO handle %s - %s multistatus result %s\n", 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, contentLength int64) (status int, err error) {
	rs, err := c.req("PUT", path, stream, func(r *http.Request) {
		r.ContentLength = contentLength
	})
	if err != nil {
		return
	}
	defer rs.Body.Close()

	status = rs.StatusCode
	return
}

func (c *Client) createParentCollection(itemPath string) (err error) {
	parentPath := path.Dir(itemPath)
	if parentPath == "." || parentPath == "/" {
		return nil
	}

	return c.MkdirAll(parentPath, 0755)
}


================================================
FILE: utils.go
================================================
package gowebdav

import (
	"bytes"
	"encoding/xml"
	"io"
	"net/url"
	"strconv"
	"strings"
	"time"
)

// PathEscape escapes all segments 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 !strings.HasPrefix(s, "/") {
		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
}

// 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()
}


================================================
FILE: utils_test.go
================================================
package gowebdav

import (
	"fmt"
	"net/url"
	"testing"
)

func TestJoin(t *testing.T) {
	eq(t, "/", "", "")
	eq(t, "/", "/", "/")
	eq(t, "/foo", "", "/foo")
	eq(t, "foo/foo", "foo/", "/foo")
	eq(t, "foo/foo", "foo/", "foo")
}

func eq(t *testing.T, expected string, s0 string, s1 string) {
	s := Join(s0, s1)
	if s != expected {
		t.Error("For", "'"+s0+"','"+s1+"'", "expeted", "'"+expected+"'", "got", "'"+s+"'")
	}
}

func ExamplePathEscape() {
	fmt.Println(PathEscape(""))
	fmt.Println(PathEscape("/"))
	fmt.Println(PathEscape("/web"))
	fmt.Println(PathEscape("/web/"))
	fmt.Println(PathEscape("/w e b/d a v/s%u&c#k:s/"))

	// Output:
	//
	// /
	// /web
	// /web/
	// /w%20e%20b/d%20a%20v/s%25u&c%23k:s/
}

func TestEscapeURL(t *testing.T) {
	ex := "https://foo.com/w%20e%20b/d%20a%20v/s%25u&c%23k:s/"
	u, _ := url.Parse("https://foo.com" + PathEscape("/w e b/d a v/s%u&c#k:s/"))
	if ex != u.String() {
		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)
	}
}
Download .txt
gitextract_nlfpais6/

├── .github/
│   ├── ISSUE_TEMPLATE/
│   │   └── bug_report.md
│   └── workflows/
│       ├── artifacts.yml
│       └── tests.yml
├── .gitignore
├── LICENSE
├── Makefile
├── README.md
├── auth.go
├── auth_test.go
├── basicAuth.go
├── basicAuth_test.go
├── client.go
├── client_test.go
├── cmd/
│   └── gowebdav/
│       ├── README.md
│       └── main.go
├── digestAuth.go
├── digestAuth_test.go
├── doc.go
├── errors.go
├── file.go
├── go.mod
├── go_test.mod
├── go_test.sum
├── netrc.go
├── passportAuth.go
├── passportAuth_test.go
├── requests.go
├── utils.go
└── utils_test.go
Download .txt
SYMBOL INDEX (197 symbols across 17 files)

FILE: auth.go
  type AuthFactory (line 13) | type AuthFactory
  type Authorizer (line 17) | type Authorizer interface
  type Authenticator (line 56) | type Authenticator interface
  type authfactory (line 68) | type authfactory struct
  type authorizer (line 74) | type authorizer struct
    method NewAuthenticator (line 153) | func (a *authorizer) NewAuthenticator(body io.Reader) (Authenticator, ...
    method AddAuthenticator (line 179) | func (a *authorizer) AddAuthenticator(key string, fn AuthFactory) {
    method factory (line 190) | func (a *authorizer) factory(c *http.Client, rs *http.Response, path s...
    method setDefaultAuthenticator (line 225) | func (a *authorizer) setDefaultAuthenticator(auth Authenticator) {
  type preemptiveAuthorizer (line 81) | type preemptiveAuthorizer struct
    method NewAuthenticator (line 402) | func (b *preemptiveAuthorizer) NewAuthenticator(body io.Reader) (Authe...
    method AddAuthenticator (line 407) | func (b *preemptiveAuthorizer) AddAuthenticator(key string, fn AuthFac...
  type authShim (line 86) | type authShim struct
    method Authorize (line 233) | func (s *authShim) Authorize(c *http.Client, rq *http.Request, path st...
    method Verify (line 254) | func (s *authShim) Verify(c *http.Client, rs *http.Response, path stri...
    method Close (line 269) | func (s *authShim) Close() error {
    method Clone (line 282) | func (s *authShim) Clone() Authenticator {
    method String (line 287) | func (s *authShim) String() string {
  type negoAuth (line 93) | type negoAuth struct
    method Authorize (line 292) | func (n *negoAuth) Authorize(c *http.Client, rq *http.Request, path st...
    method Verify (line 300) | func (n *negoAuth) Verify(c *http.Client, rs *http.Response, path stri...
    method Close (line 324) | func (n *negoAuth) Close() error {
    method Clone (line 333) | func (n *negoAuth) Clone() Authenticator {
    method String (line 341) | func (n *negoAuth) String() string {
  type nullAuth (line 99) | type nullAuth struct
    method Authorize (line 375) | func (n *nullAuth) Authorize(c *http.Client, rq *http.Request, path st...
    method Verify (line 381) | func (n *nullAuth) Verify(c *http.Client, rs *http.Response, path stri...
    method Close (line 386) | func (n *nullAuth) Close() error {
    method Clone (line 391) | func (n *nullAuth) Clone() Authenticator {
    method String (line 397) | func (n *nullAuth) String() string {
  type noAuth (line 102) | type noAuth struct
    method Authorize (line 346) | func (n *noAuth) Authorize(c *http.Client, rq *http.Request, path stri...
    method Verify (line 351) | func (n *noAuth) Verify(c *http.Client, rs *http.Response, path string...
    method Close (line 359) | func (n *noAuth) Close() error {
    method Clone (line 364) | func (n *noAuth) Clone() Authenticator {
    method String (line 370) | func (n *noAuth) String() string {
  function NewAutoAuth (line 109) | func NewAutoAuth(login string, secret string) Authorizer {
  function NewEmptyAuth (line 132) | func NewEmptyAuth() Authorizer {
  function NewPreemptiveAuth (line 148) | func NewPreemptiveAuth(auth Authenticator) Authorizer {

FILE: auth_test.go
  function TestEmptyAuth (line 10) | func TestEmptyAuth(t *testing.T) {
  function TestRedirectAuthWIP (line 20) | func TestRedirectAuthWIP(t *testing.T) {

FILE: basicAuth.go
  type BasicAuth (line 9) | type BasicAuth struct
    method Authorize (line 15) | func (b *BasicAuth) Authorize(c *http.Client, rq *http.Request, path s...
    method Verify (line 21) | func (b *BasicAuth) Verify(c *http.Client, rs *http.Response, path str...
    method Close (line 29) | func (b *BasicAuth) Close() error {
    method Clone (line 34) | func (b *BasicAuth) Clone() Authenticator {
    method String (line 40) | func (b *BasicAuth) String() string {

FILE: basicAuth_test.go
  function TestNewBasicAuth (line 8) | func TestNewBasicAuth(t *testing.T) {
  function TestBasicAuthAuthorize (line 25) | func TestBasicAuthAuthorize(t *testing.T) {
  function TestPreemtiveBasicAuth (line 34) | func TestPreemtiveBasicAuth(t *testing.T) {

FILE: client.go
  constant XInhibitRedirect (line 16) | XInhibitRedirect = "X-Gowebdav-Inhibit-Redirect"
  type Client (line 19) | type Client struct
    method SetHeader (line 49) | func (c *Client) SetHeader(key, value string) {
    method SetInterceptor (line 54) | func (c *Client) SetInterceptor(interceptor func(method string, rq *ht...
    method SetTimeout (line 59) | func (c *Client) SetTimeout(timeout time.Duration) {
    method SetTransport (line 64) | func (c *Client) SetTransport(transport http.RoundTripper) {
    method SetJar (line 69) | func (c *Client) SetJar(jar http.CookieJar) {
    method Connect (line 74) | func (c *Client) Connect() error {
    method ReadDir (line 117) | func (c *Client) ReadDir(path string) ([]os.FileInfo, error) {
    method Stat (line 184) | func (c *Client) Stat(path string) (os.FileInfo, error) {
    method Remove (line 236) | func (c *Client) Remove(path string) error {
    method RemoveAll (line 241) | func (c *Client) RemoveAll(path string) error {
    method Mkdir (line 259) | func (c *Client) Mkdir(path string, _ os.FileMode) (err error) {
    method MkdirAll (line 273) | func (c *Client) MkdirAll(path string, _ os.FileMode) (err error) {
    method Rename (line 305) | func (c *Client) Rename(oldpath, newpath string, overwrite bool) error {
    method Copy (line 310) | func (c *Client) Copy(oldpath, newpath string, overwrite bool) error {
    method Read (line 315) | func (c *Client) Read(path string) ([]byte, error) {
    method ReadStream (line 333) | func (c *Client) ReadStream(path string) (io.ReadCloser, error) {
    method ReadStreamRange (line 355) | func (c *Client) ReadStreamRange(path string, offset, length int64) (i...
    method Write (line 389) | func (c *Client) Write(path string, data []byte, _ os.FileMode) (err e...
    method WriteStream (line 419) | func (c *Client) WriteStream(path string, stream io.Reader, _ os.FileM...
    method WriteStreamWithLength (line 463) | func (c *Client) WriteStreamWithLength(path string, stream io.Reader, ...
  function NewClient (line 28) | func NewClient(uri, user, pw string) *Client {
  function NewAuthClient (line 33) | func NewAuthClient(uri string, auth Authorizer) *Client {
  type props (line 92) | type props struct
  type response (line 102) | type response struct
  function getProps (line 107) | func getProps(r *response, status string) *props {

FILE: client_test.go
  function noAuthHndl (line 21) | func noAuthHndl(h http.Handler) http.HandlerFunc {
  function basicAuth (line 27) | func basicAuth(h http.Handler) http.HandlerFunc {
  function basicAuthNoContent (line 43) | func basicAuthNoContent(h http.Handler) http.HandlerFunc {
  function basicAuthWithPostHandlerFunc (line 60) | func basicAuthWithPostHandlerFunc(h http.Handler, postHandlerFunc http.H...
  function multipleAuth (line 79) | func multipleAuth(h http.Handler) http.HandlerFunc {
  function fillFs (line 127) | func fillFs(t *testing.T, fs webdav.FileSystem) context.Context {
  function newServer (line 148) | func newServer(t *testing.T) (*Client, *httptest.Server, webdav.FileSyst...
  function newServerAuthNoContent (line 152) | func newServerAuthNoContent(t *testing.T) (*Client, *httptest.Server, we...
  function newAuthServer (line 156) | func newAuthServer(t *testing.T, auth func(h http.Handler) http.HandlerF...
  function newAuthSrv (line 162) | func newAuthSrv(t *testing.T, auth func(h http.Handler) http.HandlerFunc...
  function newAuthServerAcquireContentLength (line 174) | func newAuthServerAcquireContentLength(t *testing.T) (*Client, *httptest...
  function newAuthSrvAcquireContentLength (line 180) | func newAuthSrvAcquireContentLength(t *testing.T, authWithPostHandlerFun...
  function TestConnect (line 206) | func TestConnect(t *testing.T) {
  function TestConnectAuthNoContent (line 219) | func TestConnectAuthNoContent(t *testing.T) {
  function TestConnectMultipleAuth (line 232) | func TestConnectMultipleAuth(t *testing.T) {
  function TestConnectMultiAuthII (line 250) | func TestConnectMultiAuthII(t *testing.T) {
  function TestReadDirConcurrent (line 280) | func TestReadDirConcurrent(t *testing.T) {
  function TestRead (line 317) | func TestRead(t *testing.T) {
  function TestReadNoAuth (line 335) | func TestReadNoAuth(t *testing.T) {
  function TestReadStream (line 353) | func TestReadStream(t *testing.T) {
  function TestReadStreamRange (line 373) | func TestReadStreamRange(t *testing.T) {
  function TestReadStreamRangeUnkownLength (line 393) | func TestReadStreamRangeUnkownLength(t *testing.T) {
  function TestStat (line 413) | func TestStat(t *testing.T) {
  function TestMkdir (line 434) | func TestMkdir(t *testing.T) {
  function TestMkdirAll (line 461) | func TestMkdirAll(t *testing.T) {
  function TestCopy (line 475) | func TestCopy(t *testing.T) {
  function TestRename (line 513) | func TestRename(t *testing.T) {
  function TestRemove (line 550) | func TestRemove(t *testing.T) {
  function TestRemoveAll (line 567) | func TestRemoveAll(t *testing.T) {
  function TestWrite (line 588) | func TestWrite(t *testing.T) {
  function TestWriteStream (line 609) | func TestWriteStream(t *testing.T) {
  function TestWriteStreamFromPipe (line 634) | func TestWriteStreamFromPipe(t *testing.T) {
  function TestWriteToServerAcquireContentLength (line 662) | func TestWriteToServerAcquireContentLength(t *testing.T) {
  function TestWriteStreamToServerAcquireContentLength (line 671) | func TestWriteStreamToServerAcquireContentLength(t *testing.T) {
  function TestWriteStreamWithLength (line 695) | func TestWriteStreamWithLength(t *testing.T) {
  function TestWriteStreamWithLengthFromPipe (line 721) | func TestWriteStreamWithLengthFromPipe(t *testing.T) {
  function TestWriteStreamWithLengthIncorrectContentLength (line 749) | func TestWriteStreamWithLengthIncorrectContentLength(t *testing.T) {

FILE: cmd/gowebdav/main.go
  function main (line 19) | func main() {
  function fail (line 69) | func fail(err interface{}) {
  function getHome (line 76) | func getHome() string {
  function getCmd (line 94) | func getCmd(method string) func(c *d.Client, p0, p1 string) error {
  function cmdLs (line 130) | func cmdLs(c *d.Client, p0, _ string) (err error) {
  function cmdStat (line 141) | func cmdStat(c *d.Client, p0, _ string) (err error) {
  function cmdGet (line 149) | func cmdGet(c *d.Client, p0, p1 string) (err error) {
  function cmdRm (line 163) | func cmdRm(c *d.Client, p0, _ string) (err error) {
  function cmdMkdir (line 170) | func cmdMkdir(c *d.Client, p0, _ string) (err error) {
  function cmdMkdirAll (line 177) | func cmdMkdirAll(c *d.Client, p0, _ string) (err error) {
  function cmdMv (line 184) | func cmdMv(c *d.Client, p0, p1 string) (err error) {
  function cmdCp (line 191) | func cmdCp(c *d.Client, p0, p1 string) (err error) {
  function cmdPut (line 198) | func cmdPut(c *d.Client, p0, p1 string) (err error) {
  function writeFile (line 224) | func writeFile(path string, bytes []byte, mode os.FileMode) error {
  function getStream (line 242) | func getStream(pathOrString string) (io.ReadCloser, error) {

FILE: digestAuth.go
  type DigestAuth (line 14) | type DigestAuth struct
    method Authorize (line 26) | func (d *DigestAuth) Authorize(c *http.Client, rq *http.Request, path ...
    method Verify (line 36) | func (d *DigestAuth) Verify(c *http.Client, rs *http.Response, path st...
    method Close (line 49) | func (d *DigestAuth) Close() error {
    method Clone (line 54) | func (d *DigestAuth) Clone() Authenticator {
    method String (line 63) | func (d *DigestAuth) String() string {
  function NewDigestAuth (line 21) | func NewDigestAuth(login, secret string, rs *http.Response) (Authenticat...
  function digestParts (line 67) | func digestParts(resp *http.Response) map[string]string {
  function getMD5 (line 86) | func getMD5(text string) string {
  function getCnonce (line 92) | func getCnonce() string {
  function getDigestAuthorization (line 98) | func getDigestAuthorization(digestParts map[string]string) string {
  function isStaled (line 171) | func isStaled(rs *http.Response) bool {

FILE: digestAuth_test.go
  function TestNewDigestAuth (line 10) | func TestNewDigestAuth(t *testing.T) {
  function TestDigestAuthAuthorize (line 27) | func TestDigestAuthAuthorize(t *testing.T) {
  function TestDigestAuthVerify (line 38) | func TestDigestAuthVerify(t *testing.T) {

FILE: errors.go
  type StatusError (line 18) | type StatusError struct
    method Error (line 22) | func (se StatusError) Error() string {
  function IsErrCode (line 29) | func IsErrCode(err error, code int) bool {
  function IsErrNotFound (line 39) | func IsErrNotFound(err error) bool {
  function NewPathError (line 43) | func NewPathError(op string, path string, statusCode int) error {
  function NewPathErrorErr (line 51) | func NewPathErrorErr(op string, path string, err error) error {

FILE: file.go
  type File (line 10) | type File struct
    method Path (line 21) | func (f File) Path() string {
    method Name (line 26) | func (f File) Name() string {
    method ContentType (line 31) | func (f File) ContentType() string {
    method Size (line 36) | func (f File) Size() int64 {
    method Mode (line 41) | func (f File) Mode() os.FileMode {
    method ModTime (line 51) | func (f File) ModTime() time.Time {
    method ETag (line 56) | func (f File) ETag() string {
    method IsDir (line 61) | func (f File) IsDir() bool {
    method Sys (line 66) | func (f File) Sys() interface{} {
    method String (line 71) | func (f File) String() string {

FILE: netrc.go
  function parseLine (line 12) | func parseLine(s string) (login, pass string) {
  function ReadConfig (line 27) | func ReadConfig(uri, netrc string) (string, string) {

FILE: passportAuth.go
  type PassportAuth (line 12) | type PassportAuth struct
    method Authorize (line 32) | func (p *PassportAuth) Authorize(c *http.Client, rq *http.Request, pat...
    method Verify (line 46) | func (p *PassportAuth) Verify(c *http.Client, rs *http.Response, path ...
    method Close (line 64) | func (p *PassportAuth) Close() error {
    method Clone (line 69) | func (p *PassportAuth) Clone() Authenticator {
    method String (line 83) | func (p *PassportAuth) String() string {
    method genCookies (line 87) | func (p *PassportAuth) genCookies(c *http.Client, partnerUrl string, h...
  function NewPassportAuth (line 21) | func NewPassportAuth(c *http.Client, user, pw, partnerURL string, header...

FILE: passportAuth_test.go
  function TestNewPassportAuth (line 12) | func TestNewPassportAuth(t *testing.T) {

FILE: requests.go
  method req (line 11) | func (c *Client) req(method, path string, body io.Reader, intercept func...
  method mkcol (line 62) | func (c *Client) mkcol(path string) (status int, err error) {
  method options (line 77) | func (c *Client) options(path string) (*http.Response, error) {
  method propfind (line 83) | func (c *Client) propfind(path string, self bool, body string, resp inte...
  method doCopyMove (line 108) | func (c *Client) doCopyMove(
  method copymove (line 134) | func (c *Client) copymove(method string, oldpath string, newpath string,...
  method put (line 163) | func (c *Client) put(path string, stream io.Reader, contentLength int64)...
  method createParentCollection (line 176) | func (c *Client) createParentCollection(itemPath string) (err error) {

FILE: utils.go
  function PathEscape (line 14) | func PathEscape(path string) string {
  function FixSlash (line 23) | func FixSlash(s string) string {
  function FixSlashes (line 31) | func FixSlashes(s string) string {
  function Join (line 40) | func Join(path0 string, path1 string) string {
  function String (line 45) | func String(r io.Reader) string {
  function parseUint (line 52) | func parseUint(s *string) uint {
  function parseInt64 (line 59) | func parseInt64(s *string) int64 {
  function parseModified (line 66) | func parseModified(s *string) time.Time {
  function parseXML (line 73) | func parseXML(data io.Reader, resp interface{}, parse func(resp interfac...
  type limitedReadCloser (line 91) | type limitedReadCloser struct
    method Read (line 96) | func (l *limitedReadCloser) Read(buf []byte) (int, error) {
    method Close (line 111) | func (l *limitedReadCloser) Close() error {

FILE: utils_test.go
  function TestJoin (line 9) | func TestJoin(t *testing.T) {
  function eq (line 17) | func eq(t *testing.T, expected string, s0 string, s1 string) {
  function ExamplePathEscape (line 24) | func ExamplePathEscape() {
  function TestEscapeURL (line 39) | func TestEscapeURL(t *testing.T) {
  function TestFixSlashes (line 47) | func TestFixSlashes(t *testing.T) {
Condensed preview — 29 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (129K chars).
[
  {
    "path": ".github/ISSUE_TEMPLATE/bug_report.md",
    "chars": 432,
    "preview": "---\nname: Bug report\nabout: Create a report to help us improve\n\n---\n\nHello Collaborators,\n\n**Describe the bug**\nA short "
  },
  {
    "path": ".github/workflows/artifacts.yml",
    "chars": 1284,
    "preview": "name: Build Artifacts\non:\n  workflow_dispatch:\n  push:\n    branches:\n      - master\n    paths-ignore:\n      - \"**.md\"\n\nj"
  },
  {
    "path": ".github/workflows/tests.yml",
    "chars": 680,
    "preview": "name: Unit Tests\non:\n  workflow_dispatch:\n  push:\n    branches:\n      - \"*\"\n    paths-ignore:\n      - \"**.md\"\n  pull_req"
  },
  {
    "path": ".gitignore",
    "chars": 254,
    "preview": "# Folders to ignore\n/src\n/bin\n/pkg\n/gowebdav\n/.idea\n\n# Binaries for programs and plugins\n*.exe\n*.exe~\n*.dll\n*.so\n*.dylib"
  },
  {
    "path": "LICENSE",
    "chars": 1501,
    "preview": "Copyright (c) 2014, Studio B12 GmbH\nAll rights reserved.\n\nRedistribution and use in source and binary forms, with or wit"
  },
  {
    "path": "Makefile",
    "chars": 1003,
    "preview": "BIN := gowebdav\nSRC := $(wildcard *.go) cmd/gowebdav/main.go\n\nall: test cmd\n\ncmd: ${BIN}\n\n${BIN}: ${SRC}\n\tgo build -o $@"
  },
  {
    "path": "README.md",
    "chars": 34049,
    "preview": "# GoWebDAV\n\n[![Unit Tests Status](https://github.com/studio-b12/gowebdav/actions/workflows/tests.yml/badge.svg)](https:/"
  },
  {
    "path": "auth.go",
    "chars": 12348,
    "preview": "package gowebdav\n\nimport (\n\t\"bytes\"\n\t\"errors\"\n\t\"io\"\n\t\"net/http\"\n\t\"strings\"\n\t\"sync\"\n)\n\n// AuthFactory prototype function "
  },
  {
    "path": "auth_test.go",
    "chars": 1527,
    "preview": "package gowebdav\n\nimport (\n\t\"bytes\"\n\t\"net/http\"\n\t\"strings\"\n\t\"testing\"\n)\n\nfunc TestEmptyAuth(t *testing.T) {\n\tauth := New"
  },
  {
    "path": "basicAuth.go",
    "chars": 869,
    "preview": "package gowebdav\n\nimport (\n\t\"fmt\"\n\t\"net/http\"\n)\n\n// BasicAuth structure holds our credentials\ntype BasicAuth struct {\n\tu"
  },
  {
    "path": "basicAuth_test.go",
    "chars": 1169,
    "preview": "package gowebdav\n\nimport (\n\t\"net/http\"\n\t\"testing\"\n)\n\nfunc TestNewBasicAuth(t *testing.T) {\n\ta := &BasicAuth{user: \"user\""
  },
  {
    "path": "client.go",
    "chars": 11096,
    "preview": "package gowebdav\n\nimport (\n\t\"bytes\"\n\t\"encoding/xml\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"os\"\n\tpathpkg \"path\"\n\t\"strings\""
  },
  {
    "path": "client_test.go",
    "chars": 19850,
    "preview": "package gowebdav\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"crypto/rand\"\n\t\"errors\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\""
  },
  {
    "path": "cmd/gowebdav/README.md",
    "chars": 2690,
    "preview": "# Description\nCommand line tool for [gowebdav](https://github.com/studio-b12/gowebdav) library.\n\n# Prerequisites\n## Soft"
  },
  {
    "path": "cmd/gowebdav/main.go",
    "chars": 4776,
    "preview": "package main\n\nimport (\n\t\"errors\"\n\t\"flag\"\n\t\"fmt\"\n\t\"io\"\n\t\"io/fs\"\n\t\"os\"\n\t\"os/user\"\n\t\"path\"\n\t\"path/filepath\"\n\t\"runtime\"\n\t\"st"
  },
  {
    "path": "digestAuth.go",
    "chars": 4421,
    "preview": "package gowebdav\n\nimport (\n\t\"crypto/md5\"\n\t\"crypto/rand\"\n\t\"encoding/hex\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"strings\"\n)\n\n// Digest"
  },
  {
    "path": "digestAuth_test.go",
    "chars": 1946,
    "preview": "package gowebdav\n\nimport (\n\t\"errors\"\n\t\"net/http\"\n\t\"strings\"\n\t\"testing\"\n)\n\nfunc TestNewDigestAuth(t *testing.T) {\n\ta := &"
  },
  {
    "path": "doc.go",
    "chars": 102,
    "preview": "// Package gowebdav is a WebDAV client library with a command line tool\n// included.\npackage gowebdav\n"
  },
  {
    "path": "errors.go",
    "chars": 1325,
    "preview": "package gowebdav\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\t\"os\"\n)\n\n// ErrAuthChanged must be returned from the Verify method as an err"
  },
  {
    "path": "file.go",
    "chars": 1428,
    "preview": "package gowebdav\n\nimport (\n\t\"fmt\"\n\t\"os\"\n\t\"time\"\n)\n\n// File is our structure for a given file\ntype File struct {\n\tpath   "
  },
  {
    "path": "go.mod",
    "chars": 47,
    "preview": "module github.com/studio-b12/gowebdav\n\ngo 1.17\n"
  },
  {
    "path": "go_test.mod",
    "chars": 108,
    "preview": "module github.com/studio-b12/gowebdav\n\ngo 1.17\n\nrequire golang.org/x/net v0.0.0-20221014081412-f15817d10f9b\n"
  },
  {
    "path": "go_test.sum",
    "chars": 718,
    "preview": "golang.org/x/net v0.0.0-20221014081412-f15817d10f9b h1:tvrvnPFcdzp294diPnrdZZZ8XUt2Tyj7svb7X52iDuU=\ngolang.org/x/net v0."
  },
  {
    "path": "netrc.go",
    "chars": 904,
    "preview": "package gowebdav\n\nimport (\n\t\"bufio\"\n\t\"fmt\"\n\t\"net/url\"\n\t\"os\"\n\t\"regexp\"\n\t\"strings\"\n)\n\nfunc parseLine(s string) (login, pas"
  },
  {
    "path": "passportAuth.go",
    "chars": 4882,
    "preview": "package gowebdav\n\nimport (\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"strings\"\n)\n\n// PassportAuth structure holds our credent"
  },
  {
    "path": "passportAuth_test.go",
    "chars": 1716,
    "preview": "package gowebdav\n\nimport (\n\t\"bytes\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"regexp\"\n\t\"testing\"\n)\n\n// testing the creation is enough as "
  },
  {
    "path": "requests.go",
    "chars": 3776,
    "preview": "package gowebdav\n\nimport (\n\t\"io\"\n\t\"log\"\n\t\"net/http\"\n\t\"path\"\n\t\"strings\"\n)\n\nfunc (c *Client) req(method, path string, body"
  },
  {
    "path": "utils.go",
    "chars": 2242,
    "preview": "package gowebdav\n\nimport (\n\t\"bytes\"\n\t\"encoding/xml\"\n\t\"io\"\n\t\"net/url\"\n\t\"strconv\"\n\t\"strings\"\n\t\"time\"\n)\n\n// PathEscape esca"
  },
  {
    "path": "utils_test.go",
    "chars": 1457,
    "preview": "package gowebdav\n\nimport (\n\t\"fmt\"\n\t\"net/url\"\n\t\"testing\"\n)\n\nfunc TestJoin(t *testing.T) {\n\teq(t, \"/\", \"\", \"\")\n\teq(t, \"/\","
  }
]

About this extraction

This page contains the full source code of the studio-b12/gowebdav GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 29 files (115.8 KB), approximately 35.2k tokens, and a symbol index with 197 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!