50 Commits
0 ... 7

Author SHA1 Message Date
misha-plus
9ff8e33634 fix authorization 2018-06-10 11:01:51 +02:00
Aaron Bieber
a33240e4ab add ability to read login / pw from ~/.netrc 2018-05-26 01:36:44 +02:00
Christoph Polcin
fbcb29d33e docs 2018-05-25 23:59:53 +02:00
David
6d8c168f72 Add Authenticator interface and Digest auth support 2018-05-25 22:40:13 +02:00
Christoph Polcin
5bedad6f1e cmd: refactor method 2018-05-25 20:31:58 +02:00
Christoph Polcin
31e0b57e53 cmd: PUT with optinal FILE argument 2018-05-25 20:27:56 +02:00
Christoph Polcin
ba3a71318b cmd: refactor output message 2018-05-25 19:58:46 +02:00
Christoph Polcin
fa51555f16 cmd: simplify arg handling 2018-05-25 12:28:37 +02:00
Christoph Polcin
e0b778960b cmd: GET with optional FILE argument 2018-05-25 12:28:37 +02:00
Christoph Polcin
a98da9745e cmd: create parent directories if writing files 2018-05-25 12:28:37 +02:00
Christoph Polcin
1786d37966 ignore second cmd arg 2018-05-25 12:28:37 +02:00
Christoph Polcin
32d5561fb6 add make check 2018-05-25 12:28:37 +02:00
Christoph Polcin
d6b82df09a update cmd usage 2018-05-25 12:28:34 +02:00
Christoph Polcin
c16034adb0 refactor getCmd 2018-05-25 10:15:26 +02:00
Christoph Polcin
1e4b7a5554 refactor client 2018-05-24 01:24:27 +02:00
Christoph Polcin
3993494db0 refactor cmd 2018-05-24 01:24:25 +02:00
Christoph Polcin
34368960d0 refactor 2018-05-23 14:15:46 +02:00
Christoph Polcin
375f391c8a Escape URL 2018-05-23 13:41:06 +02:00
Shishkin Artem
c49c91989e % and # in the URL 2018-05-22 12:34:34 +02:00
Christoph Polcin
7ea52a8e4f fix close 2018-05-17 01:24:14 +02:00
Abdurrahman
0e7fe90d28 Fetch ContentType and ETag 2018-05-17 01:22:58 +02:00
Christoph Polcin
0120c3c3c4 hello travis-ci 2018-05-17 00:27:52 +02:00
Christoph Polcin
410e447c49 simplify 409 write error handler 2018-05-16 23:39:37 +02:00
Christoph Polcin
65a34986d4 update Readme 2018-05-16 14:35:38 +02:00
Keith Ball
73b90bf27e Do not do a read on the root on connect 2018-05-16 14:17:35 +02:00
Keith Ball
9c695fe0df Return codes 2018-05-16 14:15:47 +02:00
Christoph Polcin
732590873e docs 2018-05-16 14:05:18 +02:00
Christoph Polcin
5a1a85f622 update README 2018-05-16 14:05:18 +02:00
Christoph Polcin
056ad2c4d5 refactor main 2018-05-16 12:23:42 +02:00
Aaron Bieber
8a7d0f4982 cleanup 2018-05-16 10:43:04 +02:00
Aaron Bieber
beeefa572c more docs / cleanup 2018-05-16 10:42:53 +02:00
Aaron Bieber
b290410ef9 add a bit of doc, cleanup some warnings 2018-05-16 10:42:38 +02:00
Aaron Bieber
af17afdfdc reference full path for gowebdav 2018-05-16 10:34:52 +02:00
Aaron Bieber
76d61cc2bc use defaults for GOPATH 2018-05-16 10:34:42 +02:00
Christoph Polcin
06583fe483 update rfc links 2018-05-15 12:14:08 +02:00
Christoph Polcin
49e0e45d8f fmt 2018-05-15 11:51:59 +02:00
Christoph Polcin
a3c4dbab4a add SetTimeout 2018-05-15 11:51:53 +02:00
Christoph Polcin
f60c73fbb4 Merge pull request #8 from Ferada/fix-read-status-code
Handle error response when reading a file.
2017-12-20 19:29:10 +01:00
Olof-Joachim Frahm
a11466bd13 Handle error response when reading a file. 2017-12-20 10:53:49 +01:00
Christoph Polcin
e3cd1f98e7 Merge pull request #6 from ProgramYazar/master
fix delete for yandex
2017-09-19 07:22:16 +02:00
Christoph Polcin
e3a31466a7 Merge pull request #4 from ayllon/master
Add SetTransport method
2017-09-19 07:19:23 +02:00
Engin KIZILGÜN
2b5dab74d3 fix delete for yandex
204 success added
2017-09-18 15:54:19 +03:00
Alejandro Alvarez Ayllon
79a29f3ad5 Add SetTransport method 2017-08-07 12:13:13 +02:00
Christoph Polcin
a03a0a3645 Merge pull request #3 from deyring/master
SetHeader method added to client
2017-04-12 17:59:32 +02:00
Daniel Eyring
12fe295146 SetHeader method added to client 2016-07-27 14:36:21 +02:00
Christoph Polcin
8f99657223 client: dry 2015-12-09 10:02:37 +01:00
Christoph Polcin
b12f1c1b33 Merge pull request #2 from mattn/handle-href
look href if displayname not exists

thanks!
2015-12-09 09:56:31 +01:00
Yasuhiro Matsumoto
33816041d6 Use href instead of displayname for Name() 2015-12-09 17:35:24 +09:00
Christoph Polcin
31c3cc07c7 Merge pull request #1 from mattn/fix-directory-timestamp
directory should have mod-time
2015-12-09 09:32:20 +01:00
Yasuhiro Matsumoto
87bbafc0c0 directory should have mod-time 2015-11-16 22:41:20 +09:00
15 changed files with 1169 additions and 284 deletions

6
.gitignore vendored
View File

@@ -1,2 +1,4 @@
src /src
bin /bin
/pkg
/gowebdav

4
.travis.yml Normal file
View File

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

View File

