23 Commits

Author SHA1 Message Date
Ringo Hoffmann
0d8627db50 Re-add setting requestBuf after seeking 2022-01-28 17:24:17 +01:00
Ringo Hoffmann
c42caf78a2 update doucmentaiton comment 2022-01-28 17:19:52 +01:00
Ringo Hoffmann
341db84788 close closable body after request 2022-01-28 17:06:32 +01:00
Ringo Hoffmann
b51247bb2c inhibit stream close on request 2022-01-28 16:31:16 +01:00
Ringo Hoffmann
3f8721cd4b fix crash when req is called with no body 2021-11-09 09:32:28 +01:00
Ringo Hoffmann
adba8dc051 add .vscode directory 2021-11-09 09:32:28 +01:00
Ringo Hoffmann
2f2cda4122 use seeker when available on request 2021-11-08 09:54:01 +01:00
Ringo Hoffmann
73a7f0bf37 add artifacts workflow 2021-11-07 22:37:54 +01:00
Ringo Hoffmann
aff231de53 add github workflow for unit tests 2021-11-07 22:36:17 +01:00
Ringo Hoffmann
e5dd1e70b1 create go module 2021-11-07 22:35:47 +01:00
Christoph Polcin
29e74efa70 Merge pull request #46 from jkowalski/read-stream-range
added ReadStreamRange() method to efficiently read a range of data
2021-11-06 10:05:35 +01:00
Jarek Kowalski
741fdbda3d added ReadStreamRange() method to efficiently read a range of data
It passes "Range: bytes=X-Y" and if the server returns HTTP 206,
we know it complied with the request.

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

