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
*.out
.vscode/

View File

@@ -167,7 +167,7 @@ included.
* [func String(r io.Reader) string](#String)
* [type Authenticator](#Authenticator)
* [type BasicAuth](#BasicAuth)
* [func (b *BasicAuth) Authorize(c *Client, method string, path string)](#BasicAuth.Authorize)
* [func (b *BasicAuth) Authorize(req *http.Request, method string, path string)](#BasicAuth.Authorize)
* [func (b *BasicAuth) Pass() string](#BasicAuth.Pass)
* [func (b *BasicAuth) Type() string](#BasicAuth.Type)
* [func (b *BasicAuth) User() string](#BasicAuth.User)
@@ -184,13 +184,14 @@ included.
* [func (c *Client) RemoveAll(path string) error](#Client.RemoveAll)
* [func (c *Client) Rename(oldpath, newpath string, overwrite bool) error](#Client.Rename)
* [func (c *Client) SetHeader(key, value string)](#Client.SetHeader)
* [func (c *Client) SetInterceptor(interceptor func(method string, rq *http.Request))](#Client.SetInterceptor)
* [func (c *Client) SetTimeout(timeout time.Duration)](#Client.SetTimeout)
* [func (c *Client) SetTransport(transport http.RoundTripper)](#Client.SetTransport)
* [func (c *Client) Stat(path string) (os.FileInfo, error)](#Client.Stat)
* [func (c *Client) Write(path string, data []byte, _ os.FileMode) error](#Client.Write)
* [func (c *Client) WriteStream(path string, stream io.Reader, _ os.FileMode) error](#Client.WriteStream)
* [type DigestAuth](#DigestAuth)
* [func (d *DigestAuth) Authorize(c *Client, method string, path string)](#DigestAuth.Authorize)
* [func (d *DigestAuth) Authorize(req *http.Request, method string, path string)](#DigestAuth.Authorize)
* [func (d *DigestAuth) Pass() string](#DigestAuth.Pass)
* [func (d *DigestAuth) Type() string](#DigestAuth.Type)
* [func (d *DigestAuth) User() string](#DigestAuth.User)
@@ -206,7 +207,7 @@ included.
* [func (f File) String() string](#File.String)
* [func (f File) Sys() interface{}](#File.Sys)
* [type NoAuth](#NoAuth)
* [func (n *NoAuth) Authorize(c *Client, method string, path string)](#NoAuth.Authorize)
* [func (n *NoAuth) Authorize(req *http.Request, method string, path string)](#NoAuth.Authorize)
* [func (n *NoAuth) Pass() string](#NoAuth.Pass)
* [func (n *NoAuth) Type() string](#NoAuth.Type)
* [func (n *NoAuth) User() string](#NoAuth.User)
@@ -254,18 +255,18 @@ func String(r io.Reader) string
```
String pulls a string out of our io.Reader
### <a name="Authenticator">type</a> [Authenticator](https://github.com/studio-b12/gowebdav/blob/master/client.go?s=285:398#L24)
### <a name="Authenticator">type</a> [Authenticator](https://github.com/studio-b12/gowebdav/blob/master/client.go?s=381:500#L28)
``` go
type Authenticator interface {
Type() string
User() string
Pass() string
Authorize(*Client, string, string)
Authorize(*http.Request, string, string)
}
```
Authenticator stub
### <a name="BasicAuth">type</a> [BasicAuth](https://github.com/studio-b12/gowebdav/blob/master/basicAuth.go?s=94:145#L8)
### <a name="BasicAuth">type</a> [BasicAuth](https://github.com/studio-b12/gowebdav/blob/master/basicAuth.go?s=106:157#L9)
``` go
type BasicAuth struct {
// contains filtered or unexported fields
@@ -273,31 +274,31 @@ type BasicAuth struct {
```
BasicAuth structure holds our credentials
#### <a name="BasicAuth.Authorize">func</a> (\*BasicAuth) [Authorize](https://github.com/studio-b12/gowebdav/blob/master/basicAuth.go?s=461:529#L29)
#### <a name="BasicAuth.Authorize">func</a> (\*BasicAuth) [Authorize](https://github.com/studio-b12/gowebdav/blob/master/basicAuth.go?s=473:549#L30)
``` go
func (b *BasicAuth) Authorize(c *Client, method string, path string)
func (b *BasicAuth) Authorize(req *http.Request, method string, path string)
```
Authorize the current request
#### <a name="BasicAuth.Pass">func</a> (\*BasicAuth) [Pass](https://github.com/studio-b12/gowebdav/blob/master/basicAuth.go?s=376:409#L24)
#### <a name="BasicAuth.Pass">func</a> (\*BasicAuth) [Pass](https://github.com/studio-b12/gowebdav/blob/master/basicAuth.go?s=388:421#L25)
``` go
func (b *BasicAuth) Pass() string
```
Pass holds the BasicAuth password
#### <a name="BasicAuth.Type">func</a> (\*BasicAuth) [Type](https://github.com/studio-b12/gowebdav/blob/master/basicAuth.go?s=189:222#L14)
#### <a name="BasicAuth.Type">func</a> (\*BasicAuth) [Type](https://github.com/studio-b12/gowebdav/blob/master/basicAuth.go?s=201:234#L15)
``` go
func (b *BasicAuth) Type() string
```
Type identifies the BasicAuthenticator
#### <a name="BasicAuth.User">func</a> (\*BasicAuth) [User](https://github.com/studio-b12/gowebdav/blob/master/basicAuth.go?s=285:318#L19)
#### <a name="BasicAuth.User">func</a> (\*BasicAuth) [User](https://github.com/studio-b12/gowebdav/blob/master/basicAuth.go?s=297:330#L20)
``` go
func (b *BasicAuth) User() string
```
User holds the BasicAuth username
### <a name="Client">type</a> [Client](https://github.com/studio-b12/gowebdav/blob/master/client.go?s=157:261#L16)
### <a name="Client">type</a> [Client](https://github.com/studio-b12/gowebdav/blob/master/client.go?s=165:357#L17)
``` go
type Client struct {
// contains filtered or unexported fields
@@ -305,103 +306,109 @@ type Client struct {
```
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
func NewClient(uri, user, pw string) *Client
```
NewClient creates a new instance of client
#### <a name="Client.Connect">func</a> (\*Client) [Connect](https://github.com/studio-b12/gowebdav/blob/master/client.go?s=1516:1548#L77)
#### <a name="Client.Connect">func</a> (\*Client) [Connect](https://github.com/studio-b12/gowebdav/blob/master/client.go?s=1836:1868#L86)
``` 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=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
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=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
func (c *Client) Mkdir(path string, _ os.FileMode) error
```
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
func (c *Client) MkdirAll(path string, _ os.FileMode) 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=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
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=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
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=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
func (c *Client) ReadStream(path string) (io.ReadCloser, error)
```
ReadStream reads the stream for a given path
#### <a name="Client.Remove">func</a> (\*Client) [Remove](https://github.com/studio-b12/gowebdav/blob/master/client.go?s=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
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=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
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=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
func (c *Client) Rename(oldpath, newpath string, overwrite bool) error
```
Rename moves a file from A to B
#### <a name="Client.SetHeader">func</a> (\*Client) [SetHeader](https://github.com/studio-b12/gowebdav/blob/master/client.go?s=1099:1144#L62)
#### <a name="Client.SetHeader">func</a> (\*Client) [SetHeader](https://github.com/studio-b12/gowebdav/blob/master/client.go?s=1228:1273#L66)
``` go
func (c *Client) SetHeader(key, value string)
```
SetHeader lets us set arbitrary headers for a given client
#### <a name="Client.SetTimeout">func</a> (\*Client) [SetTimeout](https://github.com/studio-b12/gowebdav/blob/master/client.go?s=1244:1294#L67)
#### <a name="Client.SetInterceptor">func</a> (\*Client) [SetInterceptor](https://github.com/studio-b12/gowebdav/blob/master/client.go?s=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
func (c *Client) SetTimeout(timeout time.Duration)
```
SetTimeout exposes the ability to set a time limit for requests
#### <a name="Client.SetTransport">func</a> (\*Client) [SetTransport](https://github.com/studio-b12/gowebdav/blob/master/client.go?s=1387:1445#L72)
#### <a name="Client.SetTransport">func</a> (\*Client) [SetTransport](https://github.com/studio-b12/gowebdav/blob/master/client.go?s=1707:1765#L81)
``` 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=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
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=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
func (c *Client) Write(path string, data []byte, _ os.FileMode) 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=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
func (c *Client) WriteStream(path string, stream io.Reader, _ os.FileMode) error
```
@@ -415,9 +422,9 @@ type DigestAuth struct {
```
DigestAuth structure holds our credentials
#### <a name="DigestAuth.Authorize">func</a> (\*DigestAuth) [Authorize](https://github.com/studio-b12/gowebdav/blob/master/digestAuth.go?s=577:646#L36)
#### <a name="DigestAuth.Authorize">func</a> (\*DigestAuth) [Authorize](https://github.com/studio-b12/gowebdav/blob/master/digestAuth.go?s=577:654#L36)
``` go
func (d *DigestAuth) Authorize(c *Client, method string, path string)
func (d *DigestAuth) Authorize(req *http.Request, method string, path string)
```
Authorize the current request
@@ -507,7 +514,7 @@ func (f File) Sys() interface{}
```
Sys ????
### <a name="NoAuth">type</a> [NoAuth](https://github.com/studio-b12/gowebdav/blob/master/client.go?s=442:490#L32)
### <a name="NoAuth">type</a> [NoAuth](https://github.com/studio-b12/gowebdav/blob/master/client.go?s=544:592#L36)
``` go
type NoAuth struct {
// contains filtered or unexported fields
@@ -515,25 +522,25 @@ type NoAuth struct {
```
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
func (n *NoAuth) Authorize(c *Client, method string, path string)
func (n *NoAuth) Authorize(req *http.Request, method string, path string)
```
Authorize the current request
#### <a name="NoAuth.Pass">func</a> (\*NoAuth) [Pass](https://github.com/studio-b12/gowebdav/blob/master/client.go?s=703:733#L48)
#### <a name="NoAuth.Pass">func</a> (\*NoAuth) [Pass](https://github.com/studio-b12/gowebdav/blob/master/client.go?s=805:835#L52)
``` go
func (n *NoAuth) Pass() string
```
Pass returns the current password
#### <a name="NoAuth.Type">func</a> (\*NoAuth) [Type](https://github.com/studio-b12/gowebdav/blob/master/client.go?s=529:559#L38)
#### <a name="NoAuth.Type">func</a> (\*NoAuth) [Type](https://github.com/studio-b12/gowebdav/blob/master/client.go?s=631:661#L42)
``` go
func (n *NoAuth) Type() string
```
Type identifies the authenticator
#### <a name="NoAuth.User">func</a> (\*NoAuth) [User](https://github.com/studio-b12/gowebdav/blob/master/client.go?s=615:645#L43)
#### <a name="NoAuth.User">func</a> (\*NoAuth) [User](https://github.com/studio-b12/gowebdav/blob/master/client.go?s=717:747#L47)
``` go
func (n *NoAuth) User() string
```

View File

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

View File

@@ -3,21 +3,26 @@ package gowebdav
import (
"bytes"
"encoding/xml"
"fmt"
"io"
"net/http"
"net/url"
"os"
pathpkg "path"
"strings"
"sync"
"time"
)
// Client defines our structure
type Client struct {
root string
headers http.Header
c *http.Client
auth Authenticator
root string
headers http.Header
interceptor func(method string, rq *http.Request)
c *http.Client
authMutex sync.Mutex
auth Authenticator
}
// Authenticator stub
@@ -25,7 +30,7 @@ type Authenticator interface {
Type() string
User() string
Pass() string
Authorize(*Client, string, string)
Authorize(*http.Request, string, string)
}
// NoAuth structure holds our credentials
@@ -50,12 +55,12 @@ func (n *NoAuth) Pass() string {
}
// Authorize the current request
func (n *NoAuth) Authorize(c *Client, method string, path string) {
func (n *NoAuth) Authorize(req *http.Request, method string, path string) {
}
// NewClient creates a new instance of client
func NewClient(uri, user, pw string) *Client {
return &Client{FixSlash(uri), make(http.Header), &http.Client{}, &NoAuth{user, pw}}
return &Client{FixSlash(uri), make(http.Header), nil, &http.Client{}, sync.Mutex{}, &NoAuth{user, pw}}
}
// SetHeader lets us set arbitrary headers for a given client
@@ -63,6 +68,11 @@ func (c *Client) SetHeader(key, value string) {
c.headers.Add(key, value)
}
// SetInterceptor lets us set an arbitrary interceptor for a given client
func (c *Client) SetInterceptor(interceptor func(method string, rq *http.Request)) {
c.interceptor = interceptor
}
// SetTimeout exposes the ability to set a time limit for requests
func (c *Client) SetTimeout(timeout time.Duration) {
c.c.Timeout = timeout
@@ -135,7 +145,7 @@ func (c *Client) ReadDir(path string) ([]os.FileInfo, error) {
if p := getProps(r, "200"); p != nil {
f := new(File)
if ps, err := url.QueryUnescape(r.Href); err == nil {
if ps, err := url.PathUnescape(r.Href); err == nil {
f.name = pathpkg.Base(ps)
} else {
f.name = p.Name
@@ -337,6 +347,43 @@ func (c *Client) ReadStream(path string) (io.ReadCloser, error) {
return nil, newPathError("ReadStream", path, rs.StatusCode)
}
// ReadStreamRange reads the stream representing a subset of bytes for a given path,
// utilizing HTTP Range Requests if the server supports it.
// The range is expressed as offset from the start of the file and length, for example
// offset=10, length=10 will return bytes 10 through 19.
//
// If the server does not support partial content requests and returns full content instead,
// this function will emulate the behavior by skipping `offset` bytes and limiting the result
// to `length`.
func (c *Client) ReadStreamRange(path string, offset, length int64) (io.ReadCloser, error) {
rs, err := c.req("GET", path, nil, func(r *http.Request) {
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
func (c *Client) Write(path string, data []byte, _ os.FileMode) error {
s := c.put(path, bytes.NewReader(data))

View File

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

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) {
// Tee the body, because if authorization fails we will need to read from it again.
var r *http.Request
var ba bytes.Buffer
bb := io.TeeReader(body, &ba)
var retryBuf io.Reader
if body == nil {
r, err = http.NewRequest(method, PathEscape(Join(c.root, path)), nil)
if body != 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 {
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)), bb)
r, err = http.NewRequest(method, PathEscape(Join(c.root, path)), nil)
}
if err != nil {
return nil, err
}
c.auth.Authorize(c, method, path)
for k, vals := range c.headers {
for _, v := range vals {
r.Header.Add(k, v)
}
}
// make sure we read 'c.auth' only once since it will be substituted below
// and that is unsafe to do when multiple goroutines are running at the same time.
c.authMutex.Lock()
auth := c.auth
c.authMutex.Unlock()
auth.Authorize(r, method, path)
if intercept != nil {
intercept(r)
}
if c.interceptor != nil {
c.interceptor(method, r)
}
rs, err := c.c.Do(r)
if err != nil {
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"))
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 {
c.auth = &BasicAuth{c.auth.User(), c.auth.Pass()}
c.authMutex.Lock()
c.auth = &BasicAuth{auth.User(), auth.Pass()}
c.authMutex.Unlock()
} else {
return rs, newPathError("Authorize", c.root, rs.StatusCode)
}
if body == nil {
return c.req(method, path, nil, intercept)
} else {
return c.req(method, path, &ba, intercept)
}
// retryBuf will be nil if body was nil initially so no check
// for body == nil is required here.
return c.req(method, path, retryBuf, intercept)
} else if rs.StatusCode == 401 {
return rs, newPathError("Authorize", c.root, rs.StatusCode)
}
@@ -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 {
s, data := c.doCopyMove(method, oldpath, newpath, overwrite)
defer data.Close()
if data != nil {
defer data.Close()
}
switch s {
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 {
s := strings.Split(path, "/")
for i, e := range s {
@@ -51,9 +51,10 @@ func FixSlash(s string) string {
// FixSlashes appends and prepends a / if they are missing
func FixSlashes(s string) string {
if s[0] != '/' {
if !strings.HasPrefix(s, "/") {
s = "/" + s
}
return FixSlash(s)
}
@@ -107,3 +108,39 @@ func parseXML(data io.Reader, resp interface{}, parse func(resp interface{}) err
}
return nil
}
// limitedReadCloser wraps a io.ReadCloser and limits the number of bytes that can be read from it.
type limitedReadCloser struct {
rc io.ReadCloser
remaining int
}
func (l *limitedReadCloser) Read(buf []byte) (int, error) {
if l.remaining <= 0 {
return 0, io.EOF
}
if len(buf) > l.remaining {
buf = buf[0:l.remaining]
}
n, err := l.rc.Read(buf)
l.remaining -= n
return n, err
}
func (l *limitedReadCloser) Close() error {
return l.rc.Close()
}
// 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())
}
}
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)
}
}