@@ -1,21 +1,33 @@
SRC := $(wildcard *.go) main/client.go BIN := gowebdav
BIN := bin SRC := $(wildcard *.go) cmd/gowebdav/main.go
CLIENT := ${BIN}/client
GOPATH ?= ${PWD}
all: test client all: test cmd
client: ${CLIENT} cmd: ${BIN}
${CLIENT}: ${SRC} ${BIN}: ${SRC}
@echo build $@ go build -o $@ ./cmd/gowebdav
@GOPATH=${GOPATH} go build -o $@ -- main/client.go
test: test:
@GOPATH=${GOPATH} go test go test -v --short ./...
api:
@sed '/^## API$$/,$$d' -i README.md
@echo '## API' >> README.md
@godoc2md github.com/studio-b12/gowebdav | sed '/^$$/N;/^\n$$/D' |\
sed '2d' |\
sed 's/\/src\/github.com\/studio-b12\/gowebdav\//https:\/\/github.com\/studio-b12\/gowebdav\/blob\/master\//g' |\
sed 's/\/src\/target\//https:\/\/github.com\/studio-b12\/gowebdav\/blob\/master\//g' |\
sed 's/^#/##/g' >> README.md
check:
gofmt -w -s $(SRC)
@echo
gocyclo -over 15 .
@echo
golint ./...
clean: clean:
@echo clean ${BIN} @rm -f ${BIN}
@rm -f ${BIN}/*
.PHONY: all client clean .PHONY: all cmd clean test api check

473
README.md
View File

@@ -1,10 +1,473 @@
# GOWEBDAV - WebDAV Client for golang # GoWebDAV
[![Build Status](https://travis-ci.org/studio-b12/gowebdav.svg?branch=master)](https://travis-ci.org/studio-b12/gowebdav)
[![GoDoc](https://godoc.org/github.com/studio-b12/gowebdav?status.svg)](https://godoc.org/github.com/studio-b12/gowebdav)
[![Go Report Card](https://goreportcard.com/badge/github.com/studio-b12/gowebdav)](https://goreportcard.com/report/github.com/studio-b12/gowebdav)
A golang WebDAV client library and command line tool.
## Install command line tool
```sh
go get -u github.com/studio-b12/gowebdav/cmd/gowebdav
```
## Usage
```sh
$ gowebdav --help
Usage of gowebdav
-X string
Method:
LS <PATH>
STAT <PATH>
MKDIR <PATH>
MKDIRALL <PATH>
GET <PATH> [<FILE>]
PUT <PATH> [<FILE>]
MV <OLD> <NEW>
CP <OLD> <NEW>
DEL <PATH>
-netrc-file string
read login from netrc file (default "~/.netrc")
-pw string
Password [ENV.PASSWORD]
-root string
WebDAV Endpoint [ENV.ROOT]
-user string
User [ENV.USER] (default "$USER")
```
*gowebdav wrapper script*
Create a wrapper script for example `$EDITOR ./dav && chmod a+x ./dav` for your
server and use [pass](https://www.passwordstore.org/ "the standard unix password manager")
or similar tools to retrieve the password.
```sh
#!/bin/sh
ROOT="https://my.dav.server/" \
USER="foo" \
PASSWORD="$(pass dav/foo@my.dav.server)" \
gowebdav $@
```
*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
```
## LINKS ## LINKS
* [rfc2518](http://asg.andrew.cmu.edu/rfc/rfc2518.html) * [RFC 2518 - HTTP Extensions for Distributed Authoring -- WEBDAV](http://www.faqs.org/rfcs/rfc2518.html "RFC 2518 - HTTP Extensions for Distributed Authoring -- WEBDAV")
* [rfc2616](http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html) * [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")
## Books ## API
* [WebDav: Next Gen](https://books.google.de/books?isbn=0130652083 "WebDav: Next Generation Collaborative Web Authoring By Lisa Dusseault") `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>
* [func FixSlash(s string) string](#FixSlash)
* [func FixSlashes(s string) string](#FixSlashes)
* [func Join(path0 string, path1 string) string](#Join)
* [func PathEscape(path string) string](#PathEscape)
* [func ReadConfig(uri, netrc string) (string, string)](#ReadConfig)
* [func String(r io.Reader) string](#String)
* [type Authenticator](#Authenticator)
* [type BasicAuth](#BasicAuth)
* [func (b *BasicAuth) Authorize(c *Client, method string, path string)](#BasicAuth.Authorize)
* [func (b *BasicAuth) Pass() string](#BasicAuth.Pass)
* [func (b *BasicAuth) Type() string](#BasicAuth.Type)
* [func (b *BasicAuth) User() string](#BasicAuth.User)
* [type Client](#Client)
* [func NewClient(uri, user, pw string) *Client](#NewClient)
* [func (c *Client) Connect() error](#Client.Connect)
* [func (c *Client) Copy(oldpath, newpath string, overwrite bool) error](#Client.Copy)
* [func (c *Client) Mkdir(path string, _ os.FileMode) error](#Client.Mkdir)
* [func (c *Client) MkdirAll(path string, _ os.FileMode) error](#Client.MkdirAll)
* [func (c *Client) 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) 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) 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) Pass() string](#DigestAuth.Pass)
* [func (d *DigestAuth) Type() string](#DigestAuth.Type)
* [func (d *DigestAuth) User() string](#DigestAuth.User)
* [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) Size() int64](#File.Size)
* [func (f File) String() string](#File.String)
* [func (f File) Sys() interface{}](#File.Sys)
* [type NoAuth](#NoAuth)
* [func (n *NoAuth) Authorize(c *Client, method string, path string)](#NoAuth.Authorize)
* [func (n *NoAuth) Pass() string](#NoAuth.Pass)
* [func (n *NoAuth) Type() string](#NoAuth.Type)
* [func (n *NoAuth) User() string](#NoAuth.User)
##### <a name="pkg-examples">Examples</a>
* [PathEscape](#example_PathEscape)
##### <a name="pkg-files">Package files</a>
[basicAuth.go](https://github.com/studio-b12/gowebdav/blob/master/basicAuth.go) [client.go](https://github.com/studio-b12/gowebdav/blob/master/client.go) [digestAuth.go](https://github.com/studio-b12/gowebdav/blob/master/digestAuth.go) [doc.go](https://github.com/studio-b12/gowebdav/blob/master/doc.go) [file.go](https://github.com/studio-b12/gowebdav/blob/master/file.go) [netrc.go](https://github.com/studio-b12/gowebdav/blob/master/netrc.go) [requests.go](https://github.com/studio-b12/gowebdav/blob/master/requests.go) [utils.go](https://github.com/studio-b12/gowebdav/blob/master/utils.go)
### <a name="FixSlash">func</a> [FixSlash](https://github.com/studio-b12/gowebdav/blob/master/utils.go?s=707:737#L45)
``` go
func FixSlash(s string) string
```
FixSlash appends a trailing / to our string
### <a name="FixSlashes">func</a> [FixSlashes](https://github.com/studio-b12/gowebdav/blob/master/utils.go?s=859:891#L53)
``` go
func FixSlashes(s string) string
```
FixSlashes appends and prepends a / if they are missing
### <a name="Join">func</a> [Join](https://github.com/studio-b12/gowebdav/blob/master/utils.go?s=976:1020#L61)
``` go
func Join(path0 string, path1 string) string
```
Join joins two paths
### <a name="PathEscape">func</a> [PathEscape](https://github.com/studio-b12/gowebdav/blob/master/utils.go?s=506:541#L36)
``` go
func PathEscape(path string) string
```
PathEscape escapes all segemnts 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=1150:1181#L66)
``` go
func String(r io.Reader) string
```
String pulls a string out of our io.Reader
### <a name="Authenticator">type</a> [Authenticator](https://github.com/studio-b12/gowebdav/blob/master/client.go?s=285:398#L24)
``` go
type Authenticator interface {
Type() string
User() string
Pass() string
Authorize(*Client, string, string)
}
```
Authenticator stub
### <a name="BasicAuth">type</a> [BasicAuth](https://github.com/studio-b12/gowebdav/blob/master/basicAuth.go?s=94:145#L8)
``` go
type BasicAuth struct {
// contains filtered or unexported fields
}
```
BasicAuth structure holds our credentials
#### <a name="BasicAuth.Authorize">func</a> (\*BasicAuth) [Authorize](https://github.com/studio-b12/gowebdav/blob/master/basicAuth.go?s=461:529#L29)
``` go
func (b *BasicAuth) Authorize(c *Client, 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)
``` 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)
``` 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)
``` 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)
``` go
type Client struct {
// contains filtered or unexported fields
}
```
Client defines our structure
#### <a name="NewClient">func</a> [NewClient](https://github.com/studio-b12/gowebdav/blob/master/client.go?s=902:946#L57)
``` 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)
``` go
func (c *Client) Connect() error
```
Connect connects to our dav server
#### <a name="Client.Copy">func</a> (\*Client) [Copy](https://github.com/studio-b12/gowebdav/blob/master/client.go?s=6960:7028#L314)
``` go
func (c *Client) Copy(oldpath, newpath string, overwrite bool) error
```
Copy copies a file from A to B
#### <a name="Client.Mkdir">func</a> (\*Client) [Mkdir](https://github.com/studio-b12/gowebdav/blob/master/client.go?s=6051:6107#L273)
``` 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=6286:6345#L284)
``` 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=7134:7184#L319)
``` go
func (c *Client) Read(path string) ([]byte, error)
```
Read reads the contents of a remote file
#### <a name="Client.ReadDir">func</a> (\*Client) [ReadDir](https://github.com/studio-b12/gowebdav/blob/master/client.go?s=3126:3186#L131)
``` go
func (c *Client) ReadDir(path string) ([]os.FileInfo, error)
```
ReadDir reads the contents of a remote directory
#### <a name="Client.ReadStream">func</a> (\*Client) [ReadStream](https://github.com/studio-b12/gowebdav/blob/master/client.go?s=7495:7558#L337)
``` go
func (c *Client) ReadStream(path string) (io.ReadCloser, error)
```
ReadStream reads the stream for a given path
#### <a name="Client.Remove">func</a> (\*Client) [Remove](https://github.com/studio-b12/gowebdav/blob/master/client.go?s=5557:5599#L250)
``` go
func (c *Client) Remove(path string) error
```
Remove removes a remote file
#### <a name="Client.RemoveAll">func</a> (\*Client) [RemoveAll](https://github.com/studio-b12/gowebdav/blob/master/client.go?s=5665:5710#L255)
``` go
func (c *Client) RemoveAll(path string) error
```
RemoveAll removes remote files
#### <a name="Client.Rename">func</a> (\*Client) [Rename](https://github.com/studio-b12/gowebdav/blob/master/client.go?s=6794:6864#L309)
``` 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)
``` 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)
``` 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)
``` go
func (c *Client) SetTransport(transport http.RoundTripper)
```
SetTransport exposes the ability to define custom transports
#### <a name="Client.Stat">func</a> (\*Client) [Stat](https://github.com/studio-b12/gowebdav/blob/master/client.go?s=4513:4568#L198)
``` go
func (c *Client) Stat(path string) (os.FileInfo, error)
```
Stat returns the file stats for a specified path
#### <a name="Client.Write">func</a> (\*Client) [Write](https://github.com/studio-b12/gowebdav/blob/master/client.go?s=7849:7918#L352)
``` 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=8320:8400#L374)
``` go
func (c *Client) WriteStream(path string, stream io.Reader, _ os.FileMode) error
```
WriteStream writes a stream
### <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=577:646#L36)
``` go
func (d *DigestAuth) Authorize(c *Client, method string, path string)
```
Authorize the current request
#### <a name="DigestAuth.Pass">func</a> (\*DigestAuth) [Pass](https://github.com/studio-b12/gowebdav/blob/master/digestAuth.go?s=491:525#L31)
``` go
func (d *DigestAuth) Pass() string
```
Pass holds the DigestAuth password
#### <a name="DigestAuth.Type">func</a> (\*DigestAuth) [Type](https://github.com/studio-b12/gowebdav/blob/master/digestAuth.go?s=299:333#L21)
``` go
func (d *DigestAuth) Type() string
```
Type identifies the DigestAuthenticator
#### <a name="DigestAuth.User">func</a> (\*DigestAuth) [User](https://github.com/studio-b12/gowebdav/blob/master/digestAuth.go?s=398:432#L26)
``` go
func (d *DigestAuth) User() string
```
User holds the DigestAuth username
### <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=388:422#L26)
``` go
func (f File) ContentType() string
```
ContentType returns the content type of a file
#### <a name="File.ETag">func</a> (File) [ETag](https://github.com/studio-b12/gowebdav/blob/master/file.go?s=841:868#L51)
``` go
func (f File) ETag() string
```
ETag returns the ETag of a file
#### <a name="File.IsDir">func</a> (File) [IsDir](https://github.com/studio-b12/gowebdav/blob/master/file.go?s=947:973#L56)
``` go
func (f File) IsDir() bool
```
IsDir let us see if a given file is a directory or not
#### <a name="File.ModTime">func</a> (File) [ModTime](https://github.com/studio-b12/gowebdav/blob/master/file.go?s=748:781#L46)
``` go
func (f File) ModTime() time.Time
```
ModTime returns the modified time of a file
#### <a name="File.Mode">func</a> (File) [Mode](https://github.com/studio-b12/gowebdav/blob/master/file.go?s=577:609#L36)
``` go
func (f File) Mode() os.FileMode
```
Mode will return the mode of a given file
#### <a name="File.Name">func</a> (File) [Name](https://github.com/studio-b12/gowebdav/blob/master/file.go?s=290:317#L21)
``` go
func (f File) Name() string
```
Name returns the name of a file
#### <a name="File.Size">func</a> (File) [Size](https://github.com/studio-b12/gowebdav/blob/master/file.go?s=485:511#L31)
``` go
func (f File) Size() int64
```
Size returns the size of a file
#### <a name="File.String">func</a> (File) [String](https://github.com/studio-b12/gowebdav/blob/master/file.go?s=1095:1124#L66)
``` go
func (f File) String() string
```
String lets us see file information
#### <a name="File.Sys">func</a> (File) [Sys](https://github.com/studio-b12/gowebdav/blob/master/file.go?s=1007:1038#L61)
``` go
func (f File) Sys() interface{}
```
Sys ????
### <a name="NoAuth">type</a> [NoAuth](https://github.com/studio-b12/gowebdav/blob/master/client.go?s=442:490#L32)
``` go
type NoAuth struct {
// contains filtered or unexported fields
}
```
NoAuth structure holds our credentials
#### <a name="NoAuth.Authorize">func</a> (\*NoAuth) [Authorize](https://github.com/studio-b12/gowebdav/blob/master/client.go?s=785:850#L53)
``` go
func (n *NoAuth) Authorize(c *Client, 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)
``` 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)
``` 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)
``` go
func (n *NoAuth) User() string
```
User returns the current user
- - -
Generated by [godoc2md](http://godoc.org/github.com/davecheney/godoc2md)

33
basicAuth.go Normal file
View File

@@ -0,0 +1,33 @@
package gowebdav
import (
"encoding/base64"
)
// BasicAuth structure holds our credentials
type BasicAuth struct {
user string
pw string
}
// Type identifies the BasicAuthenticator
func (b *BasicAuth) Type() string {
return "BasicAuth"
}
// User holds the BasicAuth username
func (b *BasicAuth) User() string {
return b.user
}
// Pass holds the BasicAuth password
func (b *BasicAuth) Pass() string {
return b.pw
}
// Authorize the current request
func (b *BasicAuth) Authorize(c *Client, method string, path string) {
a := b.user + ":" + b.pw
auth := "Basic " + base64.StdEncoding.EncodeToString([]byte(a))
c.headers.Set("Authorization", auth)
}

195
client.go
View File

@@ -2,47 +2,105 @@ package gowebdav
import ( import (
"bytes" "bytes"
"encoding/base64"
"encoding/xml" "encoding/xml"
"io" "io"
"net/http" "net/http"
"net/url"
"os" "os"
pathpkg "path"
"strings" "strings"
"time" "time"
) )
// Client defines our structure
type Client struct { type Client struct {
root string root string
headers http.Header headers http.Header
c *http.Client c *http.Client
auth Authenticator
} }
func NewClient(uri string, user string, pw string) *Client { // Authenticator stub
c := &Client{uri, make(http.Header), &http.Client{}} type Authenticator interface {
Type() string
if len(user) > 0 && len(pw) > 0 { User() string
a := user + ":" + pw Pass() string
auth := "Basic " + base64.StdEncoding.EncodeToString([]byte(a)) Authorize(*Client, string, string)
c.headers.Add("Authorization", auth)
}
c.root = FixSlash(c.root)
return c
} }
// NoAuth structure holds our credentials
type NoAuth struct {
user string
pw string
}
// Type identifies the authenticator
func (n *NoAuth) Type() string {
return "NoAuth"
}
// User returns the current user
func (n *NoAuth) User() string {
return n.user
}
// Pass returns the current password
func (n *NoAuth) Pass() string {
return n.pw
}
// Authorize the current request
func (n *NoAuth) Authorize(c *Client, method string, path string) {
}
// NewClient creates a new instance of client
func NewClient(uri, user, pw string) *Client {
return &Client{FixSlash(uri), make(http.Header), &http.Client{}, &NoAuth{user, pw}}
}
// SetHeader lets us set arbitrary headers for a given client
func (c *Client) SetHeader(key, value string) {
c.headers.Add(key, value)
}
// SetTimeout exposes the ability to set a time limit for requests
func (c *Client) SetTimeout(timeout time.Duration) {
c.c.Timeout = timeout
}
// SetTransport exposes the ability to define custom transports
func (c *Client) SetTransport(transport http.RoundTripper) {
c.c.Transport = transport
}
// Connect connects to our dav server
func (c *Client) Connect() error { func (c *Client) Connect() error {
rs, err := c.options("/") rs, err := c.options("/")
if err == nil { if err != nil {
rs.Body.Close() return err
}
if rs.StatusCode != 200 || (rs.Header.Get("Dav") == "" && rs.Header.Get("DAV") == "") { err = rs.Body.Close()
if err != nil {
return err
}
if rs.StatusCode == 401 && c.auth.Type() == "NoAuth" {
if strings.Index(rs.Header.Get("Www-Authenticate"), "Digest") > -1 {
c.auth = &DigestAuth{c.auth.User(), c.auth.Pass(), digestParts(rs)}
} else if strings.Index(rs.Header.Get("Www-Authenticate"), "Basic") > -1 {
c.auth = &BasicAuth{c.auth.User(), c.auth.Pass()}
} else {
return newPathError("Authorize", c.root, rs.StatusCode)
}
return c.Connect()
} else if rs.StatusCode == 401 {
return newPathError("Authorize", c.root, rs.StatusCode)
} else if rs.StatusCode != 200 || (rs.Header.Get("Dav") == "" && rs.Header.Get("DAV") == "") {
return newPathError("Connect", c.root, rs.StatusCode) return newPathError("Connect", c.root, rs.StatusCode)
} }
_, err = c.ReadDir("/") return nil
}
return err
} }
type props struct { type props struct {
@@ -50,8 +108,11 @@ type props struct {
Name string `xml:"DAV: prop>displayname,omitempty"` Name string `xml:"DAV: prop>displayname,omitempty"`
Type xml.Name `xml:"DAV: prop>resourcetype>collection,omitempty"` Type xml.Name `xml:"DAV: prop>resourcetype>collection,omitempty"`
Size string `xml:"DAV: prop>getcontentlength,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"` Modified string `xml:"DAV: prop>getlastmodified,omitempty"`
} }
type response struct { type response struct {
Href string `xml:"DAV: href"` Href string `xml:"DAV: href"`
Props []props `xml:"DAV: propstat"` Props []props `xml:"DAV: propstat"`
@@ -59,13 +120,14 @@ type response struct {
func getProps(r *response, status string) *props { func getProps(r *response, status string) *props {
for _, prop := range r.Props { for _, prop := range r.Props {
if strings.Index(prop.Status, status) != -1 { if strings.Contains(prop.Status, status) {
return &prop return &prop
} }
} }
return nil return nil
} }
// ReadDir reads the contents of a remote directory
func (c *Client) ReadDir(path string) ([]os.FileInfo, error) { func (c *Client) ReadDir(path string) ([]os.FileInfo, error) {
path = FixSlashes(path) path = FixSlashes(path)
files := make([]os.FileInfo, 0) files := make([]os.FileInfo, 0)
@@ -84,17 +146,22 @@ 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 {
f.name = pathpkg.Base(ps)
} else {
f.name = p.Name f.name = p.Name
}
f.path = path + f.name f.path = path + f.name
f.modified = parseModified(&p.Modified)
f.etag = p.ETag
f.contentType = p.ContentType
if p.Type.Local == "collection" { if p.Type.Local == "collection" {
f.path += "/" f.path += "/"
f.size = 0 f.size = 0
f.modified = time.Unix(0, 0)
f.isdir = true f.isdir = true
} else { } else {
f.size = parseInt64(&p.Size) f.size = parseInt64(&p.Size)
f.modified = parseModified(&p.Modified)
f.isdir = false f.isdir = false
} }
@@ -111,6 +178,8 @@ func (c *Client) ReadDir(path string) ([]os.FileInfo, error) {
<d:displayname/> <d:displayname/>
<d:resourcetype/> <d:resourcetype/>
<d:getcontentlength/> <d:getcontentlength/>
<d:getcontenttype/>
<d:getetag/>
<d:getlastmodified/> <d:getlastmodified/>
</d:prop> </d:prop>
</d:propfind>`, </d:propfind>`,
@@ -119,20 +188,23 @@ func (c *Client) ReadDir(path string) ([]os.FileInfo, error) {
if err != nil { if err != nil {
if _, ok := err.(*os.PathError); !ok { if _, ok := err.(*os.PathError); !ok {
err = &os.PathError{"ReadDir", path, err} err = newPathErrorErr("ReadDir", path, err)
} }
} }
return files, err return files, err
} }
// Stat returns the file stats for a specified path
func (c *Client) Stat(path string) (os.FileInfo, error) { func (c *Client) Stat(path string) (os.FileInfo, error) {
var f *File = nil var f *File
parse := func(resp interface{}) error { parse := func(resp interface{}) error {
r := resp.(*response) r := resp.(*response)
if p := getProps(r, "200"); p != nil && f == nil { if p := getProps(r, "200"); p != nil && f == nil {
f = new(File) f = new(File)
f.name = p.Name f.name = p.Name
f.path = path f.path = path
f.etag = p.ETag
f.contentType = p.ContentType
if p.Type.Local == "collection" { if p.Type.Local == "collection" {
if !strings.HasSuffix(f.path, "/") { if !strings.HasSuffix(f.path, "/") {
@@ -158,6 +230,8 @@ func (c *Client) Stat(path string) (os.FileInfo, error) {
<d:displayname/> <d:displayname/>
<d:resourcetype/> <d:resourcetype/>
<d:getcontentlength/> <d:getcontentlength/>
<d:getcontenttype/>
<d:getetag/>
<d:getlastmodified/> <d:getlastmodified/>
</d:prop> </d:prop>
</d:propfind>`, </d:propfind>`,
@@ -166,40 +240,47 @@ func (c *Client) Stat(path string) (os.FileInfo, error) {
if err != nil { if err != nil {
if _, ok := err.(*os.PathError); !ok { if _, ok := err.(*os.PathError); !ok {
err = &os.PathError{"ReadDir", path, err} err = newPathErrorErr("ReadDir", path, err)
} }
} }
return f, err return f, err
} }
// Remove removes a remote file
func (c *Client) Remove(path string) error { func (c *Client) Remove(path string) error {
return c.RemoveAll(path) return c.RemoveAll(path)
} }
// RemoveAll removes remote files
func (c *Client) RemoveAll(path string) error { func (c *Client) RemoveAll(path string) error {
rs, err := c.req("DELETE", path, nil, nil) rs, err := c.req("DELETE", path, nil, nil)
if err != nil { if err != nil {
return newPathError("Remove", path, 400) return newPathError("Remove", path, 400)
} }
rs.Body.Close() err = rs.Body.Close()
if err != nil {
if rs.StatusCode == 200 || rs.StatusCode == 404 { return err
return nil
} else {
return newPathError("Remove", path, rs.StatusCode)
} }
if rs.StatusCode == 200 || rs.StatusCode == 204 || rs.StatusCode == 404 {
return nil
}
return newPathError("Remove", path, rs.StatusCode)
} }
// Mkdir makes a directory
func (c *Client) Mkdir(path string, _ os.FileMode) error { func (c *Client) Mkdir(path string, _ os.FileMode) error {
path = FixSlashes(path) path = FixSlashes(path)
status := c.mkcol(path) status := c.mkcol(path)
if status == 201 { if status == 201 {
return nil return nil
} else {
return newPathError("Mkdir", path, status)
} }
return newPathError("Mkdir", path, status)
} }
// MkdirAll like mkdir -p, but for webdav
func (c *Client) MkdirAll(path string, _ os.FileMode) error { func (c *Client) MkdirAll(path string, _ os.FileMode) error {
path = FixSlashes(path) path = FixSlashes(path)
status := c.mkcol(path) status := c.mkcol(path)
@@ -224,61 +305,77 @@ func (c *Client) MkdirAll(path string, _ os.FileMode) error {
return newPathError("MkdirAll", path, status) return newPathError("MkdirAll", path, status)
} }
func (c *Client) Rename(oldpath string, newpath string, overwrite bool) error { // 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) return c.copymove("MOVE", oldpath, newpath, overwrite)
} }
func (c *Client) Copy(oldpath string, newpath string, overwrite bool) error { // 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) return c.copymove("COPY", oldpath, newpath, overwrite)
} }
// Read reads the contents of a remote file
func (c *Client) Read(path string) ([]byte, error) { func (c *Client) Read(path string) ([]byte, error) {
if stream, err := c.ReadStream(path); err == nil { var stream io.ReadCloser
defer stream.Close() var err error
buf := new(bytes.Buffer)
buf.ReadFrom(stream) if stream, err = c.ReadStream(path); err != nil {
return buf.Bytes(), nil
} else {
return nil, err 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) { func (c *Client) ReadStream(path string) (io.ReadCloser, error) {
rs, err := c.req("GET", path, nil, nil) rs, err := c.req("GET", path, nil, nil)
if err != nil { if err != nil {
return nil, newPathErrorErr("ReadStream", path, err) return nil, newPathErrorErr("ReadStream", path, err)
} }
if rs.StatusCode == 200 {
return rs.Body, nil return rs.Body, nil
}
rs.Body.Close()
return nil, newPathError("ReadStream", path, rs.StatusCode)
} }
// Write writes data to a given path
func (c *Client) Write(path string, data []byte, _ os.FileMode) error { 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))
switch s { switch s {
case 200, 201: case 200, 201, 204:
return nil return nil
case 409: case 409:
if idx := strings.LastIndex(path, "/"); idx == -1 { if i := strings.LastIndex(path, "/"); i > -1 {
// faulty root if err := c.MkdirAll(path[0:i+1], 0755); err == nil {
return newPathError("Write", path, 500)
} else {
if err := c.MkdirAll(path[0:idx+1], 0755); err == nil {
s = c.put(path, bytes.NewReader(data)) s = c.put(path, bytes.NewReader(data))
if s == 200 || s == 201 { if s == 200 || s == 201 || s == 204 {
return nil return nil
} }
} }
} }
} }
return newPathError("Write", path, s) return newPathError("Write", path, s)
} }
// WriteStream writes a stream
func (c *Client) WriteStream(path string, stream io.Reader, _ os.FileMode) error { func (c *Client) WriteStream(path string, stream io.Reader, _ os.FileMode) error {
// TODO check if parent collection exists // TODO check if parent collection exists
s := c.put(path, stream) s := c.put(path, stream)
switch s { switch s {
case 200, 201: case 200, 201, 204:
return nil return nil
default: default:

245
cmd/gowebdav/main.go Normal file
View File

@@ -0,0 +1,245 @@
package main
import (
"errors"
"flag"
"fmt"
d "github.com/studio-b12/gowebdav"
"io"
"os"
"os/user"
"path/filepath"
"strings"
)
func main() {
root := flag.String("root", os.Getenv("ROOT"), "WebDAV Endpoint [ENV.ROOT]")
usr := flag.String("user", os.Getenv("USER"), "User [ENV.USER]")
pw := flag.String("pw", os.Getenv("PASSWORD"), "Password [ENV.PASSWORD]")
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 l := len(flag.Args()); l == 0 || l > 2 {
fail("Unsupported arguments")
}
if *pw == "" {
if u, p := d.ReadConfig(*root, *netrc); u != "" && p != "" {
usr = &u
pw = &p
}
}
c := d.NewClient(*root, *usr, *pw)
if err := c.Connect(); err != nil {
fail(fmt.Sprintf("Failed to connect due to: %s", err.Error()))
}
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 {
if u, e := user.Current(); e != nil {
return u.HomeDir
}
return os.Getenv("HOME")
}
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 = filepath.Join(".", p0)
}
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 {
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,
}
}
return nopCloser{strings.NewReader(pathOrString)}, nil
}
type nopCloser struct {
io.Reader
}
func (nopCloser) Close() error {
return nil
}

83
digestAuth.go Normal file
View File

@@ -0,0 +1,83 @@
package gowebdav
import (
"crypto/md5"
"crypto/rand"
"encoding/hex"
"fmt"
"io"
"net/http"
"strings"
)
// DigestAuth structure holds our credentials
type DigestAuth struct {
user string
pw string
digestParts map[string]string
}
// Type identifies the DigestAuthenticator
func (d *DigestAuth) Type() string {
return "DigestAuth"
}
// User holds the DigestAuth username
func (d *DigestAuth) User() string {
return d.user
}
// Pass holds the DigestAuth password
func (d *DigestAuth) Pass() string {
return d.pw
}
// Authorize the current request
func (d *DigestAuth) Authorize(c *Client, method string, path string) {
d.digestParts["uri"] = path
d.digestParts["method"] = method
d.digestParts["username"] = d.user
d.digestParts["password"] = d.pw
c.headers.Set("Authorization", getDigestAuthorization(d.digestParts))
}
func digestParts(resp *http.Response) map[string]string {
result := map[string]string{}
if len(resp.Header["Www-Authenticate"]) > 0 {
wantedHeaders := []string{"nonce", "realm", "qop", "opaque"}
responseHeaders := strings.Split(resp.Header["Www-Authenticate"][0], ",")
for _, r := range responseHeaders {
for _, w := range wantedHeaders {
if strings.Contains(r, w) {
result[w] = strings.Split(r, `"`)[1]
}
}
}
}
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.
ha1 := getMD5(d["username"] + ":" + d["realm"] + ":" + d["password"])
ha2 := getMD5(d["method"] + ":" + d["uri"])
nonceCount := 00000001
cnonce := getCnonce()
response := getMD5(fmt.Sprintf("%s:%s:%v:%s:%s:%s", ha1, d["nonce"], nonceCount, cnonce, d["qop"], ha2))
authorization := fmt.Sprintf(`Digest username="%s", realm="%s", nonce="%s", uri="%s", cnonce="%s", nc="%v", qop="%s", response="%s", opaque="%s"`,
d["username"], d["realm"], d["nonce"], d["uri"], cnonce, nonceCount, d["qop"], response, d["opaque"])
return authorization
}

3
doc.go Normal file
View File

@@ -0,0 +1,3 @@
// Package gowebdav is a WebDAV client library with a command line tool
// included.
package gowebdav

28
file.go
View File

@@ -6,47 +6,67 @@ import (
"time" "time"
) )
// File is our structure for a given file
type File struct { type File struct {
path string path string
name string name string
contentType string
size int64 size int64
modified time.Time modified time.Time
etag string
isdir bool isdir bool
} }
// Name returns the name of a file
func (f File) Name() string { func (f File) Name() string {
return f.name 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 { func (f File) Size() int64 {
return f.size return f.size
} }
// Mode will return the mode of a given file
func (f File) Mode() os.FileMode { func (f File) Mode() os.FileMode {
// TODO check webdav perms // TODO check webdav perms
if f.isdir { if f.isdir {
return 0775 | os.ModeDir return 0775 | os.ModeDir
} else {
return 0664
} }
return 0664
} }
// ModTime returns the modified time of a file
func (f File) ModTime() time.Time { func (f File) ModTime() time.Time {
return f.modified 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 { func (f File) IsDir() bool {
return f.isdir return f.isdir
} }
// Sys ????
func (f File) Sys() interface{} { func (f File) Sys() interface{} {
return nil return nil
} }
// String lets us see file information
func (f File) String() string { func (f File) String() string {
if f.isdir { if f.isdir {
return fmt.Sprintf("Dir : '%s' - '%s'", f.path, f.name) return fmt.Sprintf("Dir : '%s' - '%s'", f.path, f.name)
} else {
return fmt.Sprintf("File: '%s' SIZE: %d MODIFIED: %s", f.path, f.size, f.modified.String())
} }
return fmt.Sprintf("File: '%s' SIZE: %d MODIFIED: %s ETAG: %s CTYPE: %s", f.path, f.size, f.modified.String(), f.etag, f.contentType)
} }

View File

@@ -1,181 +0,0 @@
package main
import (
"errors"
"flag"
"fmt"
d "gowebdav"
"io"
"os"
"strings"
)
func Fail(err interface{}) {
if err != nil {
fmt.Println(err)
} else {
fmt.Println("Usage: client FLAGS ARGS")
fmt.Println("Flags:")
flag.PrintDefaults()
fmt.Println("Method <ARGS>")
fmt.Println(" LS | LIST | PROPFIND <PATH>")
fmt.Println(" RM | DELETE | DEL <PATH>")
fmt.Println(" MKDIR | MKCOL <PATH>")
fmt.Println(" MKDIRALL | MKCOLALL <PATH>")
fmt.Println(" MV | MOVE | RENAME <OLD_PATH> <NEW_PATH>")
fmt.Println(" CP | COPY <OLD_PATH> <NEW_PATH>")
fmt.Println(" GET | PULL | READ <PATH>")
fmt.Println(" PUT | PUSH | WRITE <PATH> <FILE>")
fmt.Println(" STAT <PATH>")
}
os.Exit(-1)
}
func writeFile(path string, bytes []byte, mode os.FileMode) error {
f, err := os.Create(path)
if err != nil {
return err
}
defer f.Close()
_, err = f.Write(bytes)
return err
}
func main() {
root := flag.String("root", "URL", "WebDAV Endpoint")
usr := flag.String("user", "", "user")
pw := flag.String("pw", "", "password")
m := flag.String("X", "GET", "Method ...")
flag.Parse()
if *root == "URL" {
Fail(nil)
}
M := strings.ToUpper(*m)
m = &M
c := d.NewClient(*root, *usr, *pw)
if err := c.Connect(); err != nil {
Fail(fmt.Sprintf("Failed to connect due to: %s", err.Error()))
}
alen := len(flag.Args())
if alen == 1 {
path := flag.Args()[0]
switch *m {
case "LS", "LIST", "PROPFIND":
if files, err := c.ReadDir(path); err == nil {
fmt.Println(fmt.Sprintf("ReadDir: '%s' entries: %d ", path, len(files)))
for _, f := range files {
fmt.Println(f)
}
} else {
fmt.Println(err)
}
case "STAT":
if file, err := c.Stat(path); err == nil {
fmt.Println(file)
} else {
fmt.Println(err)
}
case "GET", "PULL", "READ":
if bytes, err := c.Read(path); err == nil {
if lidx := strings.LastIndex(path, "/"); lidx != -1 {
path = path[lidx+1:]
}
if err := writeFile(path, bytes, 0644); err != nil {
fmt.Println(err)
} else {
fmt.Println(fmt.Sprintf("Written %d bytes to: %s", len(bytes), path))
}
} else {
fmt.Println(err)
}
case "DELETE", "RM", "DEL":
if err := c.Remove(path); err != nil {
fmt.Println(err)
} else {
fmt.Println("Remove: " + path)
}
case "MKCOL", "MKDIR":
if err := c.Mkdir(path, 0); err != nil {
fmt.Println(err)
} else {
fmt.Println("MkDir: " + path)
}
case "MKCOLALL", "MKDIRALL":
if err := c.MkdirAll(path, 0); err != nil {
fmt.Println(err)
} else {
fmt.Println("MkDirAll: " + path)
}
default:
Fail(nil)
}
} else if alen == 2 {
a0 := flag.Args()[0]
a1 := flag.Args()[1]
switch *m {
case "RENAME", "MV", "MOVE":
if err := c.Rename(a0, a1, true); err != nil {
fmt.Println(err)
} else {
fmt.Println("Rename: " + a0 + " -> " + a1)
}
case "COPY", "CP":
if err := c.Copy(a0, a1, true); err != nil {
fmt.Println(err)
} else {
fmt.Println("Copy: " + a0 + " -> " + a1)
}
case "PUT", "PUSH", "WRITE":
stream, err := getStream(a1)
if err != nil {
Fail(err)
}
if err := c.WriteStream(a0, stream, 0644); err != nil {
fmt.Println(err)
} else {
fmt.Println(fmt.Sprintf("Written: '%s' -> %s", a1, a0))
}
default:
Fail(nil)
}
} else {
Fail(nil)
}
}
func getStream(pathOrString string) (io.ReadCloser, error) {
fi, err := os.Stat(pathOrString)
if err == nil {
if fi.IsDir() {
return nil, &os.PathError{"Open", pathOrString, errors.New("Path: '" + pathOrString + "' is a directory")}
}
f, err := os.Open(pathOrString)
if err == nil {
return f, nil
}
return nil, &os.PathError{"Open", pathOrString, err}
} else {
return nopCloser{strings.NewReader(pathOrString)}, nil
}
}
type nopCloser struct {
io.Reader
}
func (nopCloser) Close() error {
return nil
}

54
netrc.go Normal file
View File

@@ -0,0 +1,54 @@
package gowebdav
import (
"bufio"
"fmt"
"net/url"
"os"
"regexp"
"strings"
)
func parseLine(s string) (login, pass string) {
fields := strings.Fields(s)
for i, f := range fields {
if f == "login" {
login = fields[i+1]
}
if f == "password" {
pass = fields[i+1]
}
}
return login, pass
}
// ReadConfig reads login and password configuration from ~/.netrc
// machine foo.com login username password 123456
func ReadConfig(uri, netrc string) (string, string) {
u, err := url.Parse(uri)
if err != nil {
return "", ""
}
file, err := os.Open(netrc)
if err != nil {
return "", ""
}
defer file.Close()
re := fmt.Sprintf(`^.*machine %s.*$`, u.Host)
scanner := bufio.NewScanner(file)
for scanner.Scan() {
s := scanner.Text()
matched, err := regexp.MatchString(re, s)
if err != nil {
return "", ""
}
if matched {
return parseLine(s)
}
}
return "", ""
}

View File

@@ -1,18 +1,20 @@
package gowebdav package gowebdav
import ( import (
"errors"
"fmt" "fmt"
"io" "io"
"net/http" "net/http"
"strings" "strings"
) )
func (c *Client) req(method string, 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) {
r, err := http.NewRequest(method, Join(c.root, path), body) r, err := http.NewRequest(method, PathEscape(Join(c.root, path)), body)
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)
@@ -28,10 +30,10 @@ func (c *Client) req(method string, path string, body io.Reader, intercept func(
func (c *Client) mkcol(path string) int { func (c *Client) mkcol(path string) int {
rs, err := c.req("MKCOL", path, nil, nil) rs, err := c.req("MKCOL", path, nil, nil)
defer rs.Body.Close()
if err != nil { if err != nil {
return 400 return 400
} }
rs.Body.Close()
if rs.StatusCode == 201 || rs.StatusCode == 405 { if rs.StatusCode == 201 || rs.StatusCode == 405 {
return 201 return 201
@@ -59,13 +61,13 @@ func (c *Client) propfind(path string, self bool, body string, resp interface{},
// TODO add support for 'gzip,deflate;q=0.8,q=0.7' // TODO add support for 'gzip,deflate;q=0.8,q=0.7'
rq.Header.Add("Accept-Encoding", "") rq.Header.Add("Accept-Encoding", "")
}) })
defer rs.Body.Close()
if err != nil { if err != nil {
return err return err
} }
defer rs.Body.Close()
if rs.StatusCode != 207 { if rs.StatusCode != 207 {
return errors.New(fmt.Sprintf("%s - %s %s", rs.Status, "PROPFIND", path)) return fmt.Errorf("%s - %s %s", rs.Status, "PROPFIND", path)
} }
return parseXML(rs.Body, resp, parse) return parseXML(rs.Body, resp, parse)
@@ -88,9 +90,7 @@ 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:
@@ -109,9 +109,10 @@ func (c *Client) copymove(method string, oldpath string, newpath string, overwri
func (c *Client) put(path string, stream io.Reader) int { func (c *Client) put(path string, stream io.Reader) int {
rs, err := c.req("PUT", path, stream, nil) rs, err := c.req("PUT", path, stream, nil)
defer rs.Body.Close()
if err != nil { if err != nil {
return 400 return 400
} }
rs.Body.Close()
return rs.StatusCode return rs.StatusCode
} }

View File

@@ -3,9 +3,9 @@ package gowebdav
import ( import (
"bytes" "bytes"
"encoding/xml" "encoding/xml"
"errors"
"fmt" "fmt"
"io" "io"
"net/url"
"os" "os"
"strconv" "strconv"
"strings" "strings"
@@ -17,13 +17,31 @@ func log(msg interface{}) {
} }
func newPathError(op string, path string, statusCode int) error { func newPathError(op string, path string, statusCode int) error {
return &os.PathError{op, path, errors.New(fmt.Sprintf("%d", statusCode))} return &os.PathError{
Op: op,
Path: path,
Err: fmt.Errorf("%d", statusCode),
}
} }
func newPathErrorErr(op string, path string, err error) error { func newPathErrorErr(op string, path string, err error) error {
return &os.PathError{op, path, err} return &os.PathError{
Op: op,
Path: path,
Err: err,
}
} }
// PathEscape escapes all segemnts of a given path
func PathEscape(path string) string {
s := strings.Split(path, "/")
for i, e := range s {
s[i] = url.PathEscape(e)
}
return strings.Join(s, "/")
}
// FixSlash appends a trailing / to our string
func FixSlash(s string) string { func FixSlash(s string) string {
if !strings.HasSuffix(s, "/") { if !strings.HasSuffix(s, "/") {
s += "/" s += "/"
@@ -31,6 +49,7 @@ func FixSlash(s string) string {
return s return s
} }
// FixSlashes appends and prepends a / if they are missing
func FixSlashes(s string) string { func FixSlashes(s string) string {
if s[0] != '/' { if s[0] != '/' {
s = "/" + s s = "/" + s
@@ -38,13 +57,16 @@ func FixSlashes(s string) string {
return FixSlash(s) return FixSlash(s)
} }
// Join joins two paths
func Join(path0 string, path1 string) string { func Join(path0 string, path1 string) string {
return strings.TrimSuffix(path0, "/") + "/" + strings.TrimPrefix(path1, "/") return strings.TrimSuffix(path0, "/") + "/" + strings.TrimPrefix(path1, "/")
} }
// String pulls a string out of our io.Reader
func String(r io.Reader) string { func String(r io.Reader) string {
buf := new(bytes.Buffer) buf := new(bytes.Buffer)
buf.ReadFrom(r) // TODO - make String return an error as well
_, _ = buf.ReadFrom(r)
return buf.String() return buf.String()
} }

View File

@@ -1,6 +1,10 @@
package gowebdav package gowebdav
import "testing" import (
"fmt"
"net/url"
"testing"
)
func TestJoin(t *testing.T) { func TestJoin(t *testing.T) {
eq(t, "/", "", "") eq(t, "/", "", "")
@@ -16,3 +20,26 @@ func eq(t *testing.T, expected string, s0 string, s1 string) {
t.Error("For", "'"+s0+"','"+s1+"'", "expeted", "'"+expected+"'", "got", "'"+s+"'") 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())
}
}