This will greatly help https://github.com/kopia/kopia which relies on
partial reads from pack blobs.
2021-11-04 23:32:32 -07:00
Christoph Polcin
a3a86976a1 Merge pull request #45 from marcelblijleven/master
Fix index out of range runtime error when empty string is provided to FixSlashes
2021-09-17 15:32:50 +02:00
Marcel Blijleven
a2cbdfa976 Fix index out of range runtime error when provided string is empty 2021-09-17 13:24:28 +02:00
Marcel Blijleven
9a1ba21162 Fix typo in description 2021-09-17 13:23:09 +02:00
Mark Severson
7ff61aa87b Handle request errors in copymove 2021-06-30 12:06:26 +02:00
Christoph Polcin
86f8378cf1 Update API description 2021-04-27 23:21:33 +02:00
Felix Pojtinger
4145fa842c Add ability to define custom interceptors (fixes #35) 2021-04-27 23:12:51 +02:00
David Holdeman
8244b5a5f5 Merge pull request #41 from needsaholiday/bug/pluscharacter
fix unescape
2021-02-03 15:23:56 -06:00
Nick Kratzke
d02a1ebcd2 switched to PathUnescape 2021-02-03 21:16:30 +01:00
Nick
3ed042db71 fix unescape 2021-02-03 15:25:12 +01:00
Christoph Polcin
bdacfab947 update README due to API changes 2020-09-29 10:07:39 +02:00
Jarek Kowalski
617404b525 fixed panic due to concurrent map writes
Fixes #36
2020-09-28 11:17:19 +02:00
11 changed files with 300 additions and 72 deletions

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

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

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

@@ -0,0 +1,30 @@
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.17"
- "1.16"
steps:
- name: Set up Go
uses: actions/setup-go@v1
with:
go-version: ${{ matrix.goversion }}
- name: Check out code into the Go module directory
uses: actions/checkout@v2
- name: Get dependencies
run: go get ./...
- name: Run Unit Tests
run: go test -v -cover -race ./...

2
.gitignore vendored
View File

@@ -17,3 +17,5 @@
# Output of the go coverage tool, specifically when used with LiteIDE # Output of the go coverage tool, specifically when used with LiteIDE
*.out *.out
.vscode/

View File

@@ -167,7 +167,7 @@ included.
* [func String(r io.Reader) string](#String) * [func String(r io.Reader) string](#String)
* [type Authenticator](#Authenticator) * [type Authenticator](#Authenticator)
* [type BasicAuth](#BasicAuth) * [type BasicAuth](#BasicAuth)
* [func (b *BasicAuth) Authorize(c *Client, method string, path string)](#BasicAuth.Authorize) * [func (b *BasicAuth) Authorize(req *http.Request, method string, path string)](#BasicAuth.Authorize)
* [func (b *BasicAuth) Pass() string](#BasicAuth.Pass) * [func (b *BasicAuth) Pass() string](#BasicAuth.Pass)
* [func (b *BasicAuth) Type() string](#BasicAuth.Type) * [func (b *BasicAuth) Type() string](#BasicAuth.Type)
* [func (b *BasicAuth) User() string](#BasicAuth.User) * [func (b *BasicAuth) User() string](#BasicAuth.User)
@@ -184,13 +184,14 @@ included.
* [func (c *Client) RemoveAll(path string) error](#Client.RemoveAll) * [func (c *Client) RemoveAll(path string) error](#Client.RemoveAll)
* [func (c *Client) Rename(oldpath, newpath string, overwrite bool) error](#Client.Rename) * [func (c *Client) Rename(oldpath, newpath string, overwrite bool) error](#Client.Rename)
* [func (c *Client) SetHeader(key, value string)](#Client.SetHeader) * [func (c *Client) SetHeader(key, value string)](#Client.SetHeader)
* [func (c *Client) SetInterceptor(interceptor func(method string, rq *http.Request))](#Client.SetInterceptor)
* [func (c *Client) SetTimeout(timeout time.Duration)](#Client.SetTimeout) * [func (c *Client) SetTimeout(timeout time.Duration)](#Client.SetTimeout)
* [func (c *Client) SetTransport(transport http.RoundTripper)](#Client.SetTransport) * [func (c *Client) SetTransport(transport http.RoundTripper)](#Client.SetTransport)
* [func (c *Client) Stat(path string) (os.FileInfo, error)](#Client.Stat) * [func (c *Client) Stat(path string) (os.FileInfo, error)](#Client.Stat)
* [func (c *Client) Write(path string, data []byte, _ os.FileMode) error](#Client.Write) * [func (c *Client) Write(path string, data []byte, _ os.FileMode) error](#Client.Write)
* [func (c *Client) WriteStream(path string, stream io.Reader, _ os.FileMode) error](#Client.WriteStream) * [func (c *Client) WriteStream(path string, stream io.Reader, _ os.FileMode) error](#Client.WriteStream)
* [type DigestAuth](#DigestAuth) * [type DigestAuth](#DigestAuth)
* [func (d *DigestAuth) Authorize(c *Client, method string, path string)](#DigestAuth.Authorize) * [func (d *DigestAuth) Authorize(req *http.Request, method string, path string)](#DigestAuth.Authorize)
* [func (d *DigestAuth) Pass() string](#DigestAuth.Pass) * [func (d *DigestAuth) Pass() string](#DigestAuth.Pass)
* [func (d *DigestAuth) Type() string](#DigestAuth.Type) * [func (d *DigestAuth) Type() string](#DigestAuth.Type)
* [func (d *DigestAuth) User() string](#DigestAuth.User) * [func (d *DigestAuth) User() string](#DigestAuth.User)
@@ -206,7 +207,7 @@ included.
* [func (f File) String() string](#File.String) * [func (f File) String() string](#File.String)
* [func (f File) Sys() interface{}](#File.Sys) * [func (f File) Sys() interface{}](#File.Sys)
* [type NoAuth](#NoAuth) * [type NoAuth](#NoAuth)
* [func (n *NoAuth) Authorize(c *Client, method string, path string)](#NoAuth.Authorize) * [func (n *NoAuth) Authorize(req *http.Request, method string, path string)](#NoAuth.Authorize)
* [func (n *NoAuth) Pass() string](#NoAuth.Pass) * [func (n *NoAuth) Pass() string](#NoAuth.Pass)
* [func (n *NoAuth) Type() string](#NoAuth.Type) * [func (n *NoAuth) Type() string](#NoAuth.Type)
* [func (n *NoAuth) User() string](#NoAuth.User) * [func (n *NoAuth) User() string](#NoAuth.User)
@@ -254,18 +255,18 @@ func String(r io.Reader) string
``` ```
String pulls a string out of our io.Reader String pulls a string out of our io.Reader
### <a name="Authenticator">type</a> [Authenticator](https://github.com/studio-b12/gowebdav/blob/master/client.go?s=285:398#L24) ### <a name="Authenticator">type</a> [Authenticator](https://github.com/studio-b12/gowebdav/blob/master/client.go?s=381:500#L28)
``` go ``` go
type Authenticator interface { type Authenticator interface {
Type() string Type() string
User() string User() string
Pass() string Pass() string
Authorize(*Client, string, string) Authorize(*http.Request, string, string)
} }
``` ```
Authenticator stub Authenticator stub
### <a name="BasicAuth">type</a> [BasicAuth](https://github.com/studio-b12/gowebdav/blob/master/basicAuth.go?s=94:145#L8) ### <a name="BasicAuth">type</a> [BasicAuth](https://github.com/studio-b12/gowebdav/blob/master/basicAuth.go?s=106:157#L9)
``` go ``` go
type BasicAuth struct { type BasicAuth struct {
// contains filtered or unexported fields // contains filtered or unexported fields
@@ -273,31 +274,31 @@ type BasicAuth struct {
``` ```
BasicAuth structure holds our credentials BasicAuth structure holds our credentials
#### <a name="BasicAuth.Authorize">func</a> (\*BasicAuth) [Authorize](https://github.com/studio-b12/gowebdav/blob/master/basicAuth.go?s=461:529#L29) #### <a name="BasicAuth.Authorize">func</a> (\*BasicAuth) [Authorize](https://github.com/studio-b12/gowebdav/blob/master/basicAuth.go?s=473:549#L30)
``` go ``` go
func (b *BasicAuth) Authorize(c *Client, method string, path string) func (b *BasicAuth) Authorize(req *http.Request, method string, path string)
``` ```
Authorize the current request Authorize the current request
#### <a name="BasicAuth.Pass">func</a> (\*BasicAuth) [Pass](https://github.com/studio-b12/gowebdav/blob/master/basicAuth.go?s=376:409#L24) #### <a name="BasicAuth.Pass">func</a> (\*BasicAuth) [Pass](https://github.com/studio-b12/gowebdav/blob/master/basicAuth.go?s=388:421#L25)
``` go ``` go
func (b *BasicAuth) Pass() string func (b *BasicAuth) Pass() string
``` ```
Pass holds the BasicAuth password Pass holds the BasicAuth password
#### <a name="BasicAuth.Type">func</a> (\*BasicAuth) [Type](https://github.com/studio-b12/gowebdav/blob/master/basicAuth.go?s=189:222#L14) #### <a name="BasicAuth.Type">func</a> (\*BasicAuth) [Type](https://github.com/studio-b12/gowebdav/blob/master/basicAuth.go?s=201:234#L15)
``` go ``` go
func (b *BasicAuth) Type() string func (b *BasicAuth) Type() string
``` ```
Type identifies the BasicAuthenticator Type identifies the BasicAuthenticator
#### <a name="BasicAuth.User">func</a> (\*BasicAuth) [User](https://github.com/studio-b12/gowebdav/blob/master/basicAuth.go?s=285:318#L19) #### <a name="BasicAuth.User">func</a> (\*BasicAuth) [User](https://github.com/studio-b12/gowebdav/blob/master/basicAuth.go?s=297:330#L20)
``` go ``` go
func (b *BasicAuth) User() string func (b *BasicAuth) User() string
``` ```
User holds the BasicAuth username User holds the BasicAuth username
### <a name="Client">type</a> [Client](https://github.com/studio-b12/gowebdav/blob/master/client.go?s=157:261#L16) ### <a name="Client">type</a> [Client](https://github.com/studio-b12/gowebdav/blob/master/client.go?s=165:357#L17)
``` go ``` go
type Client struct { type Client struct {
// contains filtered or unexported fields // contains filtered or unexported fields
@@ -305,103 +306,109 @@ type Client struct {
``` ```
Client defines our structure Client defines our structure
#### <a name="NewClient">func</a> [NewClient](https://github.com/studio-b12/gowebdav/blob/master/client.go?s=902:946#L57) #### <a name="NewClient">func</a> [NewClient](https://github.com/studio-b12/gowebdav/blob/master/client.go?s=1012:1056#L61)
``` go ``` go
func NewClient(uri, user, pw string) *Client func NewClient(uri, user, pw string) *Client
``` ```
NewClient creates a new instance of client NewClient creates a new instance of client
#### <a name="Client.Connect">func</a> (\*Client) [Connect](https://github.com/studio-b12/gowebdav/blob/master/client.go?s=1516:1548#L77) #### <a name="Client.Connect">func</a> (\*Client) [Connect](https://github.com/studio-b12/gowebdav/blob/master/client.go?s=1836:1868#L86)
``` go ``` go
func (c *Client) Connect() error func (c *Client) Connect() error
``` ```
Connect connects to our dav server 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=6376:6444#L303) #### <a name="Client.Copy">func</a> (\*Client) [Copy](https://github.com/studio-b12/gowebdav/blob/master/client.go?s=6695:6763#L312)
``` go ``` go
func (c *Client) Copy(oldpath, newpath string, overwrite bool) error func (c *Client) Copy(oldpath, newpath string, overwrite bool) error
``` ```
Copy copies a file from A to B 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=5467:5523#L262) #### <a name="Client.Mkdir">func</a> (\*Client) [Mkdir](https://github.com/studio-b12/gowebdav/blob/master/client.go?s=5786:5842#L271)
``` go ``` go
func (c *Client) Mkdir(path string, _ os.FileMode) error func (c *Client) Mkdir(path string, _ os.FileMode) error
``` ```
Mkdir makes a directory Mkdir makes a directory
#### <a name="Client.MkdirAll">func</a> (\*Client) [MkdirAll](https://github.com/studio-b12/gowebdav/blob/master/client.go?s=5702:5761#L273) #### <a name="Client.MkdirAll">func</a> (\*Client) [MkdirAll](https://github.com/studio-b12/gowebdav/blob/master/client.go?s=6021:6080#L282)
``` go ``` go
func (c *Client) MkdirAll(path string, _ os.FileMode) error func (c *Client) MkdirAll(path string, _ os.FileMode) error
``` ```
MkdirAll like mkdir -p, but for webdav 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=6550:6600#L308) #### <a name="Client.Read">func</a> (\*Client) [Read](https://github.com/studio-b12/gowebdav/blob/master/client.go?s=6869:6919#L317)
``` go ``` go
func (c *Client) Read(path string) ([]byte, error) func (c *Client) Read(path string) ([]byte, error)
``` ```
Read reads the contents of a remote file 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=2542:2602#L120) #### <a name="Client.ReadDir">func</a> (\*Client) [ReadDir](https://github.com/studio-b12/gowebdav/blob/master/client.go?s=2862:2922#L129)
``` go ``` go
func (c *Client) ReadDir(path string) ([]os.FileInfo, error) func (c *Client) ReadDir(path string) ([]os.FileInfo, error)
``` ```
ReadDir reads the contents of a remote directory 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=6911:6974#L326) #### <a name="Client.ReadStream">func</a> (\*Client) [ReadStream](https://github.com/studio-b12/gowebdav/blob/master/client.go?s=7230:7293#L335)
``` go ``` go
func (c *Client) ReadStream(path string) (io.ReadCloser, error) func (c *Client) ReadStream(path string) (io.ReadCloser, error)
``` ```
ReadStream reads the stream for a given path ReadStream reads the stream for a given path
#### <a name="Client.Remove">func</a> (\*Client) [Remove](https://github.com/studio-b12/gowebdav/blob/master/client.go?s=4973:5015#L239) #### <a name="Client.Remove">func</a> (\*Client) [Remove](https://github.com/studio-b12/gowebdav/blob/master/client.go?s=5292:5334#L248)
``` go ``` go
func (c *Client) Remove(path string) error func (c *Client) Remove(path string) error
``` ```
Remove removes a remote file Remove removes a remote file
#### <a name="Client.RemoveAll">func</a> (\*Client) [RemoveAll](https://github.com/studio-b12/gowebdav/blob/master/client.go?s=5081:5126#L244) #### <a name="Client.RemoveAll">func</a> (\*Client) [RemoveAll](https://github.com/studio-b12/gowebdav/blob/master/client.go?s=5400:5445#L253)
``` go ``` go
func (c *Client) RemoveAll(path string) error func (c *Client) RemoveAll(path string) error
``` ```
RemoveAll removes remote files RemoveAll removes remote files
#### <a name="Client.Rename">func</a> (\*Client) [Rename](https://github.com/studio-b12/gowebdav/blob/master/client.go?s=6210:6280#L298) #### <a name="Client.Rename">func</a> (\*Client) [Rename](https://github.com/studio-b12/gowebdav/blob/master/client.go?s=6529:6599#L307)
``` go ``` go
func (c *Client) Rename(oldpath, newpath string, overwrite bool) error func (c *Client) Rename(oldpath, newpath string, overwrite bool) error
``` ```
Rename moves a file from A to B Rename moves a file from A to B
#### <a name="Client.SetHeader">func</a> (\*Client) [SetHeader](https://github.com/studio-b12/gowebdav/blob/master/client.go?s=1099:1144#L62) #### <a name="Client.SetHeader">func</a> (\*Client) [SetHeader](https://github.com/studio-b12/gowebdav/blob/master/client.go?s=1228:1273#L66)
``` go ``` go
func (c *Client) SetHeader(key, value string) func (c *Client) SetHeader(key, value string)
``` ```
SetHeader lets us set arbitrary headers for a given client SetHeader lets us set arbitrary headers for a given client
#### <a name="Client.SetTimeout">func</a> (\*Client) [SetTimeout](https://github.com/studio-b12/gowebdav/blob/master/client.go?s=1244:1294#L67) #### <a name="Client.SetInterceptor">func</a> (\*Client) [SetInterceptor](https://github.com/studio-b12/gowebdav/blob/master/client.go?s=1380:1462#L71)
``` go
func (c *Client) SetInterceptor(interceptor func(method string, rq *http.Request))
```
SetInterceptor lets us set an arbitrary interceptor for a given client
#### <a name="Client.SetTimeout">func</a> (\*Client) [SetTimeout](https://github.com/studio-b12/gowebdav/blob/master/client.go?s=1564:1614#L76)
``` go ``` go
func (c *Client) SetTimeout(timeout time.Duration) func (c *Client) SetTimeout(timeout time.Duration)
``` ```
SetTimeout exposes the ability to set a time limit for requests SetTimeout exposes the ability to set a time limit for requests
#### <a name="Client.SetTransport">func</a> (\*Client) [SetTransport](https://github.com/studio-b12/gowebdav/blob/master/client.go?s=1387:1445#L72) #### <a name="Client.SetTransport">func</a> (\*Client) [SetTransport](https://github.com/studio-b12/gowebdav/blob/master/client.go?s=1707:1765#L81)
``` go ``` go
func (c *Client) SetTransport(transport http.RoundTripper) func (c *Client) SetTransport(transport http.RoundTripper)
``` ```
SetTransport exposes the ability to define custom transports 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=3929:3984#L187) #### <a name="Client.Stat">func</a> (\*Client) [Stat](https://github.com/studio-b12/gowebdav/blob/master/client.go?s=4248:4303#L196)
``` go ``` go
func (c *Client) Stat(path string) (os.FileInfo, error) func (c *Client) Stat(path string) (os.FileInfo, error)
``` ```
Stat returns the file stats for a specified path 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=7265:7334#L341) #### <a name="Client.Write">func</a> (\*Client) [Write](https://github.com/studio-b12/gowebdav/blob/master/client.go?s=7584:7653#L350)
``` go ``` go
func (c *Client) Write(path string, data []byte, _ os.FileMode) error func (c *Client) Write(path string, data []byte, _ os.FileMode) error
``` ```
Write writes data to a given path 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=7690:7770#L364) #### <a name="Client.WriteStream">func</a> (\*Client) [WriteStream](https://github.com/studio-b12/gowebdav/blob/master/client.go?s=8009:8089#L373)
``` go ``` go
func (c *Client) WriteStream(path string, stream io.Reader, _ os.FileMode) error func (c *Client) WriteStream(path string, stream io.Reader, _ os.FileMode) error
``` ```
@@ -415,9 +422,9 @@ type DigestAuth struct {
``` ```
DigestAuth structure holds our credentials DigestAuth structure holds our credentials
#### <a name="DigestAuth.Authorize">func</a> (\*DigestAuth) [Authorize](https://github.com/studio-b12/gowebdav/blob/master/digestAuth.go?s=577:646#L36) #### <a name="DigestAuth.Authorize">func</a> (\*DigestAuth) [Authorize](https://github.com/studio-b12/gowebdav/blob/master/digestAuth.go?s=577:654#L36)
``` go ``` go
func (d *DigestAuth) Authorize(c *Client, method string, path string) func (d *DigestAuth) Authorize(req *http.Request, method string, path string)
``` ```
Authorize the current request Authorize the current request
@@ -507,7 +514,7 @@ func (f File) Sys() interface{}
``` ```
Sys ???? Sys ????
### <a name="NoAuth">type</a> [NoAuth](https://github.com/studio-b12/gowebdav/blob/master/client.go?s=442:490#L32) ### <a name="NoAuth">type</a> [NoAuth](https://github.com/studio-b12/gowebdav/blob/master/client.go?s=544:592#L36)
``` go ``` go
type NoAuth struct { type NoAuth struct {
// contains filtered or unexported fields // contains filtered or unexported fields
@@ -515,25 +522,25 @@ type NoAuth struct {
``` ```
NoAuth structure holds our credentials NoAuth structure holds our credentials
#### <a name="NoAuth.Authorize">func</a> (\*NoAuth) [Authorize](https://github.com/studio-b12/gowebdav/blob/master/client.go?s=785:850#L53) #### <a name="NoAuth.Authorize">func</a> (\*NoAuth) [Authorize](https://github.com/studio-b12/gowebdav/blob/master/client.go?s=887:960#L57)
``` go ``` go
func (n *NoAuth) Authorize(c *Client, method string, path string) func (n *NoAuth) Authorize(req *http.Request, method string, path string)
``` ```
Authorize the current request Authorize the current request
#### <a name="NoAuth.Pass">func</a> (\*NoAuth) [Pass](https://github.com/studio-b12/gowebdav/blob/master/client.go?s=703:733#L48) #### <a name="NoAuth.Pass">func</a> (\*NoAuth) [Pass](https://github.com/studio-b12/gowebdav/blob/master/client.go?s=805:835#L52)
``` go ``` go
func (n *NoAuth) Pass() string func (n *NoAuth) Pass() string
``` ```
Pass returns the current password Pass returns the current password
#### <a name="NoAuth.Type">func</a> (\*NoAuth) [Type](https://github.com/studio-b12/gowebdav/blob/master/client.go?s=529:559#L38) #### <a name="NoAuth.Type">func</a> (\*NoAuth) [Type](https://github.com/studio-b12/gowebdav/blob/master/client.go?s=631:661#L42)
``` go ``` go
func (n *NoAuth) Type() string func (n *NoAuth) Type() string
``` ```
Type identifies the authenticator Type identifies the authenticator
#### <a name="NoAuth.User">func</a> (\*NoAuth) [User](https://github.com/studio-b12/gowebdav/blob/master/client.go?s=615:645#L43) #### <a name="NoAuth.User">func</a> (\*NoAuth) [User](https://github.com/studio-b12/gowebdav/blob/master/client.go?s=717:747#L47)
``` go ``` go
func (n *NoAuth) User() string func (n *NoAuth) User() string
``` ```

View File

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

View File

@@ -3,12 +3,14 @@ package gowebdav
import ( import (
"bytes" "bytes"
"encoding/xml" "encoding/xml"
"fmt"
"io" "io"
"net/http" "net/http"
"net/url" "net/url"
"os" "os"
pathpkg "path" pathpkg "path"
"strings" "strings"
"sync"
"time" "time"
) )
@@ -16,7 +18,10 @@ import (
type Client struct { type Client struct {
root string root string
headers http.Header headers http.Header
interceptor func(method string, rq *http.Request)
c *http.Client c *http.Client
authMutex sync.Mutex
auth Authenticator auth Authenticator
} }
@@ -25,7 +30,7 @@ type Authenticator interface {
Type() string Type() string
User() string User() string
Pass() string Pass() string
Authorize(*Client, string, string) Authorize(*http.Request, string, string)
} }
// NoAuth structure holds our credentials // NoAuth structure holds our credentials
@@ -50,12 +55,12 @@ func (n *NoAuth) Pass() string {
} }
// Authorize the current request // Authorize the current request
func (n *NoAuth) Authorize(c *Client, method string, path string) { func (n *NoAuth) Authorize(req *http.Request, method string, path string) {
} }
// NewClient creates a new instance of client // NewClient creates a new instance of client
func NewClient(uri, user, pw string) *Client { func NewClient(uri, user, pw string) *Client {
return &Client{FixSlash(uri), make(http.Header), &http.Client{}, &NoAuth{user, pw}} return &Client{FixSlash(uri), make(http.Header), nil, &http.Client{}, sync.Mutex{}, &NoAuth{user, pw}}
} }
// SetHeader lets us set arbitrary headers for a given client // SetHeader lets us set arbitrary headers for a given client
@@ -63,6 +68,11 @@ func (c *Client) SetHeader(key, value string) {
c.headers.Add(key, value) 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 // SetTimeout exposes the ability to set a time limit for requests
func (c *Client) SetTimeout(timeout time.Duration) { func (c *Client) SetTimeout(timeout time.Duration) {
c.c.Timeout = timeout c.c.Timeout = timeout
@@ -135,7 +145,7 @@ func (c *Client) ReadDir(path string) ([]os.FileInfo, error) {
if p := getProps(r, "200"); p != nil { if p := getProps(r, "200"); p != nil {
f := new(File) f := new(File)
if ps, err := url.QueryUnescape(r.Href); err == nil { if ps, err := url.PathUnescape(r.Href); err == nil {
f.name = pathpkg.Base(ps) f.name = pathpkg.Base(ps)
} else { } else {
f.name = p.Name f.name = p.Name
@@ -337,6 +347,43 @@ func (c *Client) ReadStream(path string) (io.ReadCloser, error) {
return nil, newPathError("ReadStream", path, rs.StatusCode) 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) {
r.Header.Add("Range", fmt.Sprintf("bytes=%v-%v", offset, offset+length-1))
})
if err != nil {
return nil, newPathErrorErr("ReadStreamRange", path, err)
}
if rs.StatusCode == http.StatusPartialContent {
// server supported partial content, return as-is.
return rs.Body, nil
}
// server returned success, but did not support partial content, so we have the whole
// stream in rs.Body
if rs.StatusCode == 200 {
// discard first 'offset' bytes.
if _, err := io.Copy(io.Discard, io.LimitReader(rs.Body, offset)); err != nil {
return nil, newPathErrorErr("ReadStreamRange", path, err)
}
// return a io.ReadCloser that is limited to `length` bytes.
return &limitedReadCloser{rs.Body, int(length)}, nil
}
rs.Body.Close()
return nil, newPathError("ReadStream", path, rs.StatusCode)
}
// Write writes data to a given path // Write writes data to a given path
func (c *Client) Write(path string, data []byte, _ os.FileMode) error { func (c *Client) Write(path string, data []byte, _ os.FileMode) error {
s := c.put(path, bytes.NewReader(data)) s := c.put(path, bytes.NewReader(data))

View File

@@ -33,12 +33,12 @@ func (d *DigestAuth) Pass() string {
} }
// Authorize the current request // Authorize the current request
func (d *DigestAuth) Authorize(c *Client, method string, path string) { func (d *DigestAuth) Authorize(req *http.Request, method string, path string) {
d.digestParts["uri"] = path d.digestParts["uri"] = path
d.digestParts["method"] = method d.digestParts["method"] = method
d.digestParts["username"] = d.user d.digestParts["username"] = d.user
d.digestParts["password"] = d.pw d.digestParts["password"] = d.pw
c.headers.Set("Authorization", getDigestAuthorization(d.digestParts)) req.Header.Set("Authorization", getDigestAuthorization(d.digestParts))
} }
func digestParts(resp *http.Response) map[string]string { func digestParts(resp *http.Response) map[string]string {

3
go.mod Normal file
View File

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

View File

@@ -10,58 +10,89 @@ import (
) )
func (c *Client) req(method, path string, body io.Reader, intercept func(*http.Request)) (req *http.Response, err error) { func (c *Client) req(method, path string, body io.Reader, intercept func(*http.Request)) (req *http.Response, err error) {
// Tee the body, because if authorization fails we will need to read from it again.
var r *http.Request var r *http.Request
var ba bytes.Buffer var retryBuf io.Reader
bb := io.TeeReader(body, &ba)
if body == nil { if body != nil {
r, err = http.NewRequest(method, PathEscape(Join(c.root, path)), nil) // Because Request#Do closes closable streams, Seeker#Seek
// will fail on retry because stream is already closed.
// This inhibits the closing of the passed stream on passing
// it to the RoundTripper and closes the stream after we
// are done with the body content.
if cl, ok := body.(io.Closer); ok {
body = closeInhibitor{body}
defer cl.Close()
}
// If the authorization fails, we will need to restart reading
// from the passed body stream.
// When body is seekable, use seek to reset the streams
// cursor to the start.
// Otherwise, copy the stream into a buffer while uploading
// and use the buffers content on retry.
if sk, ok := body.(io.Seeker); ok {
if _, err = sk.Seek(0, io.SeekStart); err != nil {
return
}
retryBuf = body
} else { } else {
r, err = http.NewRequest(method, PathEscape(Join(c.root, path)), bb) buff := &bytes.Buffer{}
retryBuf = buff
body = io.TeeReader(body, buff)
}
r, err = http.NewRequest(method, PathEscape(Join(c.root, path)), body)
} else {
r, err = http.NewRequest(method, PathEscape(Join(c.root, path)), nil)
} }
if err != nil { if err != nil {
return nil, err return nil, err
} }
c.auth.Authorize(c, method, path)
for k, vals := range c.headers { for k, vals := range c.headers {
for _, v := range vals { for _, v := range vals {
r.Header.Add(k, v) r.Header.Add(k, v)
} }
} }
// make sure we read 'c.auth' only once since it will be substituted below
// and that is unsafe to do when multiple goroutines are running at the same time.
c.authMutex.Lock()
auth := c.auth
c.authMutex.Unlock()
auth.Authorize(r, method, path)
if intercept != nil { if intercept != nil {
intercept(r) intercept(r)
} }
if c.interceptor != nil {
c.interceptor(method, r)
}
rs, err := c.c.Do(r) rs, err := c.c.Do(r)
if err != nil { if err != nil {
return nil, err return nil, err
} }
if rs.StatusCode == 401 && c.auth.Type() == "NoAuth" { if rs.StatusCode == 401 && auth.Type() == "NoAuth" {
wwwAuthenticateHeader := strings.ToLower(rs.Header.Get("Www-Authenticate")) wwwAuthenticateHeader := strings.ToLower(rs.Header.Get("Www-Authenticate"))
if strings.Index(wwwAuthenticateHeader, "digest") > -1 { if strings.Index(wwwAuthenticateHeader, "digest") > -1 {
c.auth = &DigestAuth{c.auth.User(), c.auth.Pass(), digestParts(rs)} c.authMutex.Lock()
c.auth = &DigestAuth{auth.User(), auth.Pass(), digestParts(rs)}
c.authMutex.Unlock()
} else if strings.Index(wwwAuthenticateHeader, "basic") > -1 { } else if strings.Index(wwwAuthenticateHeader, "basic") > -1 {
c.auth = &BasicAuth{c.auth.User(), c.auth.Pass()} c.authMutex.Lock()
c.auth = &BasicAuth{auth.User(), auth.Pass()}
c.authMutex.Unlock()
} else { } else {
return rs, newPathError("Authorize", c.root, rs.StatusCode) return rs, newPathError("Authorize", c.root, rs.StatusCode)
} }
if body == nil { // retryBuf will be nil if body was nil initially so no check
return c.req(method, path, nil, intercept) // for body == nil is required here.
} else { return c.req(method, path, retryBuf, intercept)
return c.req(method, path, &ba, intercept)
}
} else if rs.StatusCode == 401 { } else if rs.StatusCode == 401 {
return rs, newPathError("Authorize", c.root, rs.StatusCode) return rs, newPathError("Authorize", c.root, rs.StatusCode)
} }
@@ -131,7 +162,9 @@ func (c *Client) doCopyMove(method string, oldpath string, newpath string, overw
func (c *Client) copymove(method string, oldpath string, newpath string, overwrite bool) error { func (c *Client) copymove(method string, oldpath string, newpath string, overwrite bool) error {
s, data := c.doCopyMove(method, oldpath, newpath, overwrite) s, data := c.doCopyMove(method, oldpath, newpath, overwrite)
if data != nil {
defer data.Close() defer data.Close()
}
switch s { switch s {
case 201, 204: case 201, 204:

View File

@@ -32,7 +32,7 @@ func newPathErrorErr(op string, path string, err error) error {
} }
} }
// PathEscape escapes all segemnts of a given path // PathEscape escapes all segments of a given path
func PathEscape(path string) string { func PathEscape(path string) string {
s := strings.Split(path, "/") s := strings.Split(path, "/")
for i, e := range s { for i, e := range s {
@@ -51,9 +51,10 @@ func FixSlash(s string) string {
// FixSlashes appends and prepends a / if they are missing // FixSlashes appends and prepends a / if they are missing
func FixSlashes(s string) string { func FixSlashes(s string) string {
if s[0] != '/' { if !strings.HasPrefix(s, "/") {
s = "/" + s s = "/" + s
} }
return FixSlash(s) return FixSlash(s)
} }
@@ -107,3 +108,39 @@ func parseXML(data io.Reader, resp interface{}, parse func(resp interface{}) err
} }
return nil 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()
}
// closeInhibitor implements io.Closer and
// wraps a Reader. When Close() is performed
// on it, it will simply be silently rejected.
type closeInhibitor struct {
io.Reader
}
func (ci closeInhibitor) Close() error {
return nil
}

View File

@@ -43,3 +43,25 @@ func TestEscapeURL(t *testing.T) {
t.Error("expected: " + ex + " got: " + 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)
}